DEV Community

Cover image for Code splitting in React JS
Franklin Martinez
Franklin Martinez

Posted on

Code splitting in React JS

When your applications grow, it would be considerable to improve the initial loading speed to make it smaller, and thus avoid the user to leave our app because of having to wait for it to load.

And for that, we can use code splitting, which will help us a lot in our applications.

Note: To understand this post, you must have react router dom bases, because we are going to use a library that we are going to use to explain the code splitting.

Β 

Table of contents.

πŸ“Œ Technologies to be used

πŸ“Œ What is code splitting?

πŸ“Œ Creating the project.

πŸ“Œ First steps.

πŸ“Œ Applying code splitting.

πŸ“Œ Conclusion.

Β 

πŸ’’ Technologies to be used.

  • React JS v18.2.0
  • React router dom v6.7.0
  • Vite JS v4.0.0
  • TypeScript v4.9.3

πŸ’’ What is code splitting?

First you need to understand how most frameworks work.
Since many bundle all dependencies into one large file, which makes it easy to add JavaScript to an HTML page.
In theory, bundling JavaScript in this way should speed up page loading and reduce the amount of traffic pages have to handle.

But as an application grows, the size of its bundles also grows and, at some point, its bundles will be so large that they will take a long time to load.

This is where the code splitting technique comes in. Code splitting consists of separating the code into several packages or components that can be loaded on demand or in parallel. This means that they are not loaded until they are needed.

The page still loads the same amount of code, but the difference is because the page may not execute all the code it loads.

The benefits of code splitting are:

  • The speed at which a website loads and displays content becomes faster.
  • The interaction time improves.
  • The percentage of users who abandon the web page without interacting with it decreases.

πŸ’’ Creating the project.

We will name the project: code-splitting-react (optional, you can name it whatever you like).

npm create vite@latest
Enter fullscreen mode Exit fullscreen mode

We create the project with Vite JS and select React with TypeScript.

Then we run the following command to navigate to the directory just created.

cd code-splitting-react
Enter fullscreen mode Exit fullscreen mode

Then we install the dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

Then we open the project in a code editor (in my case VS code).

code .
Enter fullscreen mode Exit fullscreen mode

πŸ’’ First steps.

First we are going to create a few pages. We create the folder src/pages and inside we create 6 files. Which will be very simple with very little content.

Notice that the only thing that I place is a div with the name corresponding to the page.

1- Profile.tsx

export const Profile = () => {
    return (
        <div>Profile</div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Well we do the same with the following 4 pages

2- About.tsx
3- Contact.tsx
4- FAQs.tsx
5- Login.tsx

In this file we will only export each page, that is, we will use it as a barrel file.

6- index.ts

export * from './About';
export * from './Contact';
export * from './FAQs';
export * from './Login';
export * from './Profile';
Enter fullscreen mode Exit fullscreen mode

Now let's create an example of how we would normally create an app with routes without applying code splitting.

We install react router dom

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

Create a folder src/components and create a file NavBar.tsx.

import { Link } from 'react-router-dom';

export const NavBar = () => {
    return (
        <nav>
            <Link to='/home'>Home</Link>
            <Link to='/about'>About</Link>
            <Link to='/contact'>Contact</Link>
            <Link to='/faqs'>FAQs</Link>
        </nav>
    )
}
Enter fullscreen mode Exit fullscreen mode

And now inside the src/App.tsx file we delete all its content and create a new component.

import { BrowserRouter, Navigate } from 'react-router-dom';
import { NavBar } from './components/NavBar';
import { About, Contact, FAQs, Profile, Login } from './pages'

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path='profile' element={ <Profile /> } />
        <Route path='contact' element={ <Contact /> } />
        <Route path='about' element={ <About /> } />
        <Route path='faqs' element={ <FAQs /> } />
        <Route path='login' element={ <Login /> } />

        <Route path='/*' element={<Navigate to='/login' replace />} />
      </Routes>
    </BrowserRouter>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Now we are going to modify a little, to simulate the private routes. So in the same file we create 2 new components.

In this component we are going to have these routes, that will be the private ones, that is to say that the user will see them until he is authenticated.
Also note that here we are going to show the .

export const PrivateRoutes = () => {
  return (
    <>
      <NavBar />
      <Routes>
        <Route path='profile' element={ <Profile /> } />
        <Route path='about'   element={ <About />   } />
        <Route path='contact' element={ <Contact /> } />
        <Route path='faqs'    element={ <FAQs />    } />

        <Route path='/*'      element={<Navigate to='/profile' replace />} />
      </Routes>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

Then there are the public roads.


export const PublicRoutes = () => {
  return (
    <Routes>
      <Route path='login' element={ <Login /> } />

      <Route path='/*' element={<Navigate to='/login' replace />} />
    </Routes>
  )
}
Enter fullscreen mode Exit fullscreen mode

Finally we modify the App component.
We create a constant to simulate the authentication.
And depending on that constant we will create one route or another.

const isAuthenticated = false

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        {
          (isAuthenticated)
            ? <Route path="/*" element={<PrivateRoutes />} />
            : <Route path="/*" element={<PublicRoutes />} />
        }
      </Routes>
    </BrowserRouter>
  )
}
export default App
Enter fullscreen mode Exit fullscreen mode

Maybe you think that because we only render some routes and not others, the routes that are not rendered will not be loaded. But the truth is that even if it is not rendered, either the public or private route on the screen, all the components, all the pages and their css and other packages will always be loaded.

And you can check it if you run the app, and you go to inspect the page in the Network tab and filter it by JS files, you will see how all the pages are loaded, Login.tsx, Profile.tsx, About.tsx, etc.

Maybe now you will not notice any problem in the loading speed, because this app has almost no content. But if you imagine that you have more components or more packages installed, your application would load everything even if you are not using it.

So let's try to solve this problem with code splitting.

πŸ’’ Applying code splitting.

At this moment we have an "authentication" that will allow us or not to see the private pages.
But previously we saw that even if you are authenticated or not, the pages will always load.

So, what if the user just wants to login and that's it, then there is no need to load the other pages that are private. Or what if the user is already authenticated, there is no need to load the login, until the user decides to log out.

Well to apply code splitting, let's use:

React.lazy. Feature provided directly by React, allows lazy loading of imports. It is a component function, which takes as a parameter another one and finally returns a promise as a result that is expected to be resolved by a React component.

const Login = lazy(() => import('./pages/Login'));
Enter fullscreen mode Exit fullscreen mode

But, you might get an error, because the lazy function expects the component you want it to return to be a default export.

So we just go to each page, and add its default export.

Take pages/Login.tsx as an example. We'll do the same for all the pages.

const Login = () => {
    return (
        <div>Login</div>
    )
}
export default Login
Enter fullscreen mode Exit fullscreen mode

And then we create the other pages to add the lazy function to them.

const Profile = lazy(() => import('./pages/Profile'));
const About   = lazy(() => import('./pages/About'  ));
const Contact = lazy(() => import('./pages/Contact'));
const FAQs    = lazy(() => import('./pages/FAQs'   ));
const Login   = lazy(() => import('./pages/Login'  ));
Enter fullscreen mode Exit fullscreen mode

Now, let's discuss the imports so that they do not conflict with the new components that we create

// import { About, Contact, FAQs, Profile, Login } from './pages'
Enter fullscreen mode Exit fullscreen mode

But the final step is still missing, because if we see our app, it will give us an error, and that's because we need a component that suspends the rendering of the component until all its dependencies are loaded.

For it we use the component that React provides us.

import { lazy, Suspense } from 'react';

<Route 
    path='login' 
    element={
        <Suspense> 
            <Login />
        </Suspense>
    } 
/>
Enter fullscreen mode Exit fullscreen mode

Suspense, also serves as a user interface, since it contains a prop fallback that must receive a React component, which will be displayed until the component to which we apply lazy has finished loading. So this is a good place to put a loading component.

import { lazy, Suspense } from 'react';

<Route 
    path='login' 
    element={
        <Suspense fallback={<>Loading app...</>}> 
            <Login />
        </Suspense>
    } 
/>
Enter fullscreen mode Exit fullscreen mode

You will have to add the Suspense to each page that you have applied the lazy function to. And the file would look like this.

import { lazy, Suspense } from 'react';
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
import { NavBar } from './components/NavBar';
// import { About, Contact, FAQs, Profile, Login } from './pages'

const Profile = lazy(() => import('./pages/Profile'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const FAQs = lazy(() => import('./pages/FAQs'));
const Login = lazy(() => import('./pages/Login'));

const isAuthenticated = false

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        {
          (isAuthenticated)
            ? <Route path="/*" element={<PrivateRoutes />} />
            : <Route path="/*" element={<PublicRoutes />} />
        }
      </Routes>
    </BrowserRouter>
  )
}
export default App

export const PublicRoutes = () => {
  return (
    <Routes>
      <Route path='login' element={<Suspense fallback={<>...</>}> <Login /></Suspense>} />
      <Route path='/*' element={<Navigate to='/login' replace />} />
    </Routes>
  )
}

export const PrivateRoutes = () => {
  return (
    <>
      <NavBar />
      <Routes>
        <Route path='profile' element={<Suspense fallback={<>...</>}> <Profile /></Suspense>} />
        <Route path='about' element={<Suspense fallback={<>...</>}> <About /></Suspense>} />
        <Route path='contact' element={<Suspense fallback={<>...</>}> <Contact /></Suspense>} />
        <Route path='faqs' element={<Suspense fallback={<>...</>}> <FAQs /></Suspense>} />
        <Route path='/*' element={<Navigate to='/profile' replace />} />
      </Routes>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

You can even create a list with the routes and go through it with a .map to avoid having to place the Suspense too much.

But if we now go and look again at the Network tab. The truth is that we will not see a big difference in the loading speed, because it is a very small app.

But now, change the authentication to true, so that the private part of the routes is rendered.

const isAuthenticated = true
Enter fullscreen mode Exit fullscreen mode

Pay attention to the JS files, you will see that not all of them are loaded, only the Profile.tsx because it is the page you are currently viewing.

If you start moving between pages with the Navbar, you will see how they are loaded every page you visit, so they are only loaded until you need them.

This is how we would apply code splitting to improve the performance of our React applications.

I'm not telling you to use lazy on all components either, because it can cause longer load times. Try to load only components that are not visible in the initial rendering.

πŸ’’ Conclusion.

Code splitting is a common practice in large React applications, and the speed increase it provides can determine whether a user continues to use a web application or abandons it, so trimming even fractions of a second could be significant.

I hope I have helped you understand a little more about this topic. If you know of any other different or better way to perform this application feel free to comment. πŸ™Œ

I invite you to check my portfolio in case you are interested in contacting me for a project!. Franklin Martinez Lucas

πŸ”΅ Don't forget to follow me also on twitter: @Frankomtz361

Top comments (1)

Collapse
 
pedrotoliveira profile image
Pedro Thiago A. G. Oliveira • Edited

Hey Franklin, nice topic.

I have some questions.

  1. Does React.lazy and Suspense features work with the webpack or turbo tools?
    I saw that you started the project using Vite, if not, what is the best approach to migrating a legacy project in your opinion?

  2. About splitting the application into pieces, could we achieve the same goal by using Module Federation feature in webpack? What do you think about it?
    Reference: webpack.js.org/concepts/module-fed...

Thank you for sharing! Regards.