You open dist/, add up the JS, and relax: “the bundle is 420 KB.” That number is how much the file takes on disk. It is not what the person who opens your app downloads.
Most CDNs and browsers deliver JavaScript compressed. Chrome, Cloudflare, and almost any modern host prefer Brotli and fall back to gzip. Transfer size — what travels over the network and pushes LCP, hydration, and time-to-interactive — is usually much smaller than raw. Sometimes the opposite happens: a change that barely moves disk size makes the file compress worse, and the user pays more than you thought.
The useful first habit is not “look at the file weight.” It is measuring the three numbers together: raw, gzip, and brotli. Bundlelens (CLI on npm, version 2.1.0, Node 22 or later) does exactly that: it runs your build or analyzes a folder that already exists, indexes the artifacts, and writes an HTML and JSON report with those three columns.
In five minutes
At the project root (where package.json lives):
npx bundlelens init That creates bundlelens.config.json with defaults. If a .gitignore exists, it adds the report folder (default bundlelens/), unless you pass --skip-gitignore.
Open the config and check two fields that have to match your stack:
-
buildCommand: the same command you would run in the terminal to produce the build (npm run build,pnpm run build, and so on). -
buildDir: the folder, relative to the config file, whose artifacts you want to measure.
A typical Vite or webpack setup:
{ "buildCommand": "npm run build", "buildDir": "dist", "outputDir": "bundlelens", "compression": { "gzip": true, "brotli": true } } compression.gzip and compression.brotli are what turn on the columns that matter. Then:
npx bundlelens run run executes the build and analyzes buildDir. If the folder is already there (for example in CI, after npm run build), you do not need to compile again:
npx bundlelens analyze dist When it finishes, open ./bundlelens/index.html. The ranking and files.html show raw, gzip, and brotli per file; treemap.html maps size by path. The same data is written to report.json.
If you do not want a config file:
npx bundlelens run "npm run build" --build-dir dist --output bundlelens If a key exists in the JSON (buildDir, outputDir, buildCommand), the file wins and the flag does not override it. So CI always measures the right folder, set it in the config or pass it only as a flag — not both in conflict.
What you are actually measuring
Raw is how large the artifact is uncompressed. It is useful to see which file grew, not to estimate the download.
Gzip is the historical floor: almost every server speaks it. Brotli usually compresses text better (JS, CSS, HTML, JSON) and is what most CDNs deliver today. If you only watch raw, you can fight a “huge chunk” that travels fine under brotli, or miss a vendor file that barely changes on disk but compresses worse after a refactor.
Bundlelens computes gzip and brotli per file while it indexes. The report lets you compare the three columns: that is where you see which file actually moves transfer size. That is also why buildDir matters so much: the tool measures what lives under that folder, not “what the user will request in a single visit.”
Pick the right folder for your framework
Not every framework leaves a dist/ you can compare apples to apples. Some write a working directory (cache, server output, manifests) next to what actually travels to the browser. If you point at the whole tree, raw, gzip, and brotli inflate, and you stop estimating the download.
Vite, webpack, React with Vite, Vue, or a Svelte SPA: usually dist/. That is the client package, and it is a good buildDir.
Create React App: typically build/.
Angular (17+): the web client usually lands in dist/my-app/browser. Use the path from your angular.json, not a generic dist/ if you have more than one project or output.
{ "buildCommand": "npm run build", "buildDir": "dist/my-app/browser", "outputDir": "bundlelens", "compression": { "gzip": true, "brotli": true } } Ionic (web build): usually www/.
Next.js: next build produces a working directory, not a package you should sum from end to end. If you static-export (output: "export"), measure out/. If you do not export and you want what the browser will request (hashed JS, CSS, and assets), point at .next/static:
npx bundlelens analyze .next/static --output bundlelens You can analyze all of .next (npx bundlelens analyze .next). Use that to inspect build artifacts, not to estimate a page download: it includes cache, server output, and internal files the user never requests.
SvelteKit: do not measure .svelte-kit; that is the internal build directory. With adapter-static, pages and client assets default to build/. With adapter-node, build/ mixes the server (index.js, server code) and the client; if you care about what the browser requests, point at build/client:
npx bundlelens analyze build/client --output bundlelens Nuxt: do not measure .nuxt (working directory) or all of .output (it includes the Nitro server). What is served as static files to the browser lives in .output/public:
npx bundlelens analyze .output/public --output bundlelens The rule is the same on any stack: choose as buildDir the folder that represents what you want to measure, not “everything the framework wrote while compiling.”
Fit it into the PR, not only your laptop
The measurement is useful when someone else can repeat the three numbers. A minimal GitHub Actions job, after the build:
- run: npm ci - run: npm run build - run: npx bundlelens@latest analyze dist --output bundlelens - uses: actions/upload-artifact@v4 with: name: bundlelens-report path: bundlelens/ Swap dist for your real buildDir: dist/my-app/browser on Angular, out or .next/static on Next.js, build or build/client on SvelteKit, .output/public on Nuxt, www on Ionic, build on CRA. There is no TTY in CI: set buildCommand and buildDir. If you want the job to fail when the build fails, use --fail-on-build or failOnBuild in the config.
The report is stored as a PR artifact. There you see raw, gzip, and brotli per file, measured on the folder you chose.