| name | build-optimization |
| description | Slow builds, large bundle sizes, CI pipelines taking more than 5 minutes, or when the user mentions Webpack, Vite, Turbo, esbuild, or Rollup perfor... |
Build Optimization
When to activate
Slow builds, large bundle sizes, CI pipelines taking more than 5 minutes, or when the user mentions Webpack, Vite, Turbo, esbuild, or Rollup performance issues.
When NOT to use
- Runtime performance problems (CPU, memory, network) — this skill targets compile/bundle time only
- First-time project setup where no baseline exists
- Projects using bundlers other than the ones listed above (e.g., Parcel, Brunch)
Instructions
Always start with analysis, never with changes.
Bundle Analysis (run first)
- Webpack:
npx webpack-bundle-analyzer stats.json — generate stats with webpack --profile --json > stats.json
- Vite:
npx vite-bundle-visualizer after adding { build: { reportCompressedSize: true } }
- Identify: largest chunks, duplicated dependencies, unexpectedly included modules
Code Splitting
- Dynamic imports:
const Foo = () => import('./Foo') for route-level and feature-level splits
- Route-based splitting in React Router / Next.js:
React.lazy + Suspense
- Vendor chunk isolation: separate rarely-changing third-party code from app code
- Vite
manualChunks:
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-tooltip'],
},
},
},
}
- Webpack
SplitChunksPlugin:
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: { test: /node_modules/, name: 'vendors', chunks: 'all' },
},
},
}
Tree Shaking
- Requires ES module syntax (
import/export) throughout — CommonJS (require) disables tree shaking
- Add
"sideEffects": false to package.json (or list CSS files that have side effects)
- Audit barrel files (
index.ts) — re-exporting everything defeats tree shaking; use direct imports
TypeScript Incremental Compilation
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}
Add .tsbuildinfo to .gitignore, cache it in CI keyed on source hash.
Turborepo Task Caching
{
"pipeline": {
"build": {
"outputs": ["dist/**", ".next/**"],
"inputs": ["src/**", "package.json", "tsconfig.json"]
}
}
}
Remote caching: npx turbo link — shares cache hits across team members and CI.
CI Cache Strategy (GitHub Actions pattern)
- uses: actions/cache@v3
with:
path: node_modules
key: node-${{ hashFiles('**/package-lock.json') }}
- uses: actions/cache@v3
with:
path: dist
key: build-${{ hashFiles('src/**') }}
Target: >90% cache hit rate. A miss on node_modules should not invalidate the build output cache.
Common Quick Wins (no architectural change required)
- Add
.dockerignore mirroring .gitignore — prevents sending node_modules into build context
- Enable
vite.optimizeDeps.include for large CJS deps that Vite pre-bundles slowly
- Replace
ts-node with tsx for scripts — tsx uses esbuild and is ~10× faster for one-off execution
- Switch
jest to vitest for TypeScript projects — eliminates Babel transform overhead
- Enable
esbuild as the TypeScript transformer in Webpack via esbuild-loader
Example
Symptom: CI build takes 8 minutes, bundle is 4 MB gzipped.
Steps taken:
- Run
npx vite-bundle-visualizer — reveals moment.js (300 KB) and all locales included
- Replace
moment with date-fns tree-shaken imports — saves 280 KB
- Add
manualChunks to split vendor from app code — reduces first-load chunk from 1.2 MB to 380 KB
- Add Turborepo with
outputs: ["dist/**"] — second CI run hits cache, build time drops to 45 seconds