gin by capturing the baseline payload and generating a visual dependency map.
// baseline.ts
import { execSync } from 'child_process';
const baselineCommand = 'npx vite build --mode production';
const output = execSync(baselineCommand, { encoding: 'utf-8' });
// Extract the "First Load JS" or "dist/" summary
const match = output.match(/First Load JS:\s*([\d.]+)\s*KB/);
if (match) {
console.log(`Baseline payload: ${match[1]} KB`);
}
Run the build command for your framework. Record the shared initial payload and the largest individual route. Next, inject a visualization plugin to map module weights.
// vite.config.ts
import { defineConfig } from 'vite';
import visualizer from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
],
});
The treemap output becomes your source of truth. Large rectangles indicate either heavy third-party packages, unshaken utility libraries, or missing code-split boundaries. Every unexplained block is a candidate for removal or isolation.
Phase 2: Dependency Pruning
Heavy utility and formatting libraries are the most common sources of silent bloat. They are often imported as default namespaces, which bypasses tree-shaking entirely.
Date Formatting Replacement
Legacy date libraries bundle timezone databases, locale packs, and parsing engines that most applications never invoke.
// Before: Full library import
import { parse, format as fmt } from 'date-fns';
const raw = '2026-03-15T14:30:00Z';
const parsed = parse(raw, 'yyyy-MM-dd\'T\'HH:mm:ssXXX', new Date());
const display = fmt(parsed, 'PPP');
// After: Native Intl API with zero runtime cost
const display = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date(raw));
The native Intl constructor handles formatting, localization, and timezone normalization without adding kilobytes to the bundle. Reserve dedicated date libraries only for complex calendar math or relative time calculations.
Utility Library Replacement
Monolithic utility suites ship hundreds of functions. Most applications use fewer than a dozen.
// Before: Namespace import
import _ from 'lodash';
const aggregated = _.groupBy(records, 'department');
const distinct = _.uniq(userIds);
// After: Native static methods
const aggregated = Object.groupBy(records, (record) => record.department);
const distinct = Array.from(new Set(userIds));
Object.groupBy and Array.from(new Set()) are standardized, widely supported, and require zero external dependencies. If you must retain a utility library, switch to the ES module variant and import functions individually to guarantee tree-shaking.
Icon Library Isolation
Barrel exports in icon packages often re-export entire SVG collections, defeating modular bundling.
// Before: Barrel import
import { Dashboard, Settings, Profile } from '@company/ui-icons';
// After: Direct path resolution
import Dashboard from '@company/ui-icons/dist/esm/Dashboard.js';
import Settings from '@company/ui-icons/dist/esm/Settings.js';
import Profile from '@company/ui-icons/dist/esm/Profile.js';
Verify the treemap after migration. If the icon package still appears as a single large block, your bundler is not resolving the ES module entry point. Configure resolve.mainFields to prioritize module and browser over main.
Phase 3: Runtime & Asset Optimization
Polyfill Target Alignment
Transpilation pipelines inject polyfills based on browser targets. Overly broad targets compile modern syntax into legacy equivalents, inflating the runtime.
// package.json
{
"browserslist": [
"last 2 chrome versions",
"last 2 firefox versions",
"last 2 safari versions",
"last 2 edge versions"
]
}
Narrowing the target eliminates core-js shims, regenerator-runtime wrappers, and @babel/runtime helpers. The tradeoff is explicit: if your user base includes enterprise environments locked to legacy browsers, maintain a separate build target. Otherwise, modern defaults are sufficient.
Route-Level Code Splitting
Eager imports force the bundler to include every component in the initial payload, regardless of navigation state.
// Before: Static import
import AnalyticsPanel from './components/AnalyticsPanel';
// After: Dynamic import with suspense boundary
import { lazy, Suspense } from 'react';
const AnalyticsPanel = lazy(() => import('./components/AnalyticsPanel'));
export function MainLayout() {
return (
<Suspense fallback={<div className="skeleton" />}>
<AnalyticsPanel />
</Suspense>
);
}
Framework routers handle route-level splitting automatically. Focus on component-level isolation for heavy third-party integrations: charting engines, rich text editors, payment SDKs, and media players. Each should be lazy-loaded behind a user interaction or route transition.
Modern Image Delivery
Unoptimized raster images dominate transfer weight. Serve next-generation formats with explicit fallback chains.
<picture>
<source srcset="/assets/hero.avif" type="image/avif" />
<source srcset="/assets/hero.webp" type="image/webp" />
<img
src="/assets/hero.jpg"
alt="Dashboard overview"
width="1200"
height="630"
loading="lazy"
decoding="async"
/>
</picture>
AVIF reduces file size by 30β50 percent compared to JPEG at equivalent quality. Explicit width and height attributes reserve layout space, preventing cumulative layout shift. loading="lazy" defers off-screen resources, and decoding="async" moves bitmap decoding off the main thread.
Phase 4: Re-Baseline & Validation
Execute the build command again. Compare the new initial payload against the baseline. A successful audit should reduce the shared JavaScript bundle by at least 50 percent. Validate the improvement using performance profiling tools on a throttled network connection. If the payload remains elevated, return to the treemap and identify residual heavy modules. Transitive dependencies often hide in plain sight.
Pitfall Guide
1. Blind Tree-Shaking Reliance
Explanation: Developers assume modern bundlers automatically remove unused exports. In reality, tree-shaking only works for statically analyzable ES modules. CommonJS packages, side-effect-heavy libraries, and barrel exports bypass the optimization.
Fix: Audit package.json for "sideEffects": false declarations. Prefer ES module distributions. Verify removal in the treemap; never assume.
2. Over-Lazy Loading Critical UI
Explanation: Applying lazy() to above-the-fold components introduces a network waterfall. The browser must fetch the initial HTML, parse it, request the lazy chunk, and then render. This degrades First Contentful Paint.
Fix: Reserve dynamic imports for below-the-fold content, secondary routes, or interaction-triggered panels. Keep layout primitives and navigation shells in the initial bundle.
3. Polyfill Target Misalignment
Explanation: Leaving default browser targets like "> 0.5%, last 2 versions, not dead" forces the transpiler to generate shims for obsolete engines. The polyfill chain compounds with every modern syntax feature.
Fix: Explicitly define modern targets in browserslist. Use npx browserslist to verify the resolved engine list. Remove legacy flags unless contractually required.
4. Icon Library Barrel Imports
Explanation: Importing from a package root often triggers a re-export of the entire collection. The bundler cannot prune what it cannot statically trace.
Fix: Import directly from the package's internal ES module paths. Configure the bundler to resolve module fields first. Validate with a treemap snapshot.
5. CI Budget Bypasses
Explanation: Teams set bundle size limits but allow developers to override failures with --no-verify or by merging without checking pipeline status. The constraint becomes decorative.
Fix: Enforce budget checks as required status checks in the repository settings. Block merges automatically. Require explicit justification and size delta documentation for any threshold increase.
Explanation: Serving only AVIF or WebP breaks rendering on older browsers or email clients. Missing fallbacks cause broken images or forced re-downloads.
Fix: Always include a progressive fallback chain: AVIF β WebP β JPEG/PNG. Use explicit type attributes. Test rendering across target browser matrices.
7. Ignoring Transitive Dependency Bloat
Explanation: A single lightweight package may pull in heavy utilities, polyfills, or legacy runtimes as transitive dependencies. The treemap shows the leaf package, but the weight originates upstream.
Fix: Run npm ls or yarn why to trace dependency trees. Use resolutions or overrides in package.json to force lighter alternatives. Audit node_modules size periodically.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Enterprise app with IE11 support | Maintain legacy browserslist + separate modern build | Contractual compliance requires polyfill chain | +40β80 KB payload, higher CDN egress |
| Marketing site with heavy imagery | Prioritize image optimization + lazy loading | Media accounts for 60β70% of transfer weight | -1β2 MB page weight, faster LCP |
| Internal dashboard with complex charts | Route-level splitting + dynamic chart SDK import | Charts are interaction-heavy and rarely visible initially | -150β400 KB initial bundle |
| SaaS product with frequent feature updates | CI bundle budget + PR diff analysis | Prevents regression drift during rapid iteration | Zero infra cost, prevents performance debt |
Configuration Template
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import visualizer from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
visualizer({
filename: 'dist/bundle-report.html',
open: false,
gzipSize: true,
brotliSize: true,
template: 'treemap',
}),
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
ui: ['@company/design-system'],
},
},
},
target: 'es2022',
cssCodeSplit: true,
},
resolve: {
mainFields: ['module', 'browser', 'main'],
},
});
// package.json
{
"browserslist": [
"last 2 chrome versions",
"last 2 firefox versions",
"last 2 safari versions",
"last 2 edge versions"
],
"scripts": {
"build:prod": "vite build",
"analyze": "ANALYZE=true vite build",
"check:size": "node scripts/verify-bundle-size.js"
}
}
Quick Start Guide
- Initialize measurement: Run
npm run build:prod and record the initial JavaScript payload from the terminal output.
- Inject visualization: Add the
rollup-plugin-visualizer configuration to your build config and execute npm run analyze. Open the generated HTML treemap.
- Execute dependency pruning: Replace heavy date, utility, and icon imports with native equivalents or direct path resolutions. Commit and rebuild.
- Validate reduction: Compare the new payload against the baseline. If the reduction exceeds 30 percent, proceed to code-splitting and image optimization.
- Enforce constraints: Add the bundle verification script to your CI pipeline. Set a hard threshold and block merges on regression.