All you need to know about JavaScript code splitting

JavaScript code splitting

Modern sites often combine all of their JavaScript into a single, large main.js script. This regularly contains the scripts for all your pages or routes, even if users only need a small portion for the page they're viewing. 

When JavaScript is served this way, the loading performance of your web pages can suffer – especially with responsive web design on mobile devices. So let's fix it by implementing JavaScript code splitting.

 What problem does code splitting solve? 

When a web browser sees a <script> it needs to spend time downloading and processing the JavaScript you're referencing. This can feel fast on high-end devices but loading, parsing and executing unused JavaScript code can take a while on average mobile devices with a slower network and slower CPU. If you've ever had to log on to coffee-shop or hotel WiFi, you know slow network experiences can happen to everyone.

Each second spent waiting on JavaScript to finish booting up can delay how soon users are able to interact with your experience. This is particularly the case if your UX relies on JS for critical components or even just attaching event handlers for simple pieces of UI.

Do I need to bother with code splitting?

It is definitely worth asking yourself whether you need to code-split. If your site requires JavaScript for interactive content (for features like menu drawers and carousels) or is a single-page application relying on JavaScript frameworks to render UI, the answer is likely 'yes'. Whether code splitting is worthwhile for your site is a question you'll need to answer yourself. You understand your architecture and how your site loads best. Thankfully there are tools available to help you here.

Get help

For those new to JavaScript code splitting, Lighthouse – the Audits panel in Chrome Developer Tools – can help shine a light on whether this is a problem for your site. The audit you'll want to look for is Reduce JavaScript Execution Time (documented here). This audit highlights all of the scripts on your page that can delay a user interacting with it.

PageSpeed Insights is an online tool that can also highlight your site's performance – and includes lab data from Lighthouse and real-world data on your site performance from the Chrome User Experience Report.

Code coverage in Chrome Developer Tools

If it looks like you have costly scripts that could be better split, the next tool to look at is the Code Coverage feature in the Chrome Developer Tools (DevTools>top-right menu>More tools> Coverage). This measures how much unused JavaScript (and CSS) is in your page. For each script summarised, DevTools will show the 'unused bytes'. This is code you can consider splitting out and lazy-loading when the user needs it.

The different kinds of code splitting

There are a few different approaches you can take when it comes to code splitting JavaScript. How much these apply to your site tends to vary depending on whether you wish to split up page/application 'logic' or split up libraries/frameworks from other 'vendors'.

Dynamic code splitting: Many of us 'statically' import JavaScript modules and dependencies so that they are bundled together into one file at build time. 'Dynamic' code splitting adds the ability to define points in your JavaScript that you would like to split and lazy-load as needed. Modern JavaScript uses the dynamic import() statement to achieve this. We'll cover this more shortly.

Vendor code splitting: The frameworks and libraries you rely on (e.g. React, Angular, Vue or Lodash) are unlikely to change in the scripts you send down to your users, often as the 'logic' for your site. To reduce the negative impact of cache invalidation for users returning to your site, you can split your 'vendors' into a separate script.

Entry-point code splitting: Entries are starting points in your site or app that a tool like Webpack can look at to build up your dependency tree. Splitting by entries is useful for pages where client-side routing is not used or you are relying on a combination of server and client-side rendering.

For our purposes in this article, we'll be concentrating on dynamic code splitting.

Get hands on with code splitting

Let's optimise the JavaScript performance of a simple application that sorts three numbers through code splitting – this is an app by my colleague Houssein Djirdeh. The workflow we'll be using to make our JavaScript load quickly is measure, optimise and monitor. Start here.

Measure performance

Before attempting to add any optimisations, we're first going to measure the performance of our JavaScript. As the magic sorter app is hosted on Glitch, we'll be using its coding environment. Here's how to go about it:

  • Click the Show Live button.
  • Open the DevTools by pressing CMD+OPTION+i / CTRL+SHIFT +i.
  • Select the Network panel.
  • Make sure Disable Cache is checked and reload the app.

This simple application seems to be using 71.2 KB of JavaScript just to sort through a few numbers. That certainly doesn't seem right. In our source src/index.js, the Lodash utility library is imported and we use sortBy – one of its sorting utilities – in order to sort our numbers. Lodash offers several useful functions but the app only uses a single method from it. It's a common mistake to install and import all of a third-party dependency when in actual fact you only need to use a small part of it.

Optimise your bundle

There are a few options available for trimming our JavaScript bundle size:

  1. Write a custom sort method instead of relying on a thirdparty library.
  2. Use Array.prototype.sort(), which is built into the browser.
  3. Only import the sortBy method from Lodash instead of the whole library.
  4. Only download the code for sorting when a user needs it (when they click a button).

Options 1 and 2 are appropriate for reducing our bundle size – these probably make sense for a real application. For teaching purposes, we're going to try something different. Options 3 and 4 help improve the performance of the application.

Only import the code you need

We'll modify a few files to only import the single sortBy method we need from Lodash. Let's start with replacing our lodash dependency in package.json:

"lodash": "^4.7.0",

with this:

"lodash.sortby": "^4.7.0",

In src/index.js, we'll import this more specific module:

js
import "./style.css";
import _ from "lodash";
import sortBy from "lodash.sortby";

Next, we'll update how the values get sorted:

js
form.addEventListener("submit", e => {
  e.preventDefault();
  const values = [input1.valueAsNumber, input2.valueAsNumber, input3.valueAsNumber];
  const sortedValues = _.sortBy(values);
  const sortedValues = sortBy(values);
  results.innerHTML = `
    <h2>
      ${sortedValues}
    </h2>
  `
});

Reload the magic numbers app, open up Developer Tools and look at the Network panel again. For this specific app, our bundle size was reduced by a scale of four with little work. But there's still much room for improvement.

JavaScript code splitting

Webpack is one of the most popular JavaScript module bundlers used by web developers today. It 'bundles' (combines) all your JavaScript modules and other assets into static files web browsers can read.

The single bundle in this application can be split into two separate scripts:

  • One is responsible for code making up the initial route.
  • Another one contains our sorting code.

Using dynamic imports (with the import() keyword), a second script can be lazy-loaded on demand. In our magic numbers app, the code making up the script can be loaded as needed when the user clicks the button. We begin by removing the top-level import for the sort method in src/index.js:

import sortBy from "lodash.sortby";

Import it within the event listener that fires when the button is clicked:

form.addEventListener("submit", e => {
  e.preventDefault();
  import('lodash.sortby')
    .then(module => module.default)
    .then(sortInput())
    .catch(err => { alert(err) });
});

This dynamic import() feature we're using is part of a standardstrack proposal for including the ability to dynamically import a module in the JavaScript language standard. Webpack already supports this syntax. You can read more about how dynamic imports work in this article.

The import() statement returns a Promise when it resolves. Webpack considers this as a split point that it will break out into a separate script (or chunk). Once the module is returned, the module.default is used to reference the default export provided by lodash. The Promise is chained with another .then() calling a sortInput method to sort the three input values. At the end of the Promise chain, .catch() is called upon to handle where the Promise is rejected as the result of an error.

In a real production applications, you should handle dynamic import errors appropriately. Simple alert messages (similar to what is used here) are what are used and may not provide the best user experience for letting users know something has gone wrong.

In case you see a linting error like "Parsing error: import and export may only appear at the top level", know that this is due to the dynamic import syntax not yet being finalised. Although Webpack support it, the settings for ESLint (a JavaScript linting tool) used by Glitch have not been updated to include this syntax yet but it does still work.

The last thing we need to do is write the sortInput method at the end of our file. This has to be a function returning a function that takes in the imported method from lodash.sortBy. The nested function can sort the three input values and update the DOM:

const sortInput = () => {
  return (sortBy) => {
    const values = [
      input1.valueAsNumber,
      input2.valueAsNumber,
      input3.valueAsNumber
    ];
    const sortedValues = sortBy(values);
    results.innerHTML = `
      <h2>
        ${sortedValues}
      </h2>
    `
  };
}

Monitor the numbers

Now let's reload the application one last time and keep a close eye on the Network panel. You should notice how only a small initial bundle is downloaded when the app loads. After the button is clicked to sort the input numbers, the script/ chunk containing the sorting code gets fetched and executed. Do you see how the numbers still get sorted as we would expect them to?

JavaScript code splitting and lazy-loading can be very useful for trimming down the initial bundle size of your app or site. This can directly result in faster page load times for users. Although we've looked at adding code splitting to a vanilla JavaScript application, you can also apply it to apps built with libraries or frameworks.

Lazy-loading with a JavaScript library or framework

A lot of popular frameworks support adding code splitting and lazy-loading using dynamic imports and Webpack.

Here's how you might lazy-load a movie 'description' component using React (with React.lazy() and their Suspense feature) to provide a "Loading…" fallback while the component is being lazy-loaded in (see here for some more details):

import React, { Suspense } from 'react';
const Description = React.lazy(() => import('./Description'));
function App() {
  return (
    <div>
      <h1>My Movie</h1>
      <Suspense fallback="Loading...">
        <Description />
      </Suspense>
    </div>
  );
}

Code splitting can help reduce the impact of JavaScript on your user experience. Definitely consider it if you have larger JavaScript bundles and when in doubt, don't forget to measure, optimise and monitor. 

This article was originally published in issue 317 of net, the world's best-selling magazine for web designers and developers. Buy issue 317 here or subscribe here.

Related articles: