A Gentle Introduction to Code Splitting with React

Gustavo A. López
kommit
Published in
7 min readSep 1, 2020

A lot of folks (myself included) are used to write SPA React applications that need both a user-facing part and an admin interface for managing the data. In general, there are a variety of options that are commonly used to implement this type of requirements, which typically fall into two categories:
1). Two separate UIs (one public app and the other one only accessible by admins)
2). Restrict the application pages/sections based on the users’ permissions.

I recently came across a situation while implementing a health care web application, where we had a bunch of UI components that were intended to be utilized from both medical personnel (admins) views as well as from the patients’ UI, so the second approach of the aforementioned ones made the most sense in that case.

One important issue we happen to have, however, was that even though we’ve got a lot of generic React components to be used across the entire app, a massive amount of them was not being rendered in all the pages our users were supposed to be interacting with, meaning that they’d end up downloading a bundle.js file containing lots of unneeded fragments of code. If a patient is simply filling out a form to schedule a doctor’s appointment, they won’t need the huge Google Maps component to locate the clinics they went to before, which might be only required by navigating to the /previousAppointments route for example.
So the idea is quite simple, just don’t have the users download code until they need it.

Now, you might be thinking that something like this would do the trick:

If you’re familiar with JavaScript and ES modules it wouldn’t take you so long to figure out that the above naive approach is not going to work, since by definition the modules are static, and therefore we must declare what we’re importing/exporting at compile-time, not at run time. In other words, they may only be specified at the top level of the files and not in other places such as an if statement.

That being said, let’s pretend for a moment that the above conditionally imported module worked. Can you think of any benefits of doing so? Certainly, instead of having to load all the application modules/components up-front, we’d be allowed to import them on demand depending on what page our visitors are at any given time, so that’d be a reasonable approach towards only downloading code the user requires.

Chances are you don’t care about IE11 anymore. But in our case, IE11 support was a must. So we concluded that if we ever find out a way to get the modules loaded conditionally, we might apply the same idea to execute legacy code only when the IE11 users landed on our web.

Dynamic EcmaScript modules to the rescue

It turns out that there is a way to overcome the above-described issue: using the dynamic import() syntax. The difference with the normal import is that instead of using it as we’d normally do, we use it like a function that returns a promise — although import() looks like a function call, it is not actually, it’s a special syntax that just happens to use parentheses similar to super().
Using this expression, we can load the module and return a promise that resolves into a module object that contains all of its exports. Getting back to our example, it would be something like:

We can even use await if the import statement were inside an async function:

One thing to be aware of, though, is that this syntax is not officially added to the EcmaScript specification yet, and at the time of this writing it’s in stage 4 of the process. If you’re using Create React App it’s already supported by default, otherwise, you just need to install a babel plugin named plugin-syntax-dynamic-import

npm install — save-dev @babel/plugin-syntax-dynamic-import

and then use it in the .babelrc configuration file:

{ "plugins": ["@babel/plugin-syntax-dynamic-import"] }

OK so now that we all know how to import modules dynamically, you might be wondering how to use this knowledge with React, and consequently, where should we split our application at? Well, there are a couple of approaches to address this question:

  1. Route level splitting
  2. Component level splitting

Route level splitting

This is the most popular way when it comes to code splitting our app. Assuming you have some experience with any kind of routing tools, this should feel pretty natural after all, isn’t it? So this is where the React.lazy function comes into play. It accepts a function which must call the dynamic import, and returns a promise which resolves to a module with a default export containing a React component. This means that we don’t need to render the lazily loaded component by ourselves, instead, we render what React.lazy returns and it’ll take care of actually rendering the component.
Let’s take a look at how this would look like — I’m using reach-router instead of the typical React Router, but it shouldn’t make any difference for this example:

And that’s all that it takes to code split our app at the route level. In the snippet, we’re just dynamically importing the PreviousAppointments route component, but we can also use the same approach on the other two routes if we wanted to. Below is the output when we build our app — yarn run build or npm run build depending on what the package manager of your choice is.

Asset                 Size       Chunksapp.91ac87d5.js       114  KiB   0 [emitted] 
1.19z8duj4.chunk.js 7.97 KiB 1 [emitted]
vendors.j8z7cn1q.js 869 KiB 2 [emitted]

You’ll get one chunk file per each dynamic import() in the app.

What if we wanted to show some content to the users while the components are being loaded dynamically? That’s what the React.Suspense component is used for. Anytime we want to import dynamic components we can wrap them all into a Suspense instance and pass it a fallback prop which is a React element that is rendered until all of its children’s components get successfully loaded. Now let’s give it a shot at Suspense by turning all of our routes into dynamic components and passing it a <Loading /> element as a fallback:

Component level splitting

Code splitting your application at the routes level might represent a significant improvement in terms of the size of files that webpack builds out, so you may consider it as good enough in a few cases. However, there is still a more granular way of dynamically importing code in React — at the Component level.
Inside a single component, you might need to render a child component based on a certain state. Assuming our Map component is tremendously heavy, we can get it loaded if and only if it’s necessary — so we’d first show a button to the users and if they want to get the Map functionality in place on the current page then they can click on it to load the component based on that action.
At a glance, it might sound a little bit tricky to achieve, but in practice, it’s pretty straightforward:

As you might have seen, there’s one small caveat in the above code.
If the module we’re dynamically importing is using ES modules (export default), it’ll have a .default property; this is only the case for ES modules, meaning that if the module is using commonjs (module.exports), it won’t.

Besides the dynamic import() proposal, another common approach to performing code splitting in client-side JavaScript is using the require.ensure method from webpack, but as stated in their docs the import() syntax should be preferred.

Final thoughts

A big gain we got in our application was the fact that we managed to optimize the bundles’ sizes since each of them ended up including only the sufficient modules/components depending on the type of users coming through the portal.

React lazy and Suspense is definitely not a good fit if you’re using server-side rendering (SSR) in your project as this is not supported yet — I believe that might be one of the primary reasons for which those are still experimental features and are not included as part of the stable version of React. For SSR needs, React recommends a library named Loadable Components. It has a nice guide for bundle splitting with server-side rendering.

Another advantage of using code splitting was the ability to exclude administrative code for regular users to hide potentially sensitive data, which is a must while developing health care applications.

Last but not least, be careful and use dynamic imports only when it makes sense. The static form is preferable for loading initial dependencies and can benefit more readily from static analysis tools and tree shaking.

If you’ve worked with React before, I hope this helped get you started with when and how to use the code splitting optimization technique in your applications for the sake of speeding up the performance and therefore better user experience.

Thanks for reading and happy coding!

References

Image designed by Freepik

--

--