Automatically divide your JavaScript into files that are dynamically included at runtime on pages where they’re needed.

Recently I created an interactive demo of RSA built with Vue.js, but I didn’t want to include Vue on every page. I could use Mix to build a separate JS file for each demo, but this doesn’t scale well if I want to make a few pages that use large libraries.

Dynamic Imports

At its core it uses Dynamic Imports. This feature lets us load a module conditionally, at runtime and returning a promise, so we can use it like this:

import('my-module.js')
    .then((module) => {
        module.init();
    });

Mix-ing things up

Dynamic Imports have okay browser support, but it’s better to be safe. We can use a Babel plugin to compile this down to widely supported JS:

$ npm install babel-plugin-syntax-dynamic-import

Now we need to tell Babel to use this plugin, so create a .babelrc file with:

{
  "plugins": ["babel-plugin-syntax-dynamic-import"]
}

Mix will detect this and merge it into the default config it ships with (which you can see here).

A nice Vue

Usually with Vue you’d initialise a root Vue instance on an element and then just use components inside it. We can’t do that because we don’t want to always initialise Vue. Instead, we’ll make a script that initialises our root instance and then mounts our component as its template.

export default function () {
    new Vue({
        el: '#my-component',
        components: { MyComponent },
        template: '<MyComponent />',
        mounted() {
            console.log("Mounted MyComponent root")
        }
    })
}

Then, from our main app file we can dynamically import it:

if (document.getElementById("my-component")) {
    import("./my-component/init" /* webpackChunkName: "js/my-component" */)
        .then(initMyComponent => {
            initMyComponent.default();
        })
}

We check if the element we mount into is on the current page, and if it is then we dynamically import our initialisation script. It immediately returns a promise, so we have to wait for this to complete before we can call the function our initialisation script exports.

We’re setting the webpackChunkName manually because otherwise Mix will put these split out files directly into our public path. This should be the path from your public path to where you want the script to go (without the js extension).

vend… or… not?

This will actually give us two files: js/my-component.js and vendor~js/my-component.js — the vendor file will have any npm packages we import

Splitting out this vendor file is great if we have a multi entry point app, but we don’t so there’s not much point. We can disable it with webpack config. To our webpack.mix.js file add:

mix.webpackConfig({
    optimization: {
        splitChunks: {
            cacheGroups: {
                vendors: false,
            },
        },
    }
})

The Downsides

This approach stops our extra code increasing page load times on other site pages, but it does increase load time for pages that use runtime imports like this. I think this tradeoff is acceptable for my site, but your mileage may vary.

You can improve load speed by preloading the script file on pages where it’s needed by putting this in the page’s head:

<link rel="preload" href="/js/my-component.js" as="script">