The esbuild metafile is a hidden gem that reveals exactly where your JavaScript bundle size is coming from.

Let’s see it in action. Imagine you have a simple index.js that imports a utility library:

// index.js
import { debounce } from 'lodash-es';

const debouncedLog = debounce(console.log, 100);

debouncedLog('Hello, esbuild!');

When you build this with esbuild, you’ll want to generate the metafile:

esbuild index.js --bundle --outfile=out.js --metafile=meta.json

Now, open meta.json. It’s a JSON file that looks something like this:

{
  "inputs": {
    "index.js": {
      "bytes": 77,
      "inputs": {
        "node_modules/lodash-es/lodash.js": {
          "bytes": 250000,
          "order": 2
        }
      },
      "order": 1
    }
  },
  "outputs": [
    {
      "entryPoint": "index.js",
      "inputs": {
        "node_modules/lodash-es/lodash.js": {
          "bytes": 250000,
          "order": 2
        },
        "index.js": {
          "bytes": 77,
          "order": 1
        }
      },
      "bytes": 250100,
      "path": "out.js"
    }
  ]
}

This metafile is a snapshot of esbuild’s build process. The inputs section shows every file esbuild processed, along with its original size in bytes. The outputs section details what went into each generated output file, including the final size. Notice how lodash-es, even though we only imported debounce, is listed as a significant contributor to the bundle size.

The problem this solves is the "mystery bloat" in JavaScript bundles. Developers often struggle to understand why their bundle is so large. Tools like Webpack’s bundle-analyzer are great, but the esbuild metafile provides a more direct, programmatic, and often faster way to get this information, especially if you’re integrating it into CI/CD or automated analysis.

Internally, esbuild tracks every file it reads and writes. The metafile is essentially the serialized form of this internal graph. When esbuild analyzes index.js, it sees the import statement. It then resolves lodash-es and, by default, includes the entire lodash-es module. The metafile records this dependency chain. The bytes field in inputs shows the uncompressed size of the source file, and the bytes field in outputs shows the size of the generated output file before minification and compression.

The order field is crucial. It represents the order in which esbuild processed these files during the build. This can sometimes reveal subtle dependency issues or the order in which code is executed.

Here’s a key detail: the bytes in the outputs section for a specific input file (like node_modules/lodash-es/lodash.js within out.js) represent the size of that specific file’s contribution to the output bundle after esbuild has processed it, but before final minification and compression. This is different from the bytes in the inputs section, which is the original file size. esbuild might perform some tree-shaking or other optimizations that change the effective size of a file as it’s incorporated into the bundle, and the outputs section reflects that.

If you want to see the actual size of the lodash-es library, you’d look at its entry in the inputs section. To see how much lodash-es contributes to the final output file, you’d look at its entry in the outputs section for out.js.

The next step after analyzing your metafile is often to implement more aggressive tree-shaking or to manually import only the specific functions you need from libraries.

Want structured learning?

Take the full Esbuild course →