How to lazy load with esbuild

In this article, I will show you how to lazy load with esbuild. It's achieved by using a work-in-progress flag --splitting, so you may want to check out the documentation before you will start building something very complex with it.

Lazy loading

Is a pattern of delaying the download of a resource until it's needed. A common approach in web applications is to split critical & non-critical code into different files. In this way, non-critical code can be lazy-loaded in the background, while the user has already access to most of the features of the app.

The example

Similar to what I used in the webpack example, here we will have a simple js application, that happens to depend on a big, 3rd party library. The library I use, PDF-LIB was already discussed in an earlier post. PDF creation is a complex task, which requires a lot of code. Let's imagine an invoice application - one that allows for creating invoices & generating PDFs. it's an important feature of an application, but only called from some route & even there not needed immediately.

The code

For the example application, I have few files. index.html:

<!-- index.html -->
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <title>Lazy load in esbuild</title>
    <link rel="shortcut icon" href="#" />

    <div id="view"></div>

    <script type="module" src="./dist/index.js"></script>
  </head>
  <body></body>
</html>

Simple loading the builts JS from the dist folder.

src/index.js:

const view = document.getElementById("view");

view.innerHTML = `<button id="pdf-button">Generate PDF</button>
<br>
<iframe id="pdf" style="width: 350px; height: 600px"></iframe>`;

import("pdf-lib").then(({ PDFDocument }) => {
  const pdfButton = document.getElementById("pdf-button");

  pdfButton.addEventListener("click", async () => {
    const pdfDoc = await PDFDocument.create();
    const page = pdfDoc.addPage([350, 400]);
    page.moveTo(110, 200);
    page.drawText("Hello World!");
    const pdfDataUri = await pdfDoc.saveAsBase64({ dataUri: true });
    document.getElementById("pdf").src = pdfDataUri;
  });
});

In this one file, we have 2 sections that will be executed in a different moments. The first 2 lines are run immediately after loading the js. They have our critical path - they set up the view for the user to interact with, while we load the rest of JS. The other is the callback for the dynamic import of pdf-lib. You can read more about dynamic imports on mdn, but in short, they are a part of the es-module specification. In short - it's loading another file during the runtime, and resolving a promise when it's available.

For the best user experience, you could set the Generate PDF button inactive here, and turn it active after PDF-LIB is available. For the sake of simplicity of the example code, I left the button unresponsive while the library loads.

Dependencies

After initializing your package with:

$ npm init -y

you can install all dependencies with:

$ npm install --save esbuild pdf-lib

Build code

You can add the build CLI command as an npm script to package.json:

{
  ...
  "scripts": {
    ...
    "build": "esbuild src/index.js --bundle --outdir=dist --splitting --format=esm"
  }
...

The values we have here:

  • src/index.js - the entry point of the application
  • --bundle - we tell the esbuild to bundle the whole application
  • --outdir=dist - because of using splitting, just specifying the output file with --outfile is not enough - esbuild needs directory to put all chunks it creates there
  • --splitting - we turn on the experimental splitting behavior
  • --format=esm - another requirement of splitting to work - as of now, it's only working with es-modules output

Video course

You can check out my course about esbuild.

Summary

After all this, our application will lazy load the big 3rd party dependency:

esbuild-lazy-load.png

If you want to see it in action yourself, the application is available here, and the source code: