Skip to content

Edvins Antonovs

Maximising performance with React code splitting techniques

React is a popular JavaScript library for building user interfaces, but with growing application sizes, the JavaScript bundle size can become large, leading to slow load times and poor performance. To mitigate this problem, React provides several code-splitting techniques that allow you to split your code into smaller chunks, reducing the initial JavaScript bundle's size and improving your application's performance.

In this blog post, we'll cover the following code-splitting techniques for React:

  • Popular ways of implementing code-splitting
    • Dynamic imports
    • React Lazy with Suspense
    • Loadable Components
  • Advanced techniques
    • Route-based code splitting
    • Preloading
    • Inlining critical CSS
    • Dynamic chunks

Popular ways of implementing code-splitting

In React, there are many ways to split your code into smaller chunks and load them on demand. So we will cover the most popular ways of implementing code-splitting in React.


Dynamic imports

Dynamic imports allow you to split your code into smaller chunks and load them on demand, rather than loading everything at once. With dynamic imports, you can ensure that only the code required for a particular feature is loaded, leading to smaller initial JavaScript bundles and improved performance.

1import React, { useState, useEffect } from 'react';
2
3const HomePage = () => {
4 const [module, setModule] = useState(null);
5
6 useEffect(() => {
7 import('./HomePageWidget').then((mod) => setModule(mod.default));
8 }, []);
9
10 return module ? <module /> : <div>Loading...</div>;
11};

React Lazy with Suspense

React Lazy is a higher-order component that allows you to load components lazily, i.e., on-demand when they are needed. With React Lazy, you can ensure that your application only loads the elements it needs, reducing the size of the initial JavaScript bundle and improving performance.

1import React, { lazy, Suspense } from 'react';
2
3const HomePage = lazy(() => import('./HomePage'));
4
5const App = () => (
6 <Suspense fallback={<div>Loading...</div>}>
7 <HomePage />
8 </Suspense>
9);

Loadable Components

Loadable Components is a higher-order component that allows you to split your code into smaller chunks and load them on demand, just like React Lazy. With Loadable Components, you can specify a loading component displayed while the code chunk is being loaded.

1import React from 'react';
2import loadable from '@loadable/component';
3
4const HomePage = loadable(() => import('./HomePage'), {
5 fallback: <div>Loading...</div>,
6});
7
8const App = () => <HomePage />;

Advanced techniques

Now when we know some ways how to implement basic code-splitting, let's use them to create more advanced techniques.


Route-based code splitting

Route-based code splitting is an optimisation technique that improves the performance of web applications by only loading the necessary code for a specific route. This can significantly reduce the amount of JavaScript that must be downloaded and parsed by the browser, leading to faster load times and improved overall user experience.

Another advantage of route-based code splitting is that it allows for better scalability as the application grows. With larger applications, the size of the JavaScript bundle can become unwieldy, leading to long load times and poor performance. By splitting the code into smaller chunks based on routes, developers can ensure that only the necessary code is loaded, improving performance even as the application grows.

1import React, { lazy, Suspense } from 'react';
2
3const HomePage = lazy(() => import('./HomePage'));
4const AboutPage = lazy(() => import('./AboutPage'));
5
6function App() {
7 return (
8 <Suspense fallback={<div>Loading...</div>}>
9 <Route exact path="/" component={HomePage} />
10 <Route path="/about" component={AboutPage} />
11 </Suspense>
12 );
13}

In this example, the HomePage and AboutPage components are loaded asynchronously using the lazy function. The Suspense component provides a fallback UI while the components are loaded. This allows the browser only to download and parse the code necessary for a specific route, improving the application's performance.


Preloading

Preloading is a technique where you can load code chunks in the background while the user interacts with the application. This ensures that the code chunks are loaded before they are needed, reducing the time taken to load them and improving the overall performance of your application.

Here's an example of how you can use the React.lazy function to implement preloading in React:

1import React, { lazy, Suspense } from 'react';
2
3const LazyComponent = lazy(() => import('./LazyComponent'));
4
5function App() {
6 return (
7 <>
8 <Suspense fallback={<div>Loading...</div>}>
9 <LazyComponent />
10 </Suspense>
11 </>
12 );
13}
14
15export default App;

In the example above, the LazyComponent is loaded lazily using the React.lazy function. The Suspense component is used to provide a fallback UI while the LazyComponent is being loaded.

In this example, the LazyComponent will be loaded in the background while the user interacts with the rest of your application. When the user navigates to the LazyComponent, it will be loaded instantly, providing a smooth and seamless experience.


Inlining critical CSS

Inlining critical CSS involves including the styles required for the initial view of a page in the HTML file itself rather than loading them from a separate CSS file. This reduces the time taken to load the styles and improves the performance of your application.

Inlining critical CSS can have some advantages, but it also has some drawbacks. Some of the benefits include:

  1. Faster page load times: By including the critical styles in the HTML file, the browser can render the page faster as it doesn't have to make additional requests to fetch the critical CSS.
  2. Improved First Contentful Paint (FCP) and First Input Delay (FID) metrics: The FCP and FID metrics are essential metrics that measure the time it takes for the page to become usable for users. Inlining critical CSS can help improve these metrics, making the page more user-friendly.

However, there are also some drawbacks to consider, such as:

  1. Increased HTML file size: Inlining the critical CSS increases the size of the HTML file, which can negatively impact page load times for users on slow networks.
  2. Maintenance issues: Maintaining the critical CSS inline in the HTML file can become complicated if there are many styles or pages with inlined CSS changes.

Overall, inlining critical CSS can be a proper optimisation technique sometimes, but weighing the advantages and disadvantages before implementing it in your application is crucial.

Here's an example of how you can inline critical CSS in a React application using the styled-components library:

1import React from 'react';
2import styled, { createGlobalStyle } from 'styled-components';
3
4const GlobalStyle = createGlobalStyle`
5 /* Your critical CSS rules */
6 .header {
7 background-color: blue;
8 }
9`;
10
11const Header = styled.header`
12 /* Your non-critical CSS rules */
13 color: white;
14 font-size: 20px;
15`;
16
17function App() {
18 return (
19 <>
20 <GlobalStyle />
21
22 <Header className="header">Welcome to my React App!</Header>
23 </>
24 );
25}
26
27export default App;

In this example, the critical CSS rules are defined in the GlobalStyle component using the createGlobalStyle method from the styled-components library. These styles will be inlined in the HTML file and be available to the browser right away, without the need for an additional CSS file.

The Header component uses the styled method to define the non-critical CSS rules, which can be loaded later as a separate CSS file. This way, the critical styles are available for the initial view of the page, improving the performance, and the non-critical styles are loaded later, which won't affect the performance as much.


Dynamic chunks

Dynamic chunks allow you to split your code into smaller, more manageable pieces that can be loaded on demand. This approach can significantly improve the performance of your application by reducing the amount of code that needs to be loaded and executed at once.

Dynamic chunks are similar to dynamic imports and React Lazy, but with an important difference. With dynamic chunks, you can specify the chunk name and the file name, giving you more control over the code-splitting process. This allows you to load only the code that is necessary for the current state of your application, improving the performance and reducing the amount of time it takes for your application to load.

By using dynamic chunks, you can also improve the organization of your codebase. By breaking down your code into smaller pieces, you can better manage the loading and execution of specific parts of your application. This can make it easier to maintain and debug your code, as well as make it more scalable for future development.

1// largeFile.js
2export const ComponentA = React.lazy(() => import('./ComponentA'));
3export const ComponentB = React.lazy(() => import('./ComponentB'));

1// index.js
2import React, { lazy, Suspense } from 'react';
3import { ComponentA, ComponentB } from './largeFile';
4
5function App() {
6 return (
7 <>
8 <Suspense fallback={<div>Loading...</div>}>
9 <ComponentA />
10 <ComponentB />
11 </Suspense>
12 </>
13 );
14}

With the above code, React will only load ComponentA and ComponentB when they are needed, rather than loading all of the components at once. This can significantly improve the performance of your application, as it reduces the amount of code that needs to be loaded and executed at once.


That's it; the post covers all the latest React code-splitting techniques and provides code examples to help you implement them in your React applications. With these techniques, you can ensure that your React applications are fast and performant, delivering a great user experience to your users.

© 2024 by Edvins Antonovs. All rights reserved.