Skip to content

Code splitting and lazy loading for large scale applications

Published: at 15:28

Code is available in my GitHub repository. README.md contains a detailed explanation of how to run the project.

Table of contents

Open Table of contents

Introduction

In today’s fast-paced digital landscape, where attention spans are short and expectations are high, performance optimization has become a cornerstone of successful web development. Users expect websites to load quickly and be responsive, regardless of the device they are using or the quality of their internet connection. As a result, developers are constantly looking for ways to improve the performance of their applications, and one of the most effective techniques for achieving this is code splitting and lazy loading.

In this blog post, I will explain what code splitting and lazy loading are, why they are important for large scale applications, and how you can implement them in your own projects. I will also discuss some best practices and common pitfalls to avoid, so you can make the most of these powerful performance optimization techniques.

What is code splitting?

Code splitting is a technique for breaking up your application’s code into smaller, more manageable chunks, which can be loaded on demand. This allows you to reduce the initial load time of your application by only loading the code that is necessary for the current page or feature. For example, if your application has a dashboard, a settings page, and a user profile page, you can split the code for each of these pages into separate bundles, so that only the code for the current page is loaded when the user navigates to it.

There are several ways to implement code splitting in a web application, but the most common approach is to use dynamic imports. Dynamic imports allow you to load modules asynchronously, which means that they are only fetched from the server when they are needed. This can be achieved using the import() function, which returns a promise that resolves to the module you want to import. For example:

import("./module.js")
  .then(module => {
    // Do something with the module
  })
  .catch(error => {
    // Handle any errors
  });

Other ways to implement code splitting

In addition to dynamic imports, there are several other ways to implement code splitting in a web application. For example, you can use a bundler like Webpack, Vite or Rollup to split your code into separate bundles, which can then be loaded on demand. Alternatively, you can use a library like React Loadable or Loadable Components to implement code splitting in a more declarative way. These libraries provide a higher-level API for dynamically loading components, which can make it easier to implement code splitting in your application.

Why is code splitting important for large scale applications?

Code splitting is particularly important for large scale applications, where the size and complexity of the codebase can have a significant impact on performance. As an application grows, so does the amount of code that needs to be loaded when the user first visits the site. This can lead to longer load times, increased memory usage, and slower page rendering, which can have a negative impact on the user experience.

Another use case for code splitting is code ownership. When a team is responsible for a large codebase, it can be difficult to maintain and understand the entire codebase. By splitting the code into smaller, more manageable chunks, you can make it easier for different teams to work on different parts of the application without stepping on each other’s toes. This can help to improve productivity and reduce the risk of introducing bugs or conflicts.

How to implement code splitting with a bundler

If you are using Webpack, Vite or Rollup to build your application, you can implement code splitting using the import() function, which is supported out of the box. For example, you can use dynamic imports to load a component when it is needed, like this:

import React, { lazy, Suspense } from "react";
// Default import
const LazyLoadedComponent = lazy(() => import("./MyLazyLoadedComponent"));
// Named import. Notice the .then() method
// const LazyLoadedComponent = lazy(() => import('./MyLazyLoadedComponent')).then(module => ({ default: module.MyLazyLoadedComponent }));

const MyComponent = () => {
  // ...
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyLoadedComponent />
      </Suspense>
    </div>
  );
};

export default MyComponent;

In this example, the MyLazyLoadedComponent is only loaded when it is needed, and the Suspense component is used to display a loading indicator while the component is being fetched. This can help to reduce the initial load time of your application and provide a better user experience for your visitors. It is important to note that the dynamic import has to be wrapped in a lazy function, which is a built-in React function that allows you to load components asynchronously.

Vite configuration

In addition to using dynamic imports in your code, you can also manually configure Vite to split your code into separate bundles. This can be achieved using chunking strategy the build.rollupOptions.output.manualChunks option, which allows you to specify which modules should be included in each chunk. For example, you can create a separate chunk for a specific library, like this:

vite.config.js

export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          react: ["react", "react-dom"],
          lodash: ["lodash"],
        },
      },
    },
  },
};

Webpack configuration

If you are using Webpack, you can configure it to split your code into separate bundles using the SplitChunksPlugin optimization.splitChunks option.

webpack.config.js

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      cacheGroups: {
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: "react",
          chunks: "all",
        },
        lodash: {
          test: /[\\/]node_modules[\\/](lodash)[\\/]/,
          name: "lodash",
          chunks: "all",
        },
      },
    },
  },
};

Rollup configuration

If you are using Rollup, you can configure it to split your code into separate bundles using code splitting the output.manualChunks option.

rollup.config.js

export default {
  output: {
    manualChunks(id) {
      if (id.includes("node_modules/react/")) {
        return "react";
      }
      if (id.includes("node_modules/lodash/")) {
        return "lodash";
      }
    },
  },
};

Route based code splitting

One common use case for code splitting is to split your code based on the routes in your application. This can be achieved using a library like React Router, which provides a built-in mechanism for lazy loading components based on the current route. For example:

main.jsx

import React, { StrictMode } from "react";
import "./index.css";
import ReactDOM from "react-dom/client";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { Movies } from "./components/Movies";
import { fetchMoviesList } from "./utils/data";
import { Root } from "./components/Root";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Root />,
    children: [
      {
        path: "/",
        element: <Movies />,
        loader: () => fetchMoviesList(),
      },
      {
        path: "/movies/:id",
        lazy: () => import("./components/LazyMovieDetails"),
      },
    ],
  },
]);
// Render the app
const rootElement = document.getElementById("root");
if (!rootElement.innerHTML) {
  const root = ReactDOM.createRoot(rootElement);
  root.render(
    <StrictMode>
      <RouterProvider router={router} />
    </StrictMode>
  );
}

Open developer tools network panel. Notice when the user navigates to the / route, the Movies component is downloaded. However, the LazyMovieDetails component is only downloaded when the user navigates to the /movies/:id route.

Another popular router Tanstack Router has a built-in mechanism for lazy loading components based on the current route.

id.jsx

import { createFileRoute } from "@tanstack/react-router";
import { fetchMovieDetails } from "../../utils/data";

export const Route = createFileRoute("/movies/$id")({
  loader: ({ params }) => fetchMovieDetails(params.id),
});

id.lazy.jsx

import { createLazyFileRoute } from "@tanstack/react-router";
import { MovieDetails } from "../../components/MovieDetails";

export const Route = createLazyFileRoute("/movies/$id")({
  component: MovieDetails,
});

Code splitting best practices

When implementing code splitting in your application, there are several best practices to keep in mind:

Conclusion

In conclusion, code splitting and lazy loading are powerful techniques for improving the performance of your web applications, especially for large scale applications. By breaking up your code into smaller, more manageable chunks, and only loading the code that is necessary for the current page or feature, you can reduce the initial load time of your application and provide a better user experience for your visitors. Whether you are using Webpack, Vite, Rollup, or a library like React Loadable or Loadable Components, there are many ways to implement code splitting in your application, so you can find the approach that works best for your specific use case. By following best practices and testing your code splitting strategy, you can make the most of these powerful performance optimization techniques and ensure that your application remains fast, responsive, and user-friendly.