Advertisement

#10 Step By Step: Lazy Loading

If you have ever found yourself in the situation where your application supports functionality that not everyone uses all the time but adds a noticable amount to your page weight or everyone uses the functionality but it is not needed right away and you would like to defer loading the code until a future time, for example in response to a user action, then lazy loading may just be the answer to your prayers. In this article we will see how to lazy load code using webpack.

The print service

  • WebUi
    • Source
      • lazy-loading
        • print.service.ts

First off we are going to start by creating a print service. This class will sit between our components and the code that we wish to lazy load (a). While we get everything up and running our print service will just print out any messages sent to the print method to the console.

print.service.ts

export class PrintService {
    private static instance: PrintService;
    private static isCreating = false;

    constructor() {
        if (PrintService.isCreating === false) {
            throw new Error("You can't call new to create PrintService instances! Call PrintService.getInstance() instead.");
        }
    }

    public static getInstance() {
        if (typeof PrintService.instance === "undefined" || PrintService.instance === null) {
            PrintService.isCreating = true;
            PrintService.instance = new PrintService();
            PrintService.isCreating = false;
        }

        return PrintService.instance;
    }

    public print = (message: string) => {
        console.log(`[Print Service] ${message}`);
    }
}
(a) Our components will interact with our lazy loaded print function through a singleton print service.

The components

  • WebUi
    • Source
      • lazy-loading
        • lazy-loading.component1.tsx
        • lazy-loading.component2.tsx

Next up we will create a couple of components that will consume the print service. Both (b) and (c) are identical except of course for the class name and message that is being passed to the print method. The purpose for them is to show the service being used in two separate files.

lazy-loading.component1.tsx

import * as React from "react";
    
import { PrintService } from "./print.service";

class Component1 extends React.Component {
    public render() {
        return <>
            <div>Component 1</div>
            <button onClick={this.print}>Print</button>
        </>;
    }

    private print = () => {
        const message = "Component 1";
        PrintService.getInstance().print(message);
    }
}

export default Component1;
(b) Component 1 uses the getInstance method to get an instance of our print service and sends 'Component 1' as the message to the print method.

lazy-loading.component2.tsx

import * as React from "react";

import { PrintService } from "./print.service";

class Component2 extends React.Component {
    public render() {
        return <>
            <div>Component 2</div>
            <button onClick={this.print}>Print</button>
        </>;
    }

    private print = () => {
        const message = "Component 2";
        PrintService.getInstance().print(message);
    }
}

export default Component2;
(c) Component 2 uses the getInstance method to get an instance of our print service and sends 'Component 2' as the message to the print method.

Adding our components to the page

  • WebUi
    • Source
      • lazy-loading
        • lazy-loading.page.tsx

Now that we have created our components we just need to add them to the page. To accomplish this we will use the minimal react code shown in (d).

lazy-loading.page.tsx

import * as React from "react";
import * as ReactDOM from "react-dom";

import Component1 from "./lazy-loading.component1";
import Component2 from "./lazy-loading.component2";

ReactDOM.render(
    <>
        <Component1/>
        <Component2/>
    </>,
    document.getElementById("root"),
);
(d) Minimal react code that is responsible for adding both of our components to the page.

Lazy loading entry point

  • WebUi
    • webpack.lazy-loading.js

We are now ready to add an entry point to our lazy loading react page in our webpack configuration file (e).

webpack.lazy-loading.js

var common = require("./webpack.common");

common.entry = {
    "lazy-loading": "./Source/lazy-loading/lazy-loading.page.tsx"
}

module.exports = common;
(e) Adding an entry point to our webpack configuration will create a new lazy loading bundle when we run our build script.

Speaking of our build script it is now time to run it. Using our usual npm run build command we can kick off our webpack build process which should result in the output shown in (f). For now we are just creating two bundles, again this is just so we can get everything up and running and make sure it is all working.

Console output showing the results of building our bundles. We are currently building two bundles, one for
        our lazy loading code and the other for react.
(f) Console output showing the results of building our bundles. We are currently building two bundles, one for our lazy loading code and the other for react.
Advertisement

Now we need a page

  • WebUi
    • Pages
      • Lazy-Loading.cshtml
      • Lazy-Loading.cshtml.cs

For completeness I am including the code for the razor page (g) and the code behind (h). As part of the webpack config that I am using any code that is in the node_modules folder is placed in its own bundle named 'common' which you can see being referenced in the page.

Lazy-Loading.cshtml

@page
@model LazyLoadingModel
@{
    ViewData["Title"] = "Lazy Loading";
}

@section Scripts {
    <environment names="Development">
        <script src="/js/common.bundle.js"></script>
        <script src="/js/lazy-loading.bundle.js"></script>
    </environment>
}

<h1>Lazy Loading</h1>
<div id="root"></div>
(g) The razor page that will display our lazy loading components.

Lazy-Loading.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebUi.Pages
{
    public class LazyLoadingModel : PageModel
    {
        public void OnGet()
        {
        }
    }
}
(h) Minimal code behind for our lazy loading page.

With all of that done we are now in a position to make sure that everything is in fact working correctly. All we have to do is fire up which ever server we are using and navigate to our lazy loading page. If we do that we should see the page should look something like (i) with the console emitting the appropriate messages when the buttons are pushed.

Image showing what our lazy loading page should look like and the console output when the buttons
        are pressed.
(i) Image showing what our lazy loading page should look like and the console output when the buttons are pressed.

Time for that lazy loading

  • WebUi
    • Source
      • lazy-loading
        • print.ts

Now that everything is working we need to create the code that we will be lazy loading. For our purposes we will use the simple function shown in (j).

print.ts

export default function print(message: string) {
    console.log(`[Lazy Loaded Print Function]: ${message}`);
}
(j) Here is our lazy loading function for printing messages to the console.

Modifying the print service

  • WebUi
    • Source
      • lazy-loading
        • print.service.ts

To accomplish what we are setting out to do we need to modify our print service so that it dynamically imports the print function that we just created (k). In this code we see a few non-obvious changes. Within the import we are adding a comment which tells webpack that we wish the code associated with it should be added to a separate bundle, in this case, named 'lazy-loading-print'. The result of the import will be a promise that we then can the then method on. Webpack will pass in the module and to get our print function we need to reference the default property. A couple of points need to be mentioned here. If you are supporting a browser that does not natively support promises you will need to polyfill it and you should add code to handle the case when the promise rejects.

print.service.ts

export class PrintService {
    ...
    public print = (message: string) => {
        import(
            /* webpackChunkName: "lazy-loading-print" */
            "./print"
        )
        .then((module) => {
            const print = module.default;
            print(message);
        });
    }
}
(k) Modifying our print service so that webpack will create a new bundle for our print function and managing the result of calling it.

Modifying our webpack config

  • WebUi
    • webpack.lazy-loading.js

The last thing we need to do is add a setting to the output object of our webpack configuration as shown in (l). This addition is what makes webpack use the name that we specified in the comment in our import statement for the name of the resulting bundle.

webpack.lazy-loading.js

...
common.output.chunkFilename = "[name].bundle.js";
...
(l) Adding the setting that will instruct webpack how to properly name our lazy load bundle.

Next up we need to build our bundles once again. When that is done you should see output that is very similar to what is shown in (m).

Now we have the three bundles that we are expecting to have.
(m) Now we have the three bundles that we are expecting to have.

Once again if we return to the our browser and click our buttons we should see the console output shown in (n). In addition to this it is important that we notice that there is only one call to our server to get our lazy loaded code no matter how many times we click the buttons.

Image showing the console message shown when we press the buttons indicates that we are now using
        the code that we lazy loaded from the server. The image also shows that although we pressed each
        button once there was only one network request.
(n) Image showing the console message shown when we press the buttons indicates that we are now using the code that we lazy loaded from the server. The image also shows that although we pressed each button once there was only one network request.
Exciton Interactive LLC
Advertisement