About The Author
Adrian Bece is a versatile fullstack web developer with extensive eCommerce experience who is currently working at PROTOTYP as a technical lead. He enjoys …
When a user lands on a homepage, for example, the entire
app.min.js bundle with code for other pages is loaded and parsed, which means that only a part of it is used and rendered on the page. This sounds inefficient, doesn’t it? In addition to that, all users are loading a restricted part of the app which only a few users will be able to have access to — the admin page. Even though the code is partially obfuscated as part of the minification process, we risk exposing API endpoints or other data reserved for admin users.
Meet with useful tips on front-end, design & UX. Subscribe and get “Smart Interface Design Checklists” — a free PDF deck with 150+ questions to ask yourself when designing and building almost anything.
Once a week. Useful tips on front-end & UX. Trusted by 190.000 friendly folks.
defer signal to the browser not to block page processing, however, the CPU thread still gets blocked and the script needs to be executed before the page becomes responsive to user input.
dashboard.min.js, and so on.
When the user initially lands on the homepage, the main vendor bundle containing the framework and other shared dependencies should be loaded in alongside the bundle for the homepage. The user clicks on a button that toggles an account creation modal. As the user is interacting with the inputs, the expensive password strength check library is dynamically loaded. When a user creates an account and logs in successfully, they are redirected to the dashboard, and only then is the dashboard bundle loaded. It’s also important to note that this particular user doesn’t have an admin role on the web app, so the admin bundle is not loaded.
Dynamic Imports & Code-splitting In React
Code splitting is available out-of-the-box for Create React App and other frameworks that use Webpack like Gatsby and Next.js. If you have set up the React project manually or if you are using a framework that doesn’t have code-splitting configured out-of-the-box, you’ll have to consult the Webpack documentation or the documentation for the build tool that you’re using.
In the following example, we have a blog post with a comment section. We’d like to encourage our readers to create an account and leave comments, so we are offering a quick way to create an account and start commenting by displaying the form next to the comment section if they’re not logged in.
The form is using a sizeable 800kB library to check password strength which could prove problematic for performance, so it’s the right candidate for code splitting. This is the exact scenario I was dealing with last year and we managed to achieve a noticeable performance boost by code-splitting this library to a separate bundle and loading it dynamically.
Let’s see what the
Comments.jsx component looks like.
We’re importing the
zxcvbn library directly and it gets included in the main bundle as a result. The resulting minified bundle for our tiny blog post component is a whopping 442kB gzipped! React library and this blog post page barely reach 45kB gzipped, so we have slowed down the initial loading of this page considerably by instantly loading this password checking library.
We can reach the same conclusion by looking at the Webpack Bundle Analyzer output for the app. That narrow rectangle on the far right is our blog post component.
Password checking is not critical for page render. Its functionality is required only when the user interacts with the password input. So, let’s code-split
zxcvbn into a separate bundle, dynamically import it and load it only when the password input value changes, i.e. when the user starts typing their password. We need to remove the
import statement and add the dynamic import statement to the password
onChange event handler function.
Let’s see how our app behaves now after we’ve moved the library to a dynamic import.
As we can see from the video, the initial page load is around 45kB which covers only framework dependencies and the blog post page components. This is the ideal case since users will be able to get the content much faster, especially the ones using slower network connections.
Once the user starts typing in the password input, we can see the bundle for the
zxcvbn library appears in the network tab and the result of the function running is displayed below the input. Even though this process repeats on every keypress, the file is only requested once and it runs instantly once it becomes available.
We can also confirm that the library has been code-split into a separate bundle by checking Webpack Bundle Analyzer output.
Third-party React components
Code-splitting React components are simple for most cases and it consists of the following four steps:
Let’s take a look at another example. This time we’re building a date-picking component that has requirements that default HTML date input cannot meet. We have chosen
react-calendar as the library that we’re going to use.
Let’s take a look at the
DatePicker component. We can see that the
Calendar component from the
react-calendar package is being displayed conditionally when the user focuses on the date input element.
This is pretty much a standard way almost anyone would have created this app. Let’s run the Webpack Bundle Analyzer and see what the bundles look like.
react-calendar takes a considerable portion of it. Let’s see if we can code split it.
The first thing we need to notice is that the
Calendar popup is loaded conditionally, only when the
showModal state is set. This makes the
Calendar component a prime candidate for code-splitting.
Next, we need to check if
Calendar is a default export. In our case, it is.
Let’s change the
DatePicker component to lazy load the
First, we need to remove the
import statement and replace it with
lazy import statement. Next, we need to wrap the lazy-loaded component in a
Suspense component and provide a
fallback which is rendered until the lazy-loaded component becomes available.
It’s important to note that
fallback is a required prop of the
Suspense component. We can provide any valid React node as a fallback:
Let’s run Webpack Bundle Analyzer and confirm that the
react-calendar has been successfully code-split from the main bundle.
We are not limited to third-party components or NPM packages. We can code-split virtually any component in our project. Let’s take the website routes, for example, and code-split individual page components into separate bundles. That way, we’ll always load only the main (shared) bundle and a component bundle needed for the page we’re currently on.
App.jsx consists of a React router and three components that are loaded depending on the current location (URL).
Each of those page components has a default export and is currently imported in a default non-lazy way for this example.
As we’ve already concluded, these components get included in the main bundle by default (depending on the framework and build tools) meaning that everything gets loaded regardless of the route which user lands on. Both Dashboard and About components are loaded on the homepage route and so on.
Let’s refactor our
import statements like in the previous example and use
lazy import to code-split page components. We also need to nest these components under a single
Suspense component. If we had to provide a different fallback element for these components, we’d nest each component under a separate
Suspense component. Components have a default export, so we don’t need to change them.
And that’s it! Page components are neatly split into separate packages and are loaded on-demand as the user navigates between the pages. Keep in mind, that you can provide a fallback component like a spinner or a skeleton loader to provide a better loading experience on slower networks and average to low-end devices.
What Should We Code-split?
It’s crucial to understand, which functions and components should be code-split into separate bundles from the get-go. That way, we can code-split proactively and early on in development and avoid the aforementioned bundling pitfalls and having to untangle everything.
You might already have some idea on how to choose the right components for code-splitting from the examples that we’ve covered. Here is a good baseline criterion to follow when choosing potential candidates for code-splitting:
We shouldn’t get overzealous with code-splitting. Although we identified potential candidates for code-splitting, we want to dynamically load bundles that significantly impact performance or load times. We want to avoid creating bundles with the size of a few hundred bytes or a few kilobytes. These micro-bundles can actually harm the UX and performance in some cases, as we’ll see later on in the article.
Some projects will require optimization later in the development cycle or even sometime after the project goes live. The main downside of code-splitting later in the development cycle is that you’ll have to deal with components and changes on a wider scale. If some widely-used component turns out a good candidate for code-splitting and it is used across 50 other components, the scope of the pull request and changes would be large and difficult to test if no automated test exists.
Being tasked with optimizing the performance of the entire web app may be a bit overwhelming at first. A good place to start is to audit the app using Webpack Bundle Analyzer or Source Map Explorer and identify bundles that should be code-split and fit the aforementioned criteria. An additional way of identifying those bundles is to run a performance test in a browser or use WebPageTest, and check which bundles block the CPU main thread the longest.
After identifying code-splitting candidates, we need to check the scope of changes that are required to code-split this component from the main bundle. At this point, we need to evaluate if the benefit of code-splitting outweighs the scope of changes required and the development and testing time investment. This risk is minimal to none early in the development cycle.
Finally, we need to verify that the component has been code-split correctly and that the main bundle size has decreased. We also need to build and test the component to avoid introducing potential issues.
There are a lot of steps for code-splitting a single existing component, so let’s summarize the steps in a quick checklist:
We can configure our build tools and continuous integration (CI) tools to catch bundle sizing issues early in development by setting performance budgets that can serve as a performance baseline or a general asset size limit. Build tools like Webpack, CI tools, and performance audit tools like Lighthouse can use the defined performance budgets and throw a warning if some bundle or resource goes over the budget limit. We can then run code-splitting for bundles that get caught by the performance budget monitor. This is especially useful information for pull request reviews, as we check how the added features affect the overall bundle size.
We can fine-tune performance budgets to tailor for worse possible user scenarios, and use that as a baseline for performance optimization. For example, if we use the scenario of a user browsing the site on an unreliable and slow connection on an average phone with a slower CPU as a baseline, we can provide optimal user experience for a much wider range of user devices and network types.
Alex Russell has covered this topic in great detail in his article on the topic of real-world web performance budgets and found out that the optimal budget size for those worst-case scenarios lies somewhere between 130kB and 170kB.
React Suspense And Server-Side Rendering (SSR)
An important caveat that we have to be aware of is that React
Suspense component is only for client-side use, meaning that server-side rendering (SSR) will throw an error if it tries to render the
Suspense component regardless of the fallback component. This issue will be addressed in the upcoming React version 18. However, if you are working on a project running on an older version of React, you will need to address this issue.
One way to address it is to check if the code is running on the browser which is a simple solution, if not a bit hacky.
However, this solution is far from perfect. The content won’t be rendered server-side which is perfectly fine for modals and other non-essential content. Usually, when we use SSR, it is for improved performance and SEO, so we want content-rich components to render into HTML, thus crawlers can parse them to improve search result rankings.
Until React version 18 is released, React team recommends using the Loadable Components library for this exact case. This plugin extends React’s
lazy import and
Suspense components, and adds Server-side rendering support, dynamic imports with dynamic properties, custom timeouts, and more. Loadable Components library is a great solution for larger and more complex React apps, and the basic React code-splitting is perfect for smaller and some medium apps.
Benefits And Caveats Of Code-Splitting
However, code-splitting can be easily abused and developers can get overzealous and create too many micro bundles which harm usability and performance. Dynamically loading too many smaller and irrelevant components can make the UI feel unresponsive and delayed, harming the overall user experience. Overzealous code-splitting can even harm performance in cases where the bundles are served via HTTP 1.1 which lacks multiplexing.
This content was originally published here.