Dynamic Import, Code Splitting, Lazy Loading, and Error Boundaries

A detailed guide

Jennifer Fu
Jun 11 · 8 min read
Image for post
Image for post
Photo by i on

This article is a detailed guide on how to use dynamic import, which enables code splitting and lazy loading. It also describes how to use error boundaries to catch errors.

Is a Function?

Yes, but no.

is currently in stage 4 of the TC39 process. It is a function-like module loading syntactic form in JavaScript.

It acts in many ways like a function:

  • It is invoked by the () operator.
  • It returns a promise for the module namespace object of the requested module, which is created after fetching, instantiating, and evaluating all of the module’s dependencies, as well as the module itself.

But it is not a function:

  • It is a syntactic form that just happens to use parentheses, similar to ).
  • It does not inherit from Function.prototype. Therefore, it cannot be invoked with apply or call.
  • It does not inherit from Object.prototype. Therefore, it cannot be used as a variable. const a = import is illegal.

Another interesting point:import.meta, a stage 4 TC39 proposal, exposes context-specific metadata to a JavaScript module. It contains information about the module, such as the module’s URL. import.meta is an object with a null prototype. However, it is extensible, and its properties are writable, configurable, and enumerable.

Dynamic import, code splitting, lazy loading, and error boundaries are interesting technologies. Do you want to know more?


What Is Dynamic Import?

In contrast to , dynamic import is a design pattern to defer an object’s initialization until the point at which it is needed. Dynamic import enables code splitting and lazy loading. This can dramatically improve the application’s performance.

It is done by import():

import("/path/to/import-module.js") // .js can be skipped
.then((module) => {
// do something with the module
});

Five dynamic import use cases are described on :

  • When static import significantly slows the loading, and it is less likely that you will need the code, or you will need it in a later time
  • When static import significantly increases the memory usage, and it is less likely that you will need the code
  • When the module does not exist at loading time
  • When the import specifier string needs to be constructed dynamically
  • When the module being imported has side effects, and those side effects can be avoided unless certain conditions happen

Example of Static Import

Here is the user interface of a webpage. Everything is statically imported, regardless of whether a user is going to select anything.

Image for post
Image for post

The select menu has two choices:

  • The Micro Frontends topic
  • The React Utilities topic

After the Micro Frontends topic is selected, it shows two article links. When you click on either of the links, the related introduction is displayed:

Image for post
Image for post

After the React Utilities topic is selected, it shows another two article links. When you click either of the links, the related introduction is displayed:

Image for post
Image for post

Let’s create this example. is a convenient way to initiate a React coding environment:

npx create-react-app my-app
cd my-app
npm start

We add two in package.json:

"dependencies": {
"react-router-dom": "^5.2.0",
"react-select": "^3.1.0"
}
  • react-router-dom is for building routes.
  • is an elegant way to implement the dropdown list.

Change src/App.css to this for minimal styling:

In the following src/index.js, add BrowserRouter at line 10 and line 12:

Create the component generator: src/buildComponent.js.

Create the route information for the Micro Frontends topic in src/microFrontendRoutes.js.

Create the route information for the React Utilities topic in src/reactUtilitiesRoutes.js.

Now let’s see the main changes in src/App.js:

At line 4 and line 5, mfaRoutes and utilRoutes are statically imported. They are mapped at lines 26 - 32.

Line 9 defines the topic state, which is set by the Select component. The select options are defined by lines 12 - 24. The Select component is defined by lines 49 - 54. When a topic is selected, onChange at line 53 will invoke handleTopicChange(lines 36 - 43).

Line 10 defines the routes state. When handleTopicChange is invoked, it sets the selected topic at line 38 and sets the selected routes at line 39. The routes change will cause re-render of links at lines 55 - 61 and route changes at lines 63 - 67.

Everything in this example is static import.


Convert to Dynamic Import

Here is the same user interface of a webpage. Things below the Select component will be dynamically imported.

Image for post
Image for post

After the Micro Frontends topic is selected, it loads the links and related introduction.

Image for post
Image for post

After the React Utilities topic is selected, it loads the links and related introduction.

Image for post
Image for post

This example uses the same code base as the static import example, with some changes in src/App.js.

The static imports for mfaRoutes and utilRoute are removed. routeMapping at lines 24 - 30 points to filenames, instead of static imports. The file extensions can be skipped.

The key difference is in handleTopicChange(lines 34 - 43). It dynamically imports the relevant module to set currentRoutes.

That’s all the changes. It seems simple and straightforward.


Code Splitting

If you read the above src/App.js carefully, you might be wondering why we bother to construct the file path at line 37, instead of building the full path in routeMapping.

Try it.

You will encounter the error: “.” This error comes from , which is used by Create React App. Webpack performs a static analysis at build time. It handles static paths well, but it has trouble inferring from a variable to decide which files need to be in separate chunks.

If we hardcode something like import(“./microFrontendRoutes.js”), it will make this file a separate chunk.

If we code something like import(“./” + routeMapping[selected.value]), it will make every file in ./ directory a separate chunk.

If we code something like import(someVariable), Webpack throws an error.

What is code splitting?

It is a feature to split the code into various bundles (chunks) which can then be loaded on demand or in parallel. It can be used to achieve smaller bundles and control resource load prioritization. If it is used correctly, it can reduce loading time.

Webpack provides three general approaches to perform code splitting:

  • Entry points: Manually split code using entry configuration
  • Prevent duplication: Use the SplitChunksPlugin to dedupe and split chunks
  • Dynamic import: Split code via inline import()

For our static example, npm run build shows the following generated bundles:

File sizes after gzip:71.85 KB (+32.46 KB)  build/static/js/2.489c17a1.chunk.js
2.24 KB (+1.6 KB) build/static/js/main.7c91b243.chunk.js
776 B build/static/js/runtime-main.8894ea17.js
312 B (-235 B) build/static/css/main.83b9e03d.chunk.css

In the above bundles:

  • main.[hash].chunk.js: It is application code, including App.js, etc.
  • [number].[hash].chunk.js: It is either vendor code or split chunk.
  • runtime-main.[hash].js: It is a small chunk of Webpack runtime logic which is used to load and run the application.
  • main.[hash].chunk.css: It is CSS code.

With hardcoded import(“./microFrontendRoutes.js”), we can see one extra chunk is generated:

File sizes after gzip:71.85 KB (+70.95 KB)  build/static/js/2.489c17a1.chunk.js
1.17 KB (-74 B) build/static/js/runtime-main.c08b891b.js
902 B (-912 B) build/static/js/main.a5e0768c.chunk.js
885 B (-290 B) build/static/js/3.b3637929.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css

Coded as import(“./” + routeMapping[selected.value]), we can see every file in the ./ directory becomes a separate chunk.

File sizes after gzip:71.86 KB           build/static/js/9.e1addb36.chunk.js
50.68 KB build/static/js/1.ee2fd9a6.chunk.js
49.51 KB build/static/js/0.ee6ff7e2.chunk.js
1.77 KB (-477 B) build/static/js/main.21dcc146.chunk.js
1.24 KB (+496 B) build/static/js/runtime-main.834cfecb.js
1.15 KB build/static/js/3.5ca48094.chunk.js
919 B (-70.95 KB) build/static/js/2.4da83169.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css
283 B build/static/js/5.0753ed26.chunk.js
283 B build/static/js/4.7720f7a0.chunk.js
177 B build/static/js/10.880ba423.chunk.js
160 B build/static/js/6.f3a8c74d.chunk.js

Lazy Loading

Lazy loading is a follow up of code splitting. Since the code has been split at logical breakpoints, we load things when they are needed. In this way, we can dramatically improve performance. Although the overall amount of loading time may be the same, the initial loading time is improved. By doing this, we avoid loading things that a user may not access at all.

Dynamic import lazily loads any JavaScript module. React.lazy makes it easier, with the limitation rendering a dynamic import as a regular component.

The following is a similar user interface. Things below the Other Articles link are lazily loaded.

Image for post
Image for post

After the Other Articles link is clicked, it loads the remaining content:

Image for post
Image for post

Create the component to be lazily loaded in src/OtherRouteComp.js.

Currently, this React component must be exported as default. The named export does not support lazy loading.

The above component can be lazily loaded in src/App.js:

Line 6 lazily loads OtherRouteComp, which returns a promise that resolves to a component exported by src/OtherRouteComp.js. This component is wrapped in <Suspense>, which has a fallback user interface at line 10 to show the transition during loading.

Similarly, an extra chunk is generated because of lazy loading.

File sizes after gzip:47.91 KB (+480 B)  build/static/js/2.27608de7.chunk.js
1.17 KB (-1 B) build/static/js/runtime-main.fce20771.js
911 B (+232 B) build/static/js/main.6680ea99.chunk.js
372 B (-10 B) build/static/js/3.df091b29.chunk.js
312 B build/static/css/main.83b9e03d.chunk.css

On the Network tab, it shows a new chunk is loaded after the Other Articles link is clicked. This lazy loading behavior is true to all dynamic import cases.

Image for post
Image for post

Error Boundaries

Lazy loading returns a promise. What if this transition fails?

A well-designed user experience handles this situation gracefully. Error boundary is used for this purpose. It is a classic component that catches JavaScript errors anywhere in its child component tree. It logs those errors and displays a fallback user interface instead of the crashed component tree.

Here is src/App.js with MyErrorBoundary set at line 11 and line 25.

src/MyErrorBoundary.js is a typical error boundary component:

An error boundary has one or two lifecycle methods:

We can simulate an error by initializing hasError to be true at line 6. Then we will encounter the following fallback user interface defined by line 20:

Image for post
Image for post

The granularity of error boundaries is up to the developer. There can be multiple levels of error boundaries.


Conclusion

We have discussed the benefits and provided examples of dynamic import, code splitting, lazy loading, and error boundaries.

In the end, we’d like to emphasize not to overuse any technologies. The static import is preferable for loading initial dependencies, and can benefit more readily from static analysis tools and tree shaking. Use dynamic import only when it is necessary.

Thanks for reading. I hope this was helpful. You can see my other Medium publications .

Note: Jonathan Ma contributed to part of this article.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store