| | |

esbuild 25 ⚡ Metafile Analysis — Bundle composition & dependency graphs

⏱️ Estimated read: 13 min | |

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

KeyContains
inputsEvery module path, its bytes, and its imports array
outputsEvery output file, its bytesInOutput, entryPoint, and imports array
importsThe path and kind of each import edge

The visualizer tools

ToolFocusOutput
esbuild-visualizerTreemap, sunburst, networkHTML
angular-esbuild-visualizerAngular two-tier loadingHTML, JSON
esbuild-lazy-analyzerCode-splitting, eager/lazyJSON, HTML
@espcom/esbuild-plugin-webpack-analyzerLocal serverLive viewer
@rnx-kit/esbuild-bundle-analyzerCompare, duplicatesCLI

The analysis commands

CommandPurpose
esbuild-visualizer --metadata stats.jsonGenerate the treemap
esbuild-lazy-analyzer --metafile stats.jsonAnalyze code-splitting
@rnx-kit/esbuild-bundle-analyzer compareDiff two metafiles
@rnx-kit/esbuild-bundle-analyzer analyze --show-duplicatesDuplicate 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

PitfallWhy It HappensFix
The metafile is emptymetafile: true not setSet the option
The visualizer shows no dataThe wrong metafile pathCheck the --metadata path
The import chain is not obviousThe metafile is a flat objectFollow the imports arrays recursively
The comparison failsThe two metafiles have different keysUse 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

ItemValue
Metafile optionmetafile: true
Metafile structureinputs (modules, bytes, imports) and outputs (chunks, entry points)
Import chainFollow the imports array from the entry point
Visualizeresbuild-visualizer (treemap, sunburst, network)
Code-splitting analysisesbuild-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 inputs and outputs. 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 inputs object maps every module to its imports array. 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 bytesInOutput and entryPoint. A chunk that imports another chunk is a code-splitting relationship. The esbuild-lazy-analyzer reports 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-analyzer has a compare command 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.

Was this helpful?


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!