bundle-size-review
Find what makes a JavaScript bundle heavy and what can safely come out, from the bundler's stats file (webpack stats.json, an esbuild metafile, or the rollup-plugin-visualizer raw-data JSON that Vite builds use) read together with the repo around it (package.json, .browserslistrc, the lockfile, the bundler config and the source files that import the heavy modules). Gives each entry's first-load JavaScript in the exact bytes the stats record, kept apart from lazy chunks; finds whole-library imports, CommonJS packages that can't be tree-shaken, packages bundled twice and polyfills the browser targets don't need; names the import chain that pulls each module in before suggesting a removal; and, given before and after stats, says what grew, why, whether it lands in the first load and which per-entry budget would have caught it. Use when the user says "why is our bundle so big", "what can we cut from the bundle", "review this stats.json", "why is lodash or moment in our bundle", "the bundle grew in this PR", or "set a bundle size budget".
Other ways to install
With the sibling skills it hands off to:
The whole pack it belongs to:
By hand, from a clone of the repo:
In Claude Code, with the frontend plugin installed, invoke it as:
Try it
A realistic prompt from the skill's acceptance evals:
The dispatch dashboard is slow to open on our technicians' phones, and the bundle analyzer treemap shows echarts as the giant block. I ran webpack with --json and attached stats.json, plus package.json and src/utils/format.js. What's making the bundle heavy, and what should we cut first?
What a good result looks like
Example output
Worked example: a chart library moves into the first load while the total stays flat · the skill's own worked example, source
Input
PR #241 on larkspur-dispatch-web adds a small jobs-per-hour sparkline to the dashboard header. Our CI checks that total JS stays under 850 KB and it passed. A reviewer says the dashboard feels slower to open. Before and after webpack stats are attached (
inputs/stats-before.json,inputs/stats-after.json). Did this PR make the first load heavier?
What the script printed (abridged)
python3 scripts/bundle_stats.py --compare examples/inputs/stats-before.json examples/inputs/stats-after.json --budget main=330kB
Chunk sizes: emitted asset bytes (after minification, before compression).
Module and package sizes: webpack module size, before minification.
Compressed sizes: not measured; no built files were read (pass --dist DIR after a build).
All JS chunks: 812,606 B (812.6 kB) -> 810,657 B (810.7 kB) (-1,949 B); initial +492,392 B, lazy -494,341 B
Initial JS per entry
main: 311,282 B (311.3 kB) -> 803,674 B (803.7 kB) (+492,392 B)
Chunks that changed
reports lazy 498,212 B -> 3,871 B (-494,341 B) changed; loaded with import() from src/routes.js
vendors initial 301,412 B -> 792,840 B (+491,428 B) changed
main initial 9,870 B -> 10,834 B (+964 B) changed
Packages that changed (sorted by change in initial size)
echarts total 503,088 B -> 503,088 B (0 B); initial 0 B -> 503,088 B (+503,088 B) changed
chain: src/index.js > src/App.jsx > src/components/JobsSparkline.jsx > node_modules/echarts/index.js
zrender total 167,660 B -> 167,660 B (0 B); initial 0 B -> 167,660 B (+167,660 B) changed
moved from lazy to initial: echarts, zrender
Project files (root: examples/inputs)
found: (none)
absent: package.json, .browserslistrc, bundler config (webpack/vite/rollup/esbuild config, build.mjs), lockfile
bundled source files not on disk here (9): src/App.jsx, ..., src/components/JobsSparkline.jsx, ...
Budgets on each entry's initial JS, before -> after
main: budget 330,000 B (raw); before 311,282 B (PASS), after 803,674 B (OVER)
Reply
Yes, the reviewer is right: the first load went from 311,282 to 803,674 bytes of JavaScript (+492,392), while the total fell by 1,949 bytes, which is why the 850 KB total check passed. The new JobsSparkline.jsx imports echarts directly, and that pulled echarts and zrender out of the lazy reports chunk (498,212 → 3,871 bytes) into vendors, which every page loads before the first screen. Sizes are webpack's emitted files, minified and before compression; no built files were attached, so there is no compressed figure here.
- Load the sparkline's chart code on demand. Chain:
src/index.js→App.jsx→components/JobsSparkline.jsx→echarts. Render the header without the sparkline, thenimport("echarts/core")inside the component and draw once it resolves. Check the reports page still opens its charts, and that the header doesn't jump when the sparkline appears (reserve its height). - Register only the line chart. The bare
echartsimport brings every chart type and component; the moved modules include geo, dataZoom and bar views that a line sparkline never draws. Switch toecharts/corewithuse([LineChart, GridComponent, CanvasRenderer])plus what the reports page draws; open every report chart afterwards, since an unregistered type fails at runtime. Confirm the installed echarts is 5.x first (nopackage.jsonwas attached), sinceecharts/corearrived in 5.
Guard: a per-entry limit on main's initial JavaScript would have failed this PR: webpack performance: { hints: "error", maxEntrypointSize: 330000 } passes today's 311,282 bytes and fails 803,674. The total-size limit can't see a lazy-to-initial move; keep it, but add the per-entry one.
To confirm: whether the sparkline has to show on first paint. If it does, a plain SVG path drawn from the hourly counts needs no chart library at all.
How it's checked
Measured against plain Claude Code (2026-10-23): it met 53% of its checks, against 40% without it, and a blind judge preferred it. How skills are checked
73 assertions across 9 acceptance evals, and 10 trigger queries. Every skill's checks
What the output must do
When it should and shouldn't activate
Should activate:
- why is our webpack bundle so big? stats.json attached
- what can we cut from the Vite bundle, I've got the visualizer raw-data json
- this PR made main.js grow by 80 KB, here's the esbuild metafile from before and after, what grew?
- why is moment in our bundle when nothing imports it?
- we have two versions of date-fns in the bundle, which dependency pulls in the second one?
- set a bundle size budget per entry that CI can check
Should leave to another skill:
- here's our Lighthouse report, why is LCP 4.8 seconds on mobile?
- Dependabot wants to bump webpack from 5.88 to 5.94, is the PR safe to merge?
- review my PR that adds the CSV export button before I merge it
- should the product pages be server-rendered or statically generated?
Hands off to
- web-performance-review — when the user's real question is why the page is slow for users, with a Lighthouse or PageSpeed report in hand. Rank fixes by how much each moves LCP, INP and CLS, of which JavaScript weight is only one cause.
Related skills
dependency-upgrade-review, pre-merge-review, rendering-strategy
Source
SKILL.md · the whole folder on GitHub.