Bundle Size Reduction skill for Codex
Find the kilobytes that matter and cut them. Codex builds a treemap from the project's actual bundler, ranks the largest modules in the initial chunk by gzip size, and fixes the cause behind each: a whole library imported for one helper, duplicate versions, barrel files that defeat tree shaking, needless polyfills or server code leaking to the client. Every change is measured before and after, and a size-limit check keeps the win.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- bundle-size-reduction Installs to ~/.agents/skills/bundle-size-reduction/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install bundle-size-reduction The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project bundle-size-reduction Installs to .agents/skills/bundle-size-reduction/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/bundle-size-reduction.zipunzip -o bundle-size-reduction.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) bundle-size-reduction PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $bundle-size-reduction, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Measures and shrinks JavaScript bundles - generates stats with rollup-plugin-visualizer, webpack-bundle-analyzer, esbuild metafiles, source-map-explorer or the Next.js analyzer, then removes the bytes that matter - heavy or duplicated dependencies, barrel-file imports that defeat tree shaking, unnecessary polyfills and code that belongs in another chunk. Use when the main bundle is too big, TBT or LCP suffers from JS, a dependency upgrade bloated the build, or someone asks what is in the bundle and how to make it smaller.
Use it when
- The initial JavaScript for a key route is large or growing
- TBT or LCP is dominated by script download, parse or execution
- You need to know which dependency or import pulled in a large module
Use something else when
- The code is needed but not on first load and you need lazy loading patterns: use code-splitting
- The weight comes from tag managers and vendor scripts: use third-party-scripts
How to check the result
Rebuild in production mode and compare gzip and parsed sizes of the target route's initial chunks, then smoke the affected feature in a production preview.
The mistake it prevents
Marking a package sideEffects false when it imports CSS for effect, which silently drops styles from the build.
Done well means: you know which modules make up the bytes on the critical route (compressed, not raw), each removal is proven with a before/after build stat, the app still works, and a size check stops the bundle from growing back.
Measure the right thing
- Compressed (gzip or brotli) transfer size drives download time; parsed size drives parse and compile cost on slow CPUs. Report both.
- The initial JS of the most important route matters more than the total of all chunks.
- Always measure production builds. Dev builds include HMR and are unminified.
Workflow
- [ ] 1. Read AGENTS.md and package.json: bundler, framework, package manager, browserslist
- [ ] 2. Production build and record baseline sizes per entry/route (gzip + parsed)
- [ ] 3. Generate a treemap and list the 10 largest modules in initial chunks
- [ ] 4. Classify each (table below) and pick the cheapest fix with the biggest win
- [ ] 5. One change at a time; rebuild; record delta; run tests and smoke the app
- [ ] 6. Add or tighten a size limit in CI
Get a treemap
| Bundler | Command / setup |
|---|---|
| Vite (Rollup) | rollup-plugin-visualizer in build.rollupOptions.plugins or plugins, template: 'treemap', gzipSize: true, brotliSize: true |
| webpack | npx webpack --profile --json > stats.json then npx webpack-bundle-analyzer stats.json |
| Next.js (webpack build) | @next/bundle-analyzer, run ANALYZE=true next build |
| Next.js 16.1+ (Turbopack) | npx next experimental-analyze (experimental, interactive) |
| esbuild | --metafile=meta.json, then the esbuild bundle analyzer page or esbuild.analyzeMetafile |
| Any, with source maps | npx source-map-explorer 'dist/assets/*.js' |
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [visualizer({ filename: 'stats.html', template: 'treemap', gzipSize: true, brotliSize: true })],
});
Installing analyzers needs network; add them as dev dependencies with the project’s package manager, or ask before installing in a sandbox without network.
Classify and fix
| Finding in the treemap | Fix |
|---|---|
Whole library for one function (lodash, moment, icon packs) | Import the specific path or switch: lodash-es per-function, date-fns or Intl/Temporal where available, per-icon imports |
| Same package twice at different versions | npm ls <pkg> / pnpm why <pkg>; align versions, use overrides/resolutions only after checking compatibility |
Barrel file (index.ts re-exporting everything) pulls in unused modules | Import from the file directly; mark the package "sideEffects": false if true; Next.js experimental.optimizePackageImports for libraries |
| CommonJS dependency (no tree shaking) | Find an ESM build or alternative; CJS modules are included whole |
| Polyfills for browsers you do not support | Fix browserslist; drop core-js full imports; Lighthouse legacy-javascript-insight lists them |
| Feature used on one route or after interaction (editor, charts, maps, PDF) | Lazy-load it: $code-splitting |
| Large JSON, locale data, SVG strings inlined in JS | Fetch at runtime, load one locale, serve SVG as files |
| Server-only code in the client bundle (SDKs, validation schemas used only on server) | Move behind a server boundary; in Next.js add import 'server-only' to catch it |
Dev-only code shipped (console, debug panels, mocks) | Guard with import.meta.env.DEV / process.env.NODE_ENV so minifiers drop it |
Check a replacement’s real cost before switching: bundle it alone (esbuild --bundle --minify entry.js | gzip -c | wc -c) rather than trusting a README claim.
Tree shaking checklist
- Package exposes ESM (
"module"or"exports"withimportcondition). - No top-level side effects, or
"sideEffects"lists the files that do (CSS imports!). Setting"sideEffects": falseon a package that imports CSS for effect drops the CSS. - Named imports, not
import * as libused dynamically. - Babel or TS not converting ESM to CJS before the bundler (
"module": "commonjs"in a frontend tsconfig, or@babel/preset-envwithmodules: 'commonjs').
Verify a change
# Compare gzip sizes of built JS before and after (run from repo root after each build)
find dist -name '*.js' -exec sh -c 'printf "%8d %s\n" "$(gzip -9c "$1" | wc -c)" "$1"' _ {} \; | sort -rn | head -20
- Delta measured on initial JS for the target route, gzip and parsed.
- Tests pass; the affected feature works in a production preview (
vite preview,next start). Removing a “side effect” import can silently drop CSS or polyfills. - If the size did not drop, the module is still reachable: search the stats file for who imports it (webpack-bundle-analyzer and the Next analyzer show import reasons).
Keep it small
Add a limit that fails CI. Default to size-limit (.size-limit.json, plus the
@size-limit/file preset as a dev dependency):
[
{ "name": "home initial JS", "path": "dist/assets/index-*.js", "limit": "120 kB", "gzip": true }
]
npx size-limit # fails non-zero when over the limit
Set the limit a few percent above today’s size and lower it after each win. Cross-cutting
budgets (Lighthouse, API latency) belong in $performance-budgets.
Gotchas
- Treemaps of parsed size exaggerate highly compressible code (JSON, repeated strings); sort by gzip before deciding.
manualChunksthat groups all ofnode_modulesinto one vendor chunk makes every route download every dependency. Split by usage, not by origin.- Replacing a library saves nothing if another dependency still imports it.
- Upgrading a framework can double-ship two versions of React or a UI kit during the transition; check for duplicates after every major upgrade.
- Minifier settings (
drop_console,pure_funcs) only remove code without side effects; verify the output rather than assuming.
Report template
## Bundle size: <app> (<bundler>, production build)
| Entry / route | Before gzip | After gzip | Parsed after |
|---|---|---|---|
| home initial | 312 kB | 186 kB | 610 kB |
Changes:
1. Replaced moment with date-fns per-function imports: -71 kB gzip (src/utils/date.ts)
2. Lazy-loaded the chart editor on /reports: -48 kB from home initial
Guard: size-limit entry "home initial JS" at 195 kB
Not done: <duplicate react-is via library X, needs upstream fix> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Bundle Size Reduction"
short_description: "Find the kilobytes that matter and cut them"
default_prompt: "$bundle-size-reduction Our main.js is 900 kB, show me what is in it and cut it down" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.