Boost Your Webpage Load Performance with the new Webpack plugin
Tags: javascript, react, webpack, frontend
Published: 2023-10-05 13:50:28

## Introduction

A website’s initial loading speed can significantly impact user experience and SEO. Slow loading times can bother many users.
To address this issue, I’ve developed a new Webpack plugin called [`html-webpack-preload-react-router`](https://github.com/FogelAI/html-webpack-preload-react-router). This plugin improves the initial loading speed of a website by reading the react router plain objects and then preloading only the essential async chunk files and performing data fetching. All of that occurs in parallel during the initial load.

## Background

When using react router with code splitting (lazy loading) the process of loading a website is:

1. Downloading and executing the initial HTML file.
2. The HTML fetches and executes only the initial Javascripts and CSS chunks.
3. The react router code examines the current URL path and runs `loader` method if defined.
4. The react router code examines the current URL path and fetching and executing only the relevant chunk files based on `element` property.

Each step involves a network request and happens sequentially. It is a waste of time! In this article we show a solution that 2nd, 3rd and 4th steps will be parallelized network requests, which significantly reducing loading times and improving user experience.

### A regular initial webpage loading example

Before I show the solution I want to show through a basic example the network waterwall of the traditional react webpage like we described in the *Background* section.

In this example, the `Products` component is code splitted through lazy loading.

*[products.route.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/regular/src/pages/Products/products.route.js)*

```javascript
//...
const Products = lazy(() =>
  import(/* webpackChunkName: 'Products' */ "./Products")
);
//...
```

So Let's see in DevTools the network waterwall when we navigate to *[https://regular-81820.web.app/products](https://regular-81820.web.app/products)*.

![Regular waterfall](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/or98l99f85rhandj1oux.jpg)

We see that there are 4 network requests in a row and this we want to parallelize as much as possible.

## The solution

After I read the great article by *Almog Gabay* on Client-Side Rendering (CSR), particularly the *[Preloading Async pages](https://github.com/theninthsky/client-side-rendering#preloading-async-pages)* section, I tought to myself why not using react router plain objects for another purposes like preloading with help from Webpack? The idea was to have a single array of plain objects serving multiple purposes - routing and preloading. After experimenting with various edge cases, I successfully created a stable Webpack plugin. This plugin generates an HTML file that handles preloading of async chunk files derived from React Router’s plain objects and also preloads data from the database (this feature is currently limited to GET requests).

### A new webpack plugin - *html-webpack-preload-react-router*

The plugin [`html-webpack-preload-react-router`](https://github.com/FogelAI/html-webpack-preload-react-router) is easy to install and setup. It gets as an argument the path for the entry route of the website.
In each object of react router plain objects we need to give the chunk name in a property called `chunkName`. The value needs to be the same value of the magic comment `webpackChunkName` of `import()`.
In addition, because we make a fetching from database, we can preload also the data by configure the property `data`.
It will be easier to understand throughout an example.

### A preloading initial webpage loading example

Let's use the same website example of *A regular initial webpage loading example*.
And do the following steps:

1. Install the plugin.

    `npm install --save-dev html-webpack-preload-react-router`

2. Configure the `webpack.config.js` file:

    *[webpack.config.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/preloading/webpack.config.js)*

    ```diff
       const HtmlWebpackPlugin = require("html-webpack-plugin");
    +  const ReactRouterPreloadChunksPlugin = require("html-webpack-preload-react-router");
    +  const entryRouteFilename = path.resolve(`./src/pages/entryRoute.route.js`);
       module.exports = {
          //...
          plugins: [
             new MiniCssExtractPlugin(),
             //...
    +        new ReactRouterPreloadChunksPlugin({
    +           entryRoute: entryRouteFilename,
    +        }),
          ]
          //...
       };
    ```

3. Add `chunkName` property for each object in the routing of react router plain objects.

    *[products.route.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/preloading/src/pages/Products/products.route.js)*

    ```diff
       export const productsRoutes = [
         {
           path: "",
    +      chunkName: "Products",
           id: "products",
           loader: () => fetch("https://dummyjson.com/products").then((res) => res.json()),
           element: <Products />,
           children: [
             {
               path: ":productid",
    +          chunkName: "Product",
               element: <Product />,
             },
           ],
         },
       ];
    ```

4. Add `data` property for each object in the routing of react router plain objects that fetch a data.

    *[products.route.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/preloading/src/pages/Products/products.route.js)*

    ```diff
       export const productsRoutes = [
         {
           path: "",
           chunkName: "Products",
           id: "products",
           loader: () =>
             fetch("https://dummyjson.com/products").then((res) => res.json()),
           element: <Products />,
           children: [
             {
               path: ":productid",
               chunkName: "Product",
               element: <Product />,
             },
           ],
    +      data: {
    +        url: "https://dummyjson.com/products",
    +        crossorigin: "anonymous",
    +      },
         },
       ];
    ```

So let's see in DevTools the network waterwall when navigating to *[https://preloading-ea687.web.app/products](https://preloading-ea687.web.app/products)*:

![Preloading waterfall](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cnupak6mppws8ej0biel.jpg)

Now we can see in the DevTools that there are 2 network group requests (instead 4 in a row), each group has network requests in parallel (first is `products` and second are `main.9aa6dd.js`, `Products.023320.js` and `products` in parallel). This is because the HTML is using preloading for async chunk files according to the URL path. It improves our website's performance!

## Bonus - Navigation bar

After we saw that we can use react router plain objects for 2 purposes of routing and preloading, I found a third purpose of using - navigation bar.
In each object of the react router plain objects we need to give the link text in a property called `linkText`.

It will be easier to understand throughout an example.

### A navigation bar example

Let's use the same website example of *A preloading initial webpage loading example*.
And do the following steps:

1. Change the react router plain objects `entryRoute`:

    *[entryRoute.route.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/navbar/src/pages/entryRoute.route.js)*

    ```diff
       export const entryRoute = [
         {
           path: "",
           element: <Layout />,
    +      id: "links",
    +      loader: () => entryRoute[0].children,
           //...
         },
       ];
    ```

2. Change the `Layout` component for reading link texts from the react router plain objects `entryRoute`:

    *[Layout.js](https://github.com/FogelAI/html-webpack-preload-react-router/blob/main/examples/navbar/src/pages/Layout.js)*

    ```diff
       //...
       function Layout() {
    +    let links = useRouteLoaderData("links");
         return (
           <nav>
             <ul>
    -          <li>
    -            <Link to="/">Home</Link>
    -          </li>
    -          <li>
    -            <Link to="/about">About</Link>
    -          </li>
    -          <li>
    -            <Link to="/products">Products</Link>
    -          </li>
    +          {links.map((link) => {
    +            return (
    +              <li>
    +                <Link to={link.path?.replace("/*", "")}>{link.linkText}</Link>
    +              </li>
    +            );
    +          })}
             </ul>
           </nav>
           //...
         );  
       }
    ```

Now we can see that the navigation bar works the same but use the data from react router plain objects. It is easier to maintain a single source of truth rather than 3 different sources.

## Summary

In this article, we tried a new webpack plugin [`html-webpack-preload-react-router`](https://github.com/FogelAI/html-webpack-preload-react-router) , which collaborates seamlessly with React Router’s plain objects. It generates a production html file that preloads async chunk files and fetched data based on the current URL path. It parallelizes network requests, which makes our webpage load faster! Additionally, React Router’s plain objects can be utilized for the navigation bar.

In short, now we can use the react router plain objects as a single source of truth for 3 purposes:
1. Routing.
2. Preloading async chunk files and fetched data from database (GET requests).
3. Navigation.

It makes our website's code easy to understand and maintained and improve the performance of initial loading webpages in our website.

I hope you enjoyed to read my article and hope you will try my new Webpack plugin [`html-webpack-preload-react-router`](https://github.com/FogelAI/html-webpack-preload-react-router).