My Journey
As my single page application became more and more feature rich, it became larger and larger in size. In fact, every time I built, Webpack would warn me of this.
WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB).
This can impact web performance.
Assets:
static/js/main.ff956fb8.chunk.js (1.78 MiB)
Users will need wait for this 1.78MB file to be fully loaded in the browser before they can interact with it. If the network connection is slow, this can feel like forever. Something had to be done to make this initial experience faster.
Code Splitting
Code splitting splits a single page application’s code into smaller chunks. Instead of requesting it all, only chunks that are needed in the moment are requested. The more the codebase is split, the smaller each chunk, the faster the load time. Code splitting is achieved through the dynamic import syntax. Instead of the regular import syntax,
import func from './func'
func.doSomething()
dynamic import returns a promise that will resolve to the imported module’s content.
import('./func').then(func => func.doSomething());
Webpack (or other bundlers) will recognize the dynamic import syntax and put the the contents of func module into its own separate code chunk.
Lazy Loading
But this is not easily consumable. Handling promises isn’t a quick and simple change. Fortunately, React makes this a lot simpler with its lazy function. It works in conjunction with dynamic import to enable code splitting for React components.
import React, { lazy } from 'react';
const LazyLoad = lazy(() => import('./LazyLoad'));
const App = () => {
return <LazyLoad />;
};
Now, App and LazyLoad are in separate code chunks. A request is sent to fetch the LazyLoad code chunk only when App is rendered. When the request completes, React will then renderLazyLoad. You can verify this by looking at the Javascript requests in the network inspector.
Suspense
This also introduces a problem. The component is not immediately available to be rendered because it must be fetched via a network call. This may take some time to complete. How do we show something to users so they know something is loading and not just broken?
React solves this with the Suspense component. This component can detect whether any children (or grandchildren, etc) are lazy loaded. While they are still being requested, Suspense will render the specified fallback instead.
const App = () => {
return (
<Suspense fallback="Loading">
<LazyLoad />
</Suspense>
);
};
Now when App is rendered and a request is initiated to get the LazyLoad code, the fallback Loading is rendered. When this request completes, React will then render LazyLoad.
Chunk Prefetch
To minimize the load time, you can also prefetch some components if you can accurately predict what the user needs next. For example, if the user is on the login page, you can load the post-login home page. An inline comment with the webpackPrefetch key within the dynamic import function will instruct Webpack to do just this.
const PrefetchLoad = lazy(() =>
import(
/* webpackPrefetch: true */ './PrefetchLoad'
)
);
const App = () => {
return (
<Suspense fallback="Loading">
{condition && <PrefetchLoad />}
</Suspense>
);
};
Here, even if condition is not met, the app preemptively requests PrefetchLoad. When condition is met, PrefetchLoad will then be rendered immediately.
Chunk Name
By default, code split chunks are named by the index in which they are created. In the above example, LazyLoad is contained within the 0.chunk.js. If you have multiple chunks, it becomes difficult to know which chunk corresponds to which code. Similar to prefetching, you can specify the chunk name with an inline comment using the webpackChunkName key.
const LazyLoad = lazy(() =>
import(/* webpackChunkName: 'lazyload' */ './LazyLoad')
);
Now, the chunk will be called lazyload.chunk.js making it easy to identify.
Conclusion
In my app, file sizes and initial load times dropped dramatically after implementing lazy loading. The login page is in its own code chunk that is a mere 5KB. In addition, each separate route is split into its own chunk, and the common ones are all prefetched so they are immediately ready for use after the user logs in. All this code splitting was possible because of React lazy loading.