Inspect a JavaScript/TypeScript production bundle and find where the bytes actually go. The skill builds a stats report, attributes weight to specific modules and packages, and hunts for the patterns that bloat bundles in practice — a 200KB date library imported for one helper, two copies of the same package at different versions, a route that ships eagerly instead of lazily, a polyfill set targeting browsers you dropped years ago, or server-only code that slipped past the client boundary. It returns a ranked list of concrete reductions with the estimated savings of each, so you fix the 80KB win before the 4KB one.
When to use this skill
- A production bundle (or a specific route/chunk) has grown past budget and you need to know exactly what to cut.
- You suspect duplicate packages, a heavyweight dependency, or a barrel import dragging in a whole library.
- A first-load or main chunk is too big and you want to know what should be code-split or deferred.
- You want to confirm dev-only tooling, source maps, or server code is not leaking into the client bundle.
[!NOTE]
Always measure the production build, not dev. Dev bundles include HMR runtime, unminified source, and no tree-shaking, so their sizes are meaningless for this analysis. Compare gzip/brotli transfer sizes, not raw bytes — that is what users actually download.
Instructions
- Locate the build and detect the bundler. Identify the toolchain before doing anything — do not guess. Check
package.json scripts and lockfiles for next, vite, webpack, rollup, esbuild, or @remix-run. Note the package manager (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb) since duplicate-detection commands differ per manager.
- Produce a stats report using the project's own analyzer. Match existing config rather than bolting on a new tool:
- Next.js — run the production build and read its per-route First Load JS table; if
@next/bundle-analyzer is wired up, run ANALYZE=true npm run build.
- Vite/Rollup — use
rollup-plugin-visualizer if present, or build and inspect dist/assets/* sizes.
- Webpack — generate
--json stats (webpack --profile --json=stats.json, which writes the file directly so console warnings don't corrupt the JSON) and analyze, e.g. with source-map-explorer over the emitted bundle + map.
- If no analyzer is configured, fall back to
npx source-map-explorer 'dist/**/*.js' against the built output and its source maps.
- Attribute weight to packages, not just files. Map the largest modules back to their npm packages. For each heavyweight dependency, determine whether it is fully used or pulled in by a barrel/side-effect import, and whether a lighter alternative exists (e.g.
date-fns/dayjs over moment, native Intl over numeral, lodash-es with named imports over lodash).
- Detect duplicates and version skew. Run
npm ls <pkg> / pnpm why <pkg> / yarn why <pkg> on suspect packages to find the same library bundled at multiple versions, and check for both ESM and CJS copies of the same dep. Flag candidates for resolutions/overrides or dedupe.
- Find missing code-splitting and oversized polyfills. Look for large modules in the entry/main chunk that are only needed on one route or behind an interaction (charts, editors, markdown renderers, PDF libs) — these belong behind
import() / next/dynamic / React.lazy. Inspect the polyfill/transpile target (browserslist, target in tsconfig/vite/tsup) for core-js or regenerator-runtime bloat aimed at browsers you no longer support.
- Hunt for leaked dev/server code. Grep the client bundle and imports for things that should never ship: test/mock files,
process.env debug branches, server-only modules (fs, crypto server usage, DB clients, secrets), and dev dependencies imported from app code. In Next.js, confirm Server Component / "use client" boundaries are not dragging server modules into client chunks.
- Verify each proposed cut. Do not estimate from intuition alone. Where feasible, apply the change behind the analyzer (or
--dry) and re-run the build to measure the real delta. At minimum, cite the measured pre-change size from the stats report for every finding.
- Report a ranked plan. Output findings ordered by estimated gzip savings, each with: the module/package, current size, the specific fix, the expected reduction, and a rough effort/risk rating. Flag anything you could not measure precisely so the user knows what to confirm.
[!WARNING]
Tree-shaking only works on side-effect-free ESM. A default or namespace import from a CJS package (or a package missing "sideEffects": false) pulls in the whole module regardless of what you use — so "import one helper" can still cost the full library. Verify the import shape, not just the import statement.
Examples
A ranked findings report for a Next.js app whose largest route shipped 412 KB of First Load JS:
Bundle analysis — route /dashboard (First Load JS: 412 KB gzip → target 180 KB)
Ranked by estimated gzip savings:
1. moment + moment-timezone .................. 71 KB [HIGH]
Imported in 3 files for formatting only. Replace with date-fns
named imports (tree-shakeable). Est. -64 KB. Effort: M, Risk: low.
2. Duplicate react (18.2.0 + 18.3.1) .......... 44 KB [HIGH]
`npm ls react` shows two copies via an old @charting/core dep.
Add an override to pin a single version + dedupe. Est. -44 KB.
Effort: S, Risk: low.
3. recharts loaded eagerly in entry chunk ..... 38 KB [HIGH]
Only rendered below the fold on /dashboard. Move behind
next/dynamic({ ssr: false }). Est. -38 KB from First Load.
Effort: S, Risk: low.
4. lodash default import (whole library) ...... 24 KB [MED]
`import _ from "lodash"`. Switch to `lodash-es` + named imports
(debounce, groupBy). Est. -21 KB. Effort: S, Risk: low.
5. core-js polyfills for IE11 ................. 19 KB [MED]
browserslist still includes "ie 11". Drop it (no IE traffic in
analytics). Est. -19 KB. Effort: S, Risk: med (confirm targets).
6. server-only `pg` Pool pulled into client ... 12 KB [HIGH]
db/client.ts imported from a "use client" component. Move the
query behind a Server Action / route handler. Est. -12 KB +
removes a secret-leak vector. Effort: M, Risk: med.
Estimated total reduction: ~198 KB gzip (412 → ~214 KB).
Top 3 fixes alone recover 146 KB. Re-run the analyzer after each.
Re-run the build after applying the top findings to confirm the measured First Load JS dropped as projected, and re-check npm ls to verify the duplicate is gone.
1---2name: bundle-analyzer3description: Analyze a JS/TS production bundle and surface the biggest size wins — heavy dependencies, duplicate packages, missing code-splitting, oversized polyfills, and dev/server code leaking into the client. Use when a bundle is too large and you need a ranked, actionable reduction plan.4---56Inspect a JavaScript/TypeScript production bundle and find where the bytes actually go. The skill builds a stats report, attributes weight to specific modules and packages, and hunts for the patterns that bloat bundles in practice — a 200KB date library imported for one helper, two copies of the same package at different versions, a route that ships eagerly instead of lazily, a polyfill set targeting browsers you dropped years ago, or server-only code that slipped past the client boundary. It returns a ranked list of concrete reductions with the estimated savings of each, so you fix the 80KB win before the 4KB one.78## When to use this skill910- A production bundle (or a specific route/chunk) has grown past budget and you need to know exactly what to cut.11- You suspect duplicate packages, a heavyweight dependency, or a barrel import dragging in a whole library.12- A first-load or main chunk is too big and you want to know what should be code-split or deferred.13- You want to confirm dev-only tooling, source maps, or server code is not leaking into the client bundle.1415> [!NOTE]16> Always measure the **production** build, not dev. Dev bundles include HMR runtime, unminified source, and no tree-shaking, so their sizes are meaningless for this analysis. Compare **gzip/brotli** transfer sizes, not raw bytes — that is what users actually download.1718## Instructions19201. **Locate the build and detect the bundler.** Identify the toolchain before doing anything — do not guess. Check `package.json` scripts and lockfiles for `next`, `vite`, `webpack`, `rollup`, `esbuild`, or `@remix-run`. Note the package manager (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `bun.lockb`) since duplicate-detection commands differ per manager.212. **Produce a stats report using the project's own analyzer.** Match existing config rather than bolting on a new tool:22 - **Next.js** — run the production build and read its per-route First Load JS table; if `@next/bundle-analyzer` is wired up, run `ANALYZE=true npm run build`.23 - **Vite/Rollup** — use `rollup-plugin-visualizer` if present, or build and inspect `dist/assets/*` sizes.24 - **Webpack** — generate `--json` stats (`webpack --profile --json=stats.json`, which writes the file directly so console warnings don't corrupt the JSON) and analyze, e.g. with `source-map-explorer` over the emitted bundle + map.25 - If no analyzer is configured, fall back to `npx source-map-explorer 'dist/**/*.js'` against the built output and its source maps.263. **Attribute weight to packages, not just files.** Map the largest modules back to their npm packages. For each heavyweight dependency, determine whether it is fully used or pulled in by a barrel/side-effect import, and whether a lighter alternative exists (e.g. `date-fns`/`dayjs` over `moment`, native `Intl` over `numeral`, `lodash-es` with named imports over `lodash`).274. **Detect duplicates and version skew.** Run `npm ls <pkg>` / `pnpm why <pkg>` / `yarn why <pkg>` on suspect packages to find the same library bundled at multiple versions, and check for both ESM and CJS copies of the same dep. Flag candidates for `resolutions`/`overrides` or dedupe.285. **Find missing code-splitting and oversized polyfills.** Look for large modules in the entry/main chunk that are only needed on one route or behind an interaction (charts, editors, markdown renderers, PDF libs) — these belong behind `import()` / `next/dynamic` / `React.lazy`. Inspect the polyfill/transpile target (`browserslist`, `target` in `tsconfig`/`vite`/`tsup`) for `core-js` or regenerator-runtime bloat aimed at browsers you no longer support.296. **Hunt for leaked dev/server code.** Grep the client bundle and imports for things that should never ship: test/mock files, `process.env` debug branches, server-only modules (`fs`, `crypto` server usage, DB clients, secrets), and dev dependencies imported from app code. In Next.js, confirm Server Component / `"use client"` boundaries are not dragging server modules into client chunks.307. **Verify each proposed cut.** Do not estimate from intuition alone. Where feasible, apply the change behind the analyzer (or `--dry`) and re-run the build to measure the real delta. At minimum, cite the measured pre-change size from the stats report for every finding.318. **Report a ranked plan.** Output findings ordered by estimated gzip savings, each with: the module/package, current size, the specific fix, the expected reduction, and a rough effort/risk rating. Flag anything you could not measure precisely so the user knows what to confirm.3233> [!WARNING]34> Tree-shaking only works on side-effect-free ESM. A default or namespace import from a CJS package (or a package missing `"sideEffects": false`) pulls in the **whole** module regardless of what you use — so "import one helper" can still cost the full library. Verify the import shape, not just the import statement.3536## Examples3738A ranked findings report for a Next.js app whose largest route shipped 412 KB of First Load JS:3940```text41Bundle analysis — route /dashboard (First Load JS: 412 KB gzip → target 180 KB)42Ranked by estimated gzip savings:43441. moment + moment-timezone .................. 71 KB [HIGH]45 Imported in 3 files for formatting only. Replace with date-fns46 named imports (tree-shakeable). Est. -64 KB. Effort: M, Risk: low.47482. Duplicate react (18.2.0 + 18.3.1) .......... 44 KB [HIGH]49 `npm ls react` shows two copies via an old @charting/core dep.50 Add an override to pin a single version + dedupe. Est. -44 KB.51 Effort: S, Risk: low.52533. recharts loaded eagerly in entry chunk ..... 38 KB [HIGH]54 Only rendered below the fold on /dashboard. Move behind55 next/dynamic({ ssr: false }). Est. -38 KB from First Load.56 Effort: S, Risk: low.57584. lodash default import (whole library) ...... 24 KB [MED]59 `import _ from "lodash"`. Switch to `lodash-es` + named imports60 (debounce, groupBy). Est. -21 KB. Effort: S, Risk: low.61625. core-js polyfills for IE11 ................. 19 KB [MED]63 browserslist still includes "ie 11". Drop it (no IE traffic in64 analytics). Est. -19 KB. Effort: S, Risk: med (confirm targets).65666. server-only `pg` Pool pulled into client ... 12 KB [HIGH]67 db/client.ts imported from a "use client" component. Move the68 query behind a Server Action / route handler. Est. -12 KB +69 removes a secret-leak vector. Effort: M, Risk: med.7071Estimated total reduction: ~198 KB gzip (412 → ~214 KB).72Top 3 fixes alone recover 146 KB. Re-run the analyzer after each.73```7475Re-run the build after applying the top findings to confirm the measured First Load JS dropped as projected, and re-check `npm ls` to verify the duplicate is gone.