In this tutorial, we will learn about code splitting and understand how it helps to load websites faster.
Why is code-splitting necessary?
Modern frontend frameworks have really boosted the development speed at which the websites are being built with all the necessary boilerplates pre-defined and developers have to just focus on the business logic.
The Client-Side rendering approach is where the static files will be loaded once and then all the UI update logic will be abstracted to the client itself and only data will be fetched from the server. Depending upon the data, the UI will be dynamically rendered or changed, even the routing logic resides on the client side.
Though this has really made the web apps blazing fast, because the whole UI update and change is happening on the client side, the amount of Javascript code needed has increased.
Contrary to server-side rendering where the HTML was generated by the server and transferred over the network for the browser to display them and minor UI changes with JavaScript, now with the client-side approach the whole HTML is generated on run-time.
To generate the complete HTML on demand, libraries such as React and ReactDOM are used along with React-Router-DOM to manage the routing on the client and many other third-party libraries that will be required for the web applications.
Shipping all this amount of code, along with the polyfills, heavily increases the bundle size.
A 2018 poll found that the average javascript bundle size on mobile devices was 350 kb compressed; uncompressed, the size of the file is between 800 and 900 kb.
After transferring the 350kb compressed file, it will be uncompressed, parsed, and compiled in the browser which is an expensive thing for performance. This affects many metrics that impact the Search Engine Optimization like Time To Interactive, First contentful paint, etc.
What is code-splitting and how does it help?
To solve this issue we can use the Code-Splitting technique that splits the JavaScript and CSS bundle into different bundles (chunks) so that they can be loaded in parallel and on demand.
Splitting the bundle into smaller files helps to separate the required and not-required things and lazy load the not-required things when they are required. Though it does not reduce the overall bundle size (perhaps increase it a bit), but being split in part helps to load and process them faster.
Often, there is one main bundle main.123455.js
that loads initially, it holds all the core components that are required to load the website and then there is main.123455.js.map
that holds the mapping details for other components on when they need to be loaded.
The code-splitting takes place when the production build is being created and the anatomy of the files looks like [name].[hash].js
or [name].[hash].css
.
- name – Unique name of the file, which can be pulled from the component file name or can be randomly generated.
- hash – A hash generated from the current timestamp to remove the caching from the bundle, so that browser knows new files are there and it has to be loaded.
Just like Tree-Shaking, the Webpack bundler provides the code-splitting feature under the hood and we can leverage this functionality and can configure it as per our requirements.
Code-splitting in React
Let us try to understand the code splitting with a simple React web app example, suppose we have a web app with the following layout where there are three pages, Home
, About
, FAQ
.
Create a new instance of React application using create-react-app.
npx create-react-app code-splitting
The create-react-app boilerplate will include these dependencies, if you open the package.json
, you will see them,
"dependencies": { "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" },
In these, @testing-library
is only required during the development, and the remaining will be part of the production.
let us add the react-router-dom
for client-side routing.
npm i --save react-router-dom
Now that all the libraries are present, let’s create the app. As per the image, we will have three pages, so let’s create them.
Home
// Home.jsx const Home = () => { return <h1>Home Page</h1>; }; export default Home;
About
// About.jsx const About = () => { return <h1>About Page</h1>; }; export default About;
FAQ
// FAQ.jsx const FAQ = () => { return <h1>FAQ Page</h1>; }; export default FAQ;
Now let’s add these pages on their respective routes.
// index.jsx import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter, Routes, Route } from "react-router-dom"; const Home = React.lazy(() => import("./components/Home")); const About = React.lazy(() => import("./components/About")); const FAQ = React.lazy(() => import("./components/FAQ")); const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <BrowserRouter> <Routes> <Route index element={ <React.Suspense fallback={<>Loading</>}> <Home /> </React.Suspense> } /> <Route path="about" element={ <React.Suspense fallback={<>Loading</>}> <About /> </React.Suspense> } /> <Route path="faq" element={ <React.Suspense fallback={<>Loading</>}> <FAQ /> </React.Suspense> } /> </Routes> </BrowserRouter> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
In these, if you see, we are lazy loading the component using React.lazy
and providing a fallback using React.Suspense
.
This helps to decide the code-splitting as the components need to be lazy-loaded on demand. Only the Home page component has to be loaded on the initial load.
Thus if you create a production build by running the command
npm run build
In the build folder you will see multiple JavaScript files will be created, and the same for the CSS, if each component has a separate CSS file, multiple CSS files will also be created as you can see in the below image.
In these files, main.aafd5036.js
holds the code of all the core components like React
, React-DOM
, react-router-dom
as these are required every time.
But the pages can be loaded on demand, for example, we don’t require the Home page component code on the About page and vice-versa which is why there are separated in different files.
Home Page – 451.cc3aeec6.chunk.js
"use strict"; (self.webpackChunkcode_splitting_2 = self.webpackChunkcode_splitting_2 || []).push([ [451], { 451: function (e, n, t) { t.r(n); var c = t(184); n.default = function () { return (0, c.jsx)("h1", { children: "Home Page" }); }; }, }, ]); //# sourceMappingURL=451.cc3aeec6.chunk.js.map
FAQ Page – 929.b0f40744.chunk.js
"use strict"; (self.webpackChunkcode_splitting_2 = self.webpackChunkcode_splitting_2 || []).push([ [929], { 929: function (e, n, t) { t.r(n); var c = t(184); n.default = function () { return (0, c.jsx)("h1", { children: "FAQ Page" }); }; }, }, ]); //# sourceMappingURL=929.b0f40744.chunk.js.map
About Page – 411.656ace71.chunk.js
"use strict"; (self.webpackChunkcode_splitting_2 = self.webpackChunkcode_splitting_2 || []).push([ [411], { 411: function (e, t, n) { n.r(t); var u = n(184); t.default = function () { return (0, u.jsx)("h1", { children: "About Page" }); }; }, }, ]); //# sourceMappingURL=411.656ace71.chunk.js.map
As you can notice, each file is separated in its own chunk and they have their own sourceMappingURL
, in this file, they have the list of other files that this chuck has a dependency on.
Similarly, the remaining files also have chunks that are not required at the initial load, and also CSS files are separated the same way.
If we remove the lazy loading of the component, you will notice the chuck reduces.
//index.js import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import Home from "./components/Home"; import About from "./components/About"; import FAQ from "./components/FAQ"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <BrowserRouter> <Routes> <Route index element={<Home />} /> <Route path="about" element={<About />} /> <Route path="faq" element={<FAQ />} /> </Routes> </BrowserRouter> </React.StrictMode> ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
Now that components are not being lazy loaded, each component along with their dependencies is being loaded in a single file.
Thus it is very important to lazy load components, to provide Webpack the opportunity to do code-splitting ultimately boosting websites to load faster.
In the next tutorial, we will see how to configure Webpack to do custom code-splitting.