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:
-
Split your code into logical chunks: When splitting your code, it’s important to think about how your application is structured and what code is needed for each page or feature. Try to split your code into logical chunks that make sense from a user’s perspective, so that you can minimize the amount of code that needs to be loaded for each page.
-
Use a bundler to automate code splitting: If you are using a bundler like Webpack, Vite or Rollup, take advantage of its built-in code splitting features to automate the process. This can help you to split your code more efficiently and avoid common pitfalls, such as duplicating code or creating unnecessary chunks.
-
Monitor your bundle size: Keep an eye on the size of your code bundles, and use tools like Bundle Analyzer to identify any large or unnecessary chunks. This can help you to optimize your code splitting strategy and ensure that your application remains fast and responsive.
-
Test your code splitting strategy: Before deploying your application, test your code splitting strategy to ensure that it works as expected and doesn’t introduce any bugs or performance issues. This can help you to catch any problems early on and make any necessary adjustments before they become more difficult to fix.
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.