Over-eager loading of Webpack’s async chunks for performant, seamless bundle splitting

Bertrand Junqua
3 min readMar 12, 2018

--

Webpack is a fantastic build tool. One of the reasons for that is its code splitting abilities and the magic that comes with it: import() some code and voilà, it gets separated from our main bundle and loaded only on demand.

This is very nice and allows us, after some hair-pulling time spent configuring and fine tuning the entry points and the common chunks, to greatly improve the Time To Interactive (TTI) of our app.

The problem is that those separate chunks are by default lazy-loaded, meaning that the client will only fetch them when absolutely necessary. And that has a big drawback, because now we have to handle loading times we did not have before. We reduced the TTI, but significantly increased the time necessary to jump from one section to another.

Enter over-eager loading, which is basically the idea of loading resources before the user actually asks for them/needs them by anticipating his needs. Loading our async chunks in an over-eager fashion will mean to quietly load them in the background after our app is interactive so that they’re ready for when the user actually needs them.

Let’s look at how to implement that.

Let’s create a class named AsyncChunks that will be in charge of generating our webpack split points.

import Loadable from 'react-loadable';
import LoadingPage from './LoadingPage';
class AsyncChunks {
generateChunk = (loader) => {
return Loadable({ loader, loading: LoadingPage });
}
}
export default new AsyncChunks();

We threw in the handy react-loadable util that handles displaying a loading placeholder in case we try to access our chunk before it has actually loaded, plus a couple of other edge cases.

Now, in our app’s code, replace our simple imports with something like this:

AsyncChunks.generateChunk(() => import('./MyComponent'));

So far so good, we got ourselves a basic implementation of bundles lazy-loading. Let’s now sprinkle some magic on top of this to make it more interesting.

First, we’ll register all of these chunks to a queue:

class AsyncChunks {
chunksQueue = [];

appendToQueue = chunk => this.chunksQueue.push(chunk)

generateChunk = (loader) => {
this.appendToQueue(loader);
return Loadable({ loader, loading: LoadingPage });
}
}

Now, as soon as our app loads, all the async chunks are going to register to the chunksQueue array, waiting for our command. Let’s add a loadChunks method to our class to allow us to trigger the eager loading of those chunks when we see fit:

class AsyncChunks {
chunksQueue = [];

appendToQueue = chunk => this.chunksQueue.push(chunk)

generateChunk = (loader) => {
this.appendToQueue(loader);
return Loadable({ loader, loading: LoadingPage });
}
loadChunks = () => {
this.chunksQueue.forEach(loader => loader());
this.chunksQueue = [];
}
}

Now, we only have to call this loadChunks method when appropriate. And what better time that right after our app is done mounting?

In our App.jsx, we add:

componentDidMount = () => {
setTimeout(AsyncChunks.loadChunks, 2 * 1000);
}

Here, we wait for two seconds (to avoid cluttering the bandwidth while most of the HTTP calls made by the app upon boot are still running), and then start loading all our chunks.

And that’s it!

One final touch though: to have a smarter over-eager loading, we should improve our AsyncChunks class to allow us to specify chunks that do not need to be preloaded:

class AsyncChunks {
chunksQueue = [];

appendToQueue = chunk => this.chunksQueue.push(chunk)

generateChunk = (loader, register = true) => {
if (register) this.appendToQueue(loader);
return Loadable({ loader, loading: LoadingPage });
}
loadChunks = () => {
this.chunksQueue.forEach(loader => loader());
this.chunksQueue = [];
}
}

Now we can still register our likely-to-be-needed chunks just like before, and the no-so-necessary ones like so:

AsyncChunks.generateChunk(() => import(‘./MyComponent’), false);

Those registered like this will load on-demand, in a default Webpack way.

To recap, we achieved to remove the delay created by Webpack’s code splitting when the client needs a new chunk while keeping all of the advantages of code splitting: minimal initial bundle and thus reduced Time To Interactive 🎉

Our last optimisation even allowed us to avoid wasting bandwidth by only pre-loading important bundles 😎

--

--