Categories
JavaScript Software Patterns

Code-Splitting in WordPress

I’m sure you’ve been here before: you’ve just been assigned to this large project on a major client website. It includes a cohesive component-driven design system, and strives to reuse and inherit those components throughout the application.

You think to yourself, “This is going to be great!”

You open up your code editor of choice, and gleefully start the new project (this articles assumes a 10up Theme Scaffold structure, but the principles apply anywhere).

You’re ready to make some engineering magic.

Fast-forward 8 weeks and you’re about done. Each component is nestled nicely in a theme partial, and equally segmented in your CSS and JS folders, under a unified naming scheme.

Everything works great. Except one small, or shall we say large, problem: your JavaScript file is just over 2 megabytes in size, even when it’s compressed.

We’ve all been here, and there’s nothing like the feeling of months of quality module building getting nullified by poor performance.

It’s usually at this point that we begin wishing we were using React as our main application infrastructure, depressingly watching as other applications, just like ours, code-split their dependencies on the fly and deliver (if done correctly) resources to the browser half the size of ours.

The Problem

Take heart though. In many cases, the performance gains achieved by JS frameworks in terms of asset size are lost by the sheer size of the frameworks themselves, often-times hitting sizes upwards of 2-3mb.

Still, code-splitting asset delivery is the great envy of the WordPress stack. But delivering only what’s necessary to the user is an uphill battle, and usually results in multiple conditionals throughout the code-base, if not just as many JavaScript files.

When modern WordPress sites rely on JavaScript almost as much as they do PHP, we begin to dance with two resources that are not inherently aware of each other. Conditionals duplicate between languages, and if we try to achieve true dynamic asset delivery we usually pay the price in terms of technical debt.

Recently, I experimented with a method for achieving on-demand components while keeping complexity to a minimum. I’d love to hear your feedback!

Code-Splitting to the Rescue

Webpack comes with a great feature known as code-splitting. Without getting into too much configuration detail, you can enable code-splitting in the 10up Theme Scaffold with 3 basic changes:

1. Update Output Config

The first thing we need to do is configure where Webpack should store our modules. Within /config/webpack.common.js, update the output parameter with the following directives:

module.exports = {
  output: {
    chunkFilename: '[name].bundle.js',
    publicPath: '/wp-content/themes/{your-theme-name}/{your-build-folder}/'
  }
}

There are two things happening here:

  1. We’re telling Webpack what kind of name to use for chunk files. The [name] here is a Webpack variable that gets replaced at build time.
  2. We’re telling Webpack where these chunk files are located (it will default to the build destination).

2. Configure Dynamic Imports

The second thing we’ll do is add the @babel/plugin-syntax-dynamic-import dependency to our project. Do this by running:

npm install @babel/plugin-syntax-dynamic-import -D

Next, we need to make sure that Webpack knows that dynamic imports require Promise support (since internally it will fetch the module via a Promise request). We can do this with the @babel/preset-env preset by defining a couple of options.

In your /config/webpack.commons.js ensure the following is present on your babel-loader options (this might also be defined in your .babelrc file, so check there too):

loader: 'babel-loader',
options: {
  presets: [
    [
      '@babel/preset-env',
      {								          
        'useBuiltIns': 'usage',								 
        'corejs': 3,
       }
    ]
  ],
}

This tells the @babel/preset-env preset to include polyfills on a per-use basis, as encountered in your code files. When the babel-loader comes across our dynamic import, it will automatically include a Promise polyfill across the application.

3. Create an On-Demand Component

Creating an on-demand component is a fancy way of telling the JavaScript to scan the page (after loading) for any pre-defined identifier that we can check against to automatically fetch required dependencies.

For example, I recently worked on a project where we had a Subscription component. It was fairly large in terms of size, but was only present on certain pages.

Normally, in this scenario, you’d probably build the component as a separate file, compile it as a separate Webpack entry, and then do your conditional checks via PHP before loading it on the page.

With dynamic module imports, all this can happen client-side with minimal impact to file-size. Let me show you what I mean:

Let’s assume a directory structure like this:

Inside frontend.js we have all our normal logic like any normal application. The /components/ directory is where we keep (you guessed it) all our on-demand components.

Inside frontend.js we import these components like normal:

import './components/SubscribeForm';

Normally, this would end up importing the entire component, but because we are adopting a dynamic loading infrastructure, the actual file being imported is minimal.

Let’s take a look at what’s inside /components/SubscribeForm:

The component includes a lot of inner-workings, which is normal of most components. But by having the entry file as index.js, we can name the actual component file (where the real stuff happens) the same as the parent folder.

This keeps things more clear, and if you were looking at this folder without context, you might assume SubscribeForm.js is the actual component, while index.js is simply the entry file.

And this structure makes on-demand components so simple to maintain. Each component is, essentially, an isolated application. We simply include it in our main stack by calling the index.js file (this is assumed when calling a folder, as we did in frontend.js). Everything else in the component folder handles itself.

So, let’s see what’s in index.js:

/**
 * Dynamically import form dependencies and initialize.
 *
 * @type {NodeListOf<Element>}
 */
const forms = document.querySelectorAll( '.SubscribeForm' );
if ( forms.length ) {
  import( './SubscribeForm' )
    .then( ( { default: SubscribeForm } ) => {
      SubscribeForm.initialize( forms );
    }
}

Here we are doing three things:

  1. Getting all elements in the document with the class SubscribeForm.
  2. If the length of forms is greater than 0 (0 is interpreted as false, anything more is true), then we import the main SubscribeForm.js file from the relative root of the component folder, dynamically.
  3. Upon success, and with access to the new component, we initialize all the forms on the page.

In Summary

There’s more to dissect here, and this is an admittedly over-simplified tutorial on dynamic imports — but the possibilities of adopting this pattern are great:

  1. A unified component-loading pattern, in the form of a helper method, could simplify component imports. Imagine something like Components.onDemand( [ 'SubscribeForm', 'OtherComponent', 'Another' ] ).
  2. The first application JavaScript file delivered to the client could theoretically be in the ~10kb range, assuming every component was dynamic.

And there’s more, I’m sure. When I originally built this component system, I matched the patterns across PHP and JS, so that each component utilized classes and shared naming conventions, lowering the cost of shifting between backend and frontend code.

If you’ve done something similar to this before, I’d love to hear about it!

Leave a Reply

Your email address will not be published. Required fields are marked *