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:
If you want to see it in action yourself, the application is available here, and the source code: