esbuild can bundle multiple entry points into a single build, but it’s not just about spitting out multiple files; it’s about creating distinct, independent application bundles that can be loaded by different HTML pages.
Let’s say we have a multi-page application with two distinct pages: a main dashboard and a user profile page. Each will need its own JavaScript bundle.
Here’s how our project might be structured:
.
├── src
│ ├── dashboard
│ │ └── index.js
│ └── profile
│ └── index.js
├── index.html
└── profile.html
Our dashboard/index.js might look like this:
// src/dashboard/index.js
import { renderDashboard } from './ui';
import { fetchData } from './api';
async function initDashboard() {
const data = await fetchData('/api/dashboard');
renderDashboard(data);
}
initDashboard();
And profile/index.js:
// src/profile/index.js
import { renderProfile } from './ui';
import { fetchUserProfile } from './api';
async function initProfile() {
const userId = new URLSearchParams(window.location.search).get('id');
const user = await fetchUserProfile(userId);
renderProfile(user);
}
initProfile();
Our index.html would include the dashboard bundle:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
</head>
<body>
<div id="dashboard-root"></div>
<script src="dashboard-bundle.js"></script>
</body>
</html>
And profile.html would include the profile bundle:
<!-- profile.html -->
<!DOCTYPE html>
<html>
<head>
<title>User Profile</title>
</head>
<body>
<div id="profile-root"></div>
<script src="profile-bundle.js"></script>
</body>
</html>
To build these separate bundles with esbuild, we use the entryPoints option, providing an object where keys are the desired output file names and values are the paths to the entry point files.
// esbuild.config.js
const esbuild = require('esbuild');
esbuild.build({
entryPoints: {
'dashboard-bundle': 'src/dashboard/index.js',
'profile-bundle': 'src/profile/index.js',
},
bundle: true,
outdir: 'dist', // Output directory for the bundles
format: 'esm', // Or 'iife' depending on your needs
splitting: true, // Crucial for code splitting
sourcemap: true,
}).catch(() => process.exit(1));
Running this configuration will produce two distinct JavaScript files in the dist directory: dashboard-bundle.js and profile-bundle.js. Each file contains the code necessary to run its respective page, and importantly, they are independent of each other. If dashboard-bundle.js has a bug, it won’t prevent profile-bundle.js from loading and executing correctly.
The splitting: true option is key here. It tells esbuild to break down the code into smaller, reusable chunks. For multi-page apps, this is often used in conjunction with format: 'esm' (ECMAScript Modules) to generate separate module files that can be imported by the HTML. Even if you don’t explicitly use dynamic import() within your code for splitting, splitting: true with multiple entry points will ensure that common dependencies are not duplicated across bundles, and that each entry point is self-contained.
The format option determines how the output modules are structured. 'esm' generates standard ES Modules that can be imported by other modules or script tags with type="module". 'iife' (Immediately Invoked Function Expression) creates self-executing functions, which are useful if you’re not using ES Modules in your HTML or need broader browser compatibility without a build step on the client.
When esbuild encounters multiple entry points, it treats each one as a root for a separate dependency graph. It then resolves all the imports for each entry point independently. If there are shared dependencies (like a utility library used by both dashboard and profile), esbuild, when splitting: true is enabled, will automatically create separate shared chunk files (e.g., chunk-[hash].js) that are then imported by the main entry point bundles. This avoids code duplication and ensures that if a shared dependency is updated, you only need to re-download that one chunk.
The outdir option specifies where all the generated bundles, including any dynamically created shared chunks, will be placed. This keeps your build output organized.
The real power of this configuration emerges when you consider how these independent bundles are loaded. Each HTML file (index.html, profile.html) points to its specific bundle via a <script> tag. This isolation is fundamental to multi-page architectures. If the dashboard page is slow to load due to a large dashboard bundle, it doesn’t impact the initial load time or interactivity of the profile page, and vice versa.
Consider a scenario where you have a shared UI component library that both your dashboard and profile pages use. Without splitting: true, esbuild would likely duplicate the entire UI library’s code into both dashboard-bundle.js and profile-bundle.js. By enabling splitting: true, esbuild will detect this shared code and extract it into a separate file (e.g., chunk-abc123.js). Both dashboard-bundle.js and profile-bundle.js will then have an import statement pointing to this chunk-abc123.js, ensuring the library is downloaded and parsed only once for the entire application, even though the entry points are distinct.
This approach allows you to manage complexity by segmenting your application. Each entry point can be optimized independently, and the build process ensures that dependencies are handled efficiently across these separate modules. You can even have different configurations (like different CSS processing or environment variables) for each entry point if you were to use a more advanced esbuild plugin or a more sophisticated build orchestrator, though the basic entryPoints option handles the core bundling.