Code Splitting with the Preload Property

If you are bundling an application with a lot of routes, users of your application may be downloading a lot of unnecessary content just to render the initial page. Using code splitting, you can reduce the initial download size for your application by splitting code that is conditionally loaded into a separate bundle that is only downloaded when it is needed.

Note: This guide assumes that you are using Webpack 2+ to bundle your application.

An app without code splitting#

Let's start out by describing our application's routes without code splitting. We will import each route's component from the files where they are defined.

import Home from './components/Home';
import Contact from './components/Contact';
import ContactMethod from './components/ContactMethod';

const routes = [
  {
    name: 'Home',
    path: '',
    body: () => Home
  },
  {
    name: 'Contact',
    path: 'contact',
    body: () => Contact,
    children: [
      {
        name: 'Contact Method',
        path: ':method',
        body: () => ContactMethod
      }
    ]
  }
];

Removing static imports#

With code splitting, we don't want to have access to the component values when creating our routes because that means we have to download all of them before our application can render. We should remove our import calls so that that doesn't happen.

const routes = [
  {
    name: 'Home',
    path: '',
    body: () => Home
  },
  {
    name: 'Contact',
    path: 'contact',
    body: () => Contact,
    children: [
      {
        name: 'Contact Method',
        path: ':method',
        body: () => ContactMethod
      }
    ]
  }
];

Importing in preload#

Now, Home, Contact, and ContactMethod are all undefined, so if we tried to render our application we would get errors. We need to actually import our components so that our body functions actually have something to return.

We will import our components using the preload property of routes. This function will only be called the first time that its route matches, so we don't have to worry about making extra requests to our server.

preload should be a function that returns a Promise. Here, we will callimport(), which conveniently returns a Promise.

const routes = [
  {
    name: 'Home',
    path: '',
    preload: () => import('./components/Home'),
    body: () => Home
  },
  {
    name: 'Contact',
    path: 'contact',
    preload: () => import('./components/Contact'),
    body: () => Contact,
    children: [
      {
        name: 'Contact Method',
        path: ':method',
        preload: () => import('./components/ContactMethod'),
        body: () => ContactMethod
      }
    ]
  }
];

Saving our imports#

That will load our components when their route matches, but we still don't have access to the component functions that we need in order to render. We will need to use a then call to our import() Promises in order to access the component functions.

let Home;
let Contact;
let ContactMethod;

const routes = [
  {
    name: 'Home',
    path: '',
    preload: () => (
      import('./components/Home').then(module => {
        Home = module.default;
      })
    ),
    body: () => Home
  },
  {
    name: 'Contact',
    path: 'contact',
    preload: () => (
      import('./components/Contact').then(module => {
        Contact = module.default;
      })
    ),
    body: () => Contact,
    children: [
      {
        name: 'Contact Method',
        path: ':method',
        preload: () => (
          import('./components/ContactMethod').then(module => {
            ContactMethod = module.default;
          })
        ),
        body: () => ContactMethod
      }
    ]
  }
];

Storing our imports#

Our application will now only load components when they are needed and will correctly render. However, it is a bit ugly and error prone to define variables for all of our routes. Instead we can create a "store" where we can store references to each route's component. The simplest store is an object, so we will start with that.

const store = {}

const routes = [
  {
    name: 'Home',
    path: '',
    preload: () => (
      import('./components/Home').then(module => {
        store['Home'] = module.default;
      })
    ),
    body: () => store['Home']
  },
  {
    name: 'Contact',
    path: 'contact',
    preload: () => (
      import('./components/Contact').then(module => {
        store['Contact'] = module.default;
      })
    ),
    body: () => store['Contact'],
    children: [
      {
        name: 'Contact Method',
        path: ':method',
        preload: () => (
          import('./components/ContactMethod').then(module => {
            store['ContactMethod'] = module.default;
          })
        ),
        body: () => store['ContactMethod']
      }
    ]
  }
];

A better store#

That should be sufficient, although it is not an error proof approach. Our preload functions currently do nothing when there are errors in importing the components. What you do when that happens is up to you, but you would most likely want to have a default component that you display when the error occurs.

const defaultComponent = () => <div>Uh oh, something must have gone wrong</div>;
const store = {
  stored: {},
  set: function(name, value) {
    this.stored[name] = value;
  },
  get: function(name) {
    return this.stored[name] || defaultComponent;
  }
}

// usage
{
  ...,
  preload: () => (
    import('./components/Something')
      .then(module => {
        store.set('Something', module.default);
      })
      .catch(err => {
        console.error(err);
        store.set('Something', defaultComponent);
      })
  ),
  body: () => store.get('Something')
}

Next#

The approaches taken here are not the only way to do code splitting. You may choose to skip the preload method and do code splitting at other points in your application. You may also create a more full-fledged solution for storing loaded imports. Whatever path you decide to go, hopefully this has shown you that setting up code splitting with the preload property is fairly simple to do. If you are using Webpack and want to reduce your initial bundle size, preload is a great way to accomplish this.

Next, we will take a look at a related route property: load.