Skip to content
Product Documentation

Code Splitting and Dynamic Import

The mono-bundle implementation introduced with 2.3+ allowed all of feature pack’s codes to be bundled into a single bundle, thereby allowing for a quicker load time after initial render since all the codes would be loaded at the first visit of the page. However, if you would like to optimize this even further, you can do so by leveraging Webpack’s ability to split codes with dynamic import.

The use case for this would be in the cases where you want to separate a chunk of code, such as a big library like Lodash or Momentjs - which could weigh upwards of 1.5mb, or even your own feature component, and have them be loaded at later time. This would mean that the weight of those chunks of codes would not be loaded at the initial render, thereby saving time at first load. If done with features that are not often used, such as login or other less-used features on your page, it could save considerable amount of time for everyday usage.

Here is a very simple demonstration by size with lodash:

No dynamic import with lodash (lodash not used)

Terminal window
fusion-webpack | Asset Size Chunks Chunk Names
fusion-webpack | amp.css 635 bytes amp [emitted] amp
fusion-webpack | amp.js 1.64 MiB amp [emitted] amp
fusion-webpack | default.css 635 bytes default [emitted] default
fusion-webpack | default.js 1.63 MiB default [emitted] default
fusion-webpack | hydrate.css 635 bytes hydrate [emitted] hydrate
fusion-webpack | hydrate.js 1.63 MiB hydrate [emitted] hydrate

Using dynamic import with lodash

Terminal window
fusion-webpack | Asset Size Chunks Chunk Names
fusion-webpack | default.css 1.1 KiB default [emitted] default
fusion-webpack | default.js 247 KiB default [emitted] default
fusion-webpack | hydrate.css 1.1 KiB hydrate [emitted] hydrate
fusion-webpack | hydrate.js 249 KiB hydrate [emitted] hydrate
fusion-webpack | vendors~lodash.js 1.38 MiB vendors~lodash [emitted] vendors~lodash

As you can see, if you don’t use dynamic import, it will be included in every single output type, increasing the overall bundle size three times the size of the library. Using dynamic import syntax, you’re telling Webpack that it can split the library into another chunk that can be imported when needed (usually on a button click, etc.). Since the browser now doesn’t have to download the whole 1.63MiB bundle for the output at the initial render, it may save you some render time for that first render.

Implementing Dynamic Import

Below is a simple component that leverages momentjs to show what time it is when you click on a button, without using dynamic importing:

import moment from 'moment';
@Consumer
class Time extends Component {
constructor(props) {
super(props);
this.state = {
time: '',
showTime: false,
};
this.showTime = this.showTime.bind(this);
}
showTime() {
this.setState({
time: moment().format('MMMM Do YYYY, h:mm:ss a'),
showTime: true,
});
}
render() {
const {
time, showTime
} = this.state;
return (
<div>
Please click this button to display time:
<button type="button" onClick={this.showTime} />
{ showTime && time }
</div>
);
}
}

When you click on the button to invoke the showTime function, this would use the imported moment function to set the state with the current timestamp. However, because moment is imported upon creation of the component, even if it’s not used until the user clicks on the button (and they may not even click on that button during the time they browse the page!), moment will be imported anyways, adding sizable amount of bytes on your bundle.

Let’s rewrite this so that we leverage dynamic importing:

import React, { Component } from 'react';
import Consumer from 'fusion:consumer';
@Consumer
class Time extends Component {
constructor(props) {
super(props);
this.state = {
time: '',
showTime: false,
};
this.showTime = this.showTime.bind(this);
}
async showTime() {
// the import() function will return a promise, so the function will have to use async/await
const moment = await import(
/* webpackChunkName: "moment-chunk" */
'moment'
).then((module) => {
return module.default;
}).catch(error => 'An error occurred while loading the module');
this.setState({
time: moment().format('MMMM Do YYYY, h:mm:ss a'),
showTime: true,
})
}
render() {
const {
time, showTime
} = this.state;
return (
<div>
Please click this button to display time:
<button type="button" onClick={this.showTime} />
{ showTime && time }
</div>
);
}
}

You might have noticed that we removed the import statement from the top of the file, and moved it into the showTime function. Webpack will see that import() function is used to import the moment module, and will automatically split the module into a separate chunk so that until the showTime function is called, moment would not be imported. The “magic comment” is used to further customize how Webpack handles this chunk - in this case, we’re manually passing a webpackChunkName variable as moment-chunk so that it will be easy to identify later. If you do not pass this chunk name, Webpack will name them automatically by indices, like 1.chunk.js.

Now that we have code splitting via dynamic import working with a library, you might be wondering how this might work with your own components. Do not fret - we will cover that right now.

Use with components

For using dynamic importing with another React component, we will use React’s internal tool called React.Lazy. This is meant to be used just as how it sounds - for lazy loading. In conjunction with Webpack’s capability, you’ll be able to do what we did above in pretty clean manner. Let’s say you have another component called BreakingNews (it can be any component with any content, and will work similarly); your main component then will look like

import React, { Component, lazy, Suspense } from 'react';
import Consumer from 'fusion:consumer';
@Consumer
class Article extends Component {
constructor(props) {
super(props);
this.state = {
BreakingNews: '',
showBreakingNews: false,
};
this.showBreakingNews = this.showBreakingNews.bind(this);
}
importBreakingNews = () => {
const BreakingNews = lazy(() => import(
/* webpackChunkName: "breaking-news" */
'../breaking-news' // This will be the relative directory to the component you'd like to import.
));
this.setState({
BreakingNews,
showBreakingNews: true,
});
}
render() {
const {
BreakingNews,
showBreakingNews,
} = this.state;
const BreakingNewsSection = showBreakingNews ? (
<Suspense fallback="Loading...">
<BreakingNews />
</Suspense>
) : null;
return (
<div>
Please click this button to display Breaking News:
<button type="button" onClick={this.showTime} />
{ BreakingNewsSection }
</div>
);
}
}

Dynamically importing a component is slightly more involved. First, we will import lazy and Suspense from React. We will use lazy() as the wrapper for our import() function, which will be called from a button click as before. This part is pretty similar to the module import - however, when it comes time to render our component, we will have to wrap our lazy component in the React.Suspense component, which is important to serve as a fallback - if the dynamic import fails, then React will use whatever is specified in fallback prop of the Suspense component and render that instead.

Note that while we used button click as an example since it’s the simplest one to use, this should be applicable to any other kinds of triggers, like event listeners. All we need is to have it call the function that will call the lazy() and import() functions when deemed necessary.

Further Reading: