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)
fusion-webpack | Asset Size Chunks Chunk Namesfusion-webpack | amp.css 635 bytes amp [emitted] ampfusion-webpack | amp.js 1.64 MiB amp [emitted] ampfusion-webpack | default.css 635 bytes default [emitted] defaultfusion-webpack | default.js 1.63 MiB default [emitted] defaultfusion-webpack | hydrate.css 635 bytes hydrate [emitted] hydratefusion-webpack | hydrate.js 1.63 MiB hydrate [emitted] hydrate
Using dynamic import with lodash
fusion-webpack | Asset Size Chunks Chunk Namesfusion-webpack | default.css 1.1 KiB default [emitted] defaultfusion-webpack | default.js 247 KiB default [emitted] defaultfusion-webpack | hydrate.css 1.1 KiB hydrate [emitted] hydratefusion-webpack | hydrate.js 249 KiB hydrate [emitted] hydratefusion-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';
@Consumerclass 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';
@Consumerclass 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';
@Consumerclass 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: