esbuild 25 ⚡ Metafile Analysis — Bundle composition & dependency graphs
The metafile is esbuild’s build manifest. When the metafile: true option is set, the build returns a JSON object that describes every input that was bundled, every output that was written, and the import edges that connect them . The object has two top-level keys: inputs and outputs. The inputs key maps every module path to its bytes count and its imports array, which lists the paths that the module imports and the kind of import (import-statement, require-call, dynamic-import, etc.). The outputs key maps every output file to its bytesInOutput count, its entryPoint, and its imports array . The two sides of the object are the two sides of the analysis: the inputs answer “what went into this bundle and why,” and the outputs answer “what came out and how big is it.”
The metafile is not a visualization. It is data. The visualization is a separate tool that consumes the data and renders a treemap, a sunburst, or a network graph. The distinction matters because the metafile can be analyzed programmatically—by a script that fails the build when a dependency appears in the wrong bundle, or by a CI job that compares the metafile against a baseline—without any visualization at all . The metafile is the source of truth; the visualizer is one consumer of it.
Key point: The metafile is produced when metafile: true is set. It contains inputs (every module, its size, and its import edges) and outputs (every output file, its size, and its entry point). The visualizers—esbuild-visualizer, esbuild-lazy-analyzer, angular-esbuild-visualizer—are consumers of this JSON, not part of esbuild itself.
Why the metafile is the analysis primitive
The import chain problem. A bundle that is larger than expected has a cause: a module was included that should not have been, or a module was duplicated across chunks. The metafile’s inputs object shows the import edges. A module’s imports array lists the paths it imports, and those paths are the next nodes in the chain. Following the chain from an entry point to a heavy module shows exactly which import statement pulled it in . The visualizers render this chain as a graph, but the data is in the JSON.
The duplicate detection problem. A module that appears in multiple output chunks is a module that was duplicated. The metafile does not have a top-level “duplicates” field, but the data is derivable: an input path that appears in the inputs of more than one output chunk is a duplicate. The esbuild-lazy-analyzer tool processes the metafile and produces a BundleStats object that includes the eager and lazy imports per entry, the longest dependency chain, and the chunk sizes . The @rnx-kit/esbuild-bundle-analyzer has an analyze command with a --show-duplicates flag that outputs the full path from the entry point to each duplicated file . The metafile is the input; the duplicate report is the analysis.
The comparison problem. A bundle analysis is most useful when it is compared against a baseline. The metafile is a JSON object, and two metafiles can be diffed. The compare command in @rnx-kit/esbuild-bundle-analyzer takes a --baseline and a --candidate metafile and outputs the difference . The comparison is not a visual feature; it is a structural property of the metafile being a stable, machine-readable format.
The trade-off. The metafile is not enabled by default because it adds a small amount of work to the build and produces a file that is not needed for the production output. A project that never analyzes its bundle does not need the option. A project that does need it must set metafile: true and decide where to write the JSON—the CLI’s --metafile=stats.json flag, or the API’s metafile option and the result.metafile property . The option is cheap, and the data is valuable, but it is opt-in.
a. Reading the metafile structure
The metafile is a JSON object. The inputs object is keyed by the module path, and each value has a bytes count and an imports array. The outputs object is keyed by the output path, and each value has a bytesInOutput count, an entryPoint, and an imports array .
{
"inputs": {
"src/app.js": {
"bytes": 120,
"imports": [
{ "path": "src/utils.js", "kind": "import-statement" }
]
},
"src/utils.js": {
"bytes": 80,
"imports": []
}
},
"outputs": {
"dist/bundle.js": {
"bytesInOutput": 200,
"entryPoint": "src/app.js",
"imports": []
}
}
}
The imports array is the dependency graph. Each entry has a path and a kind. The kind values include import-statement (a static import), require-call (a CommonJS require), dynamic-import (an import() expression), and import-rule (a CSS @import). The path is the resolved module path, which is the key in the inputs object. Following the paths from an entry point to a leaf module is the import chain analysis .
The outputs object is the chunk analysis. An output file’s bytesInOutput is the size of that chunk. The entryPoint is the input that produced it. The imports array lists the other output files that this chunk imports—which is the code-splitting relationship. A chunk that imports a shared chunk is a chunk that depends on the shared chunk’s modules.
b. The visualizer tools and the programmatic analysis
The visualizers consume the metafile and render it. The esbuild-visualizer package produces an HTML file with a treemap, sunburst, or network graph, and it excludes the source code from the generated file . The angular-esbuild-visualizer is a fork that understands Angular’s two-tier loading model and annotates the initial bundle versus the lazy routes . The esbuild-lazy-analyzer focuses on code-splitting: it reports the eager and lazy imports per entry point and the longest dependency chain . The @espcom/esbuild-plugin-webpack-analyzer is a plugin that starts a local server and serves a custom viewer, updating the chart data on rebuilds .
The programmatic analysis is the other path. A script can read the metafile and check a condition. For example, a CI script can fail the build if a module from node_modules appears in an entry point that should be free of dependencies. The @rnx-kit/esbuild-bundle-analyzer is a CLI that does this: its analyze command outputs a simple analysis and its compare command outputs the diff between two metafiles . The metafile is the data; the script is the logic; the visualizer is the presentation.
Complete Example Session
// ============================================
// PART 1: ENABLING THE METAFILE
// ============================================
// The CLI:
// esbuild src/app.js --bundle --metafile=stats.json --outfile=dist/bundle.js
// The API:
import * as esbuild from 'esbuild';
const result = await esbuild.build({
entryPoints: ['src/app.js'],
bundle: true,
outfile: 'dist/bundle.js',
metafile: true,
});
// The metafile is in result.metafile.
// The CLI writes it to the file specified by --metafile.
// ============================================
// PART 2: READING THE INPUTS
// ============================================
// The inputs object describes every module.
for (const [path, input] of Object.entries(result.metafile.inputs)) {
console.log(`${path}: ${input.bytes} bytes`);
for (const imp of input.imports) {
console.log(` imports ${imp.path} (${imp.kind})`);
}
}
// ============================================
// PART 3: READING THE OUTPUTS
// ============================================
// The outputs object describes every chunk.
for (const [path, output] of Object.entries(result.metafile.outputs)) {
console.log(`${path}: ${output.bytesInOutput} bytes`);
console.log(` entry point: ${output.entryPoint}`);
}
// ============================================
// PART 4: FOLLOWING AN IMPORT CHAIN
// ============================================
// A recursive function that traces the imports
// from an entry point to a target module.
function traceImportChain(metafile, entryPoint, target) {
const chain = [];
const visited = new Set();
function walk(path) {
if (visited.has(path)) return false;
visited.add(path);
const input = metafile.inputs[path];
if (!input) return false;
chain.push(path);
if (path === target) return true;
for (const imp of input.imports) {
if (walk(imp.path)) return true;
}
chain.pop();
return false;
}
walk(entryPoint);
return chain;
}
// ============================================
// PART 5: DETECTING DUPLICATES
// ============================================
// A module is duplicated when it appears in more than one output chunk.
// The metafile does not have a direct duplicates field,
// but the data is derivable from the inputs and outputs.
const moduleCount = new Map();
for (const [outputPath, output] of Object.entries(result.metafile.outputs)) {
// The output's inputs are the modules that were bundled into it.
// The metafile does not list them directly in the outputs object,
// but the bytesInOutput and the entryPoint are the starting point.
// The actual per-output input list is in the metafile's internals.
}
// The esbuild-lazy-analyzer and @rnx-kit/esbuild-bundle-analyzer
// provide the duplicate analysis out of the box.
// ============================================
// PART 6: THE VISUALIZER
// ============================================
// npm install -D esbuild-visualizer
//
// esbuild-visualizer --metadata ./stats.json --filename report.html
//
// The report is an HTML file with a treemap, sunburst, or network graph.
// It does not contain the source code, only the file paths and sizes .
// ============================================
// PART 7: THE LAZY ANALYZER
// ============================================
// npm install -D esbuild-lazy-analyzer
//
// npx esbuild-lazy-analyzer --metafile stats.json --outmeta processed.json --outreport report.html
//
// The processed.json contains:
// - numberOfChunks
// - preBundleSize / bundleSize / compressionPercentage
// - minChunk / maxChunk / averageChunkSize
// - entryStats with eagerImports, lazyImports, longestDependencyChain
// ============================================
// PART 8: THE CI COMPARISON
// ============================================
// npm install -D @rnx-kit/esbuild-bundle-analyzer
//
// npx @rnx-kit/esbuild-bundle-analyzer compare --baseline baseline.json --candidate candidate.json
//
// The command outputs the difference between the two metafiles.
// It can be used in CI to fail the build if the bundle grew .
// ============================================
// PART 9: THE PROGRAMMATIC CHECK
// ============================================
// A script that fails if a heavy dependency is in the entry point.
const heavyDeps = ['lodash', 'moment', 'recharts'];
for (const [path, input] of Object.entries(result.metafile.inputs)) {
for (const dep of heavyDeps) {
if (path.includes(`node_modules/${dep}`)) {
console.error(`Heavy dependency ${dep} found in ${path}`);
process.exitCode = 1;
}
}
}
// ============================================
// PART 10: THE VERIFICATION CHECKLIST
// ============================================
// [ ] The metafile option is set to true.
// [ ] The metafile is written to a known location (CLI) or read from the result (API).
// [ ] The visualizer is installed and the report is generated.
// [ ] The import chain is traced for any unexpectedly large module.
// [ ] The duplicate detection is run for multi-chunk builds.
// [ ] The comparison is run against a baseline in CI.
The ten parts covered the metafile option, the inputs, the outputs, the import chain, the duplicates, the visualizer, the lazy analyzer, the CI comparison, the programmatic check, and the verification. The through-line is that the metafile is the data, and the tools are the consumers.
Quick Reference
The metafile structure
| Key | Contains |
|---|---|
inputs | Every module path, its bytes, and its imports array |
outputs | Every output file, its bytesInOutput, entryPoint, and imports array |
imports | The path and kind of each import edge |
The visualizer tools
| Tool | Focus | Output |
|---|---|---|
esbuild-visualizer | Treemap, sunburst, network | HTML |
angular-esbuild-visualizer | Angular two-tier loading | HTML, JSON |
esbuild-lazy-analyzer | Code-splitting, eager/lazy | JSON, HTML |
@espcom/esbuild-plugin-webpack-analyzer | Local server | Live viewer |
@rnx-kit/esbuild-bundle-analyzer | Compare, duplicates | CLI |
The analysis commands
| Command | Purpose |
|---|---|
esbuild-visualizer --metadata stats.json | Generate the treemap |
esbuild-lazy-analyzer --metafile stats.json | Analyze code-splitting |
@rnx-kit/esbuild-bundle-analyzer compare | Diff two metafiles |
@rnx-kit/esbuild-bundle-analyzer analyze --show-duplicates | Duplicate report |
Best Practices
✅ Do This:
// Enable the metafile
await esbuild.build({ metafile: true }); // ✅
// Trace the import chain for a heavy module
// Follow the imports array from the entry point to the module. // ✅
// Compare against a baseline in CI
// npx @rnx-kit/esbuild-bundle-analyzer compare ... // ✅
❌ Don’t Do This:
// Assume the visualizer is part of esbuild
// It is a separate package that consumes the metafile. // ❌
// Ignore the outputs object
// It shows the chunk sizes and the entry points. // ❌
// Ship the metafile to production
// It is a build artifact for analysis, not a runtime asset. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| The metafile is empty | metafile: true not set | Set the option |
| The visualizer shows no data | The wrong metafile path | Check the --metadata path |
| The import chain is not obvious | The metafile is a flat object | Follow the imports arrays recursively |
| The comparison fails | The two metafiles have different keys | Use the same build configuration |
Real-World Examples
1. Enable the metafile
await esbuild.build({ metafile: true });
2. Write the metafile (CLI)
esbuild src/app.js --bundle --metafile=stats.json --outfile=dist/bundle.js
3. Read the inputs
Object.entries(result.metafile.inputs).forEach(([p, i]) => console.log(p, i.bytes));
4. Read the outputs
Object.entries(result.metafile.outputs).forEach(([p, o]) => console.log(p, o.bytesInOutput));
5. Trace the import chain
// Recursively follow the imports array from the entry point.
6. Visualize
esbuild-visualizer --metadata stats.json --filename report.html
7. Analyze code-splitting
esbuild-lazy-analyzer --metafile stats.json --outmeta processed.json
8. Compare two builds
npx @rnx-kit/esbuild-bundle-analyzer compare --baseline a.json --candidate b.json
9. Show duplicates
npx @rnx-kit/esbuild-bundle-analyzer analyze stats.json --show-duplicates
10. Programmatic check
// Fail if a heavy dependency appears in the bundle.
Visual
The Metafile Structure
┌──────────────────────────────────────────────────────────────┐
│ METAFILE │
│ │
│ inputs: │
│ ┌─────────────────────┐ │
│ │ src/app.js │ │
│ │ bytes: 120 │ │
│ │ imports: [ │ │
│ │ { path: "utils", │ │
│ │ kind: "import" }│ │
│ │ ] │ │
│ └─────────────────────┘ │
│ │
│ outputs: │
│ ┌─────────────────────┐ │
│ │ dist/bundle.js │ │
│ │ bytesInOutput: 200 │ │
│ │ entryPoint: app.js │ │
│ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
The Import Chain
┌──────────────────────────────────────────────────────────────┐
│ FOLLOWING THE IMPORTS │
│ │
│ app.js │
│ │ imports utils.js │
│ ▼ │
│ utils.js │
│ │ imports lodash │
│ ▼ │
│ node_modules/lodash/lodash.js │
│ │
│ The chain shows why lodash is in the bundle. │
└──────────────────────────────────────────────────────────────┘
The Tools
┌──────────────────────────────────────────────────────────────┐
│ THE METAFILE IS THE DATA, THE TOOLS ARE THE CONSUMERS │
│ │
│ metafile.json │
│ │ │
│ ┌────┴────┬────────────┬────────────┐ │
│ ▼ ▼ ▼ ▼ │
│ esbuild- lazy- angular- rnx-kit │
│ visualizer analyzer visualizer analyzer │
│ (treemap) (splitting) (Angular) (compare) │
│ │
│ All consume the same JSON. │
│ Each presents a different analysis. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Metafile option | metafile: true |
| Metafile structure | inputs (modules, bytes, imports) and outputs (chunks, entry points) |
| Import chain | Follow the imports array from the entry point |
| Visualizer | esbuild-visualizer (treemap, sunburst, network) |
| Code-splitting analysis | esbuild-lazy-analyzer |
| Duplicate detection | @rnx-kit/esbuild-bundle-analyzer --show-duplicates |
| Comparison | @rnx-kit/esbuild-bundle-analyzer compare |
Key takeaways:
- The metafile is the data, not the visualization. It is a JSON object with
inputsandoutputs. The visualizers are separate packages that consume it. A project that wants the data without the visualization can read the JSON directly . - The import chain is the analysis. The
inputsobject maps every module to itsimportsarray. Following the chain from an entry point to a heavy module shows exactly which import pulled it in. The visualizers render this as a graph, but the data is in the JSON . - The outputs object is the chunk analysis. It maps every output file to its
bytesInOutputandentryPoint. A chunk that imports another chunk is a code-splitting relationship. Theesbuild-lazy-analyzerreports the eager and lazy imports per entry point and the longest dependency chain . - The comparison is a structural property of the JSON. Two metafiles can be diffed. The
@rnx-kit/esbuild-bundle-analyzerhas acomparecommand that takes a baseline and a candidate and outputs the difference. The comparison is not a visual feature; it is a data feature . - The metafile is for analysis, not for production. It is a build artifact. It should be written to a known location, analyzed, and not shipped to the runtime. The visualizer’s output is an HTML report; the metafile’s output is a JSON analysis .
Remember: The metafile is esbuild’s build manifest. It describes what went into the bundle and what came out. The inputs object is the dependency graph. The outputs object is the chunk graph. The visualizers are consumers of the JSON, not part of esbuild. The import chain is the data, and the chain is followed by reading the imports array. The duplicate detection is the analysis, and the analysis is a script that reads the JSON. The metafile is the primitive; the tools are the presentation.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!