JS Code Splitting: Advanced Performance Guide
Master advanced code splitting in JavaScript. Learn dynamic imports, entry point splitting, vendor chunking, prefetch strategies, granular chunk optimization, shared module deduplication, and real-world bundle analysis techniques.
Code splitting breaks your JavaScript bundle into smaller chunks loaded on demand, dramatically reducing initial load time. This guide covers advanced patterns from dynamic imports to sophisticated chunking strategies.
For route-level splitting patterns, see Implementing Route-Level Code Splitting in JS.
Why Code Splitting Matters
// WITHOUT code splitting: one massive bundle
// main.js -> 2.4MB (everything loaded upfront)
// WITH code splitting: small initial bundle + on-demand chunks
// main.js -> 120KB (critical path only)
// chart.js -> 340KB (loaded when user opens dashboard)
// editor.js -> 520KB (loaded when user opens editor)
// admin.js -> 280KB (loaded only for admin users)| Metric | Before Splitting | After Splitting | Improvement |
|---|---|---|---|
| Initial Bundle | 2.4MB | 120KB | 95% smaller |
| Time to Interactive | 8.2s | 1.8s | 78% faster |
| First Contentful Paint | 4.1s | 0.9s | 78% faster |
| Largest Contentful Paint | 6.5s | 2.1s | 68% faster |
Dynamic Import Basics
// Static import: always included in bundle
import { heavyChart } from "./chart-library";
// Dynamic import: loaded on demand, returns a Promise
async function showChart(data) {
const { heavyChart } = await import("./chart-library");
heavyChart.render(data);
}
// With loading state
async function loadFeature(featureName) {
const statusEl = document.getElementById("status");
statusEl.textContent = "Loading...";
try {
const module = await import(`./features/${featureName}.js`);
module.init();
statusEl.textContent = "";
} catch (error) {
statusEl.textContent = "Failed to load feature";
console.error("Dynamic import failed:", error);
}
}
// Named exports vs default export
async function loadUtils() {
// Named exports
const { debounce, throttle } = await import("./utils");
// Default export
const module = await import("./validator");
const Validator = module.default;
return { debounce, throttle, Validator };
}Webpack Chunk Configuration
// webpack.config.js
module.exports = {
entry: {
main: "./src/index.js",
admin: "./src/admin.js",
},
optimization: {
splitChunks: {
chunks: "all",
maxInitialRequests: 25,
maxAsyncRequests: 25,
minSize: 20000,
cacheGroups: {
// Vendor splitting: separate node_modules
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return `vendor.${packageName.replace("@", "")}`;
},
priority: 10,
},
// Framework chunk: React, ReactDOM
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
name: "framework",
priority: 20,
chunks: "all",
},
// Shared utilities across entry points
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
name: "common",
},
},
},
},
};Prefetch and Preload Strategies
// Webpack magic comments for prefetch/preload
// Prefetch: load during browser idle time (for future navigation)
function setupDashboard() {
const dashBtn = document.getElementById("open-dashboard");
dashBtn.addEventListener("click", async () => {
const { Dashboard } = await import(
/* webpackPrefetch: true */
"./dashboard/Dashboard"
);
new Dashboard().mount("#content");
});
}
// Preload: load immediately in parallel (needed soon)
async function renderPage() {
const { Header } = await import(
/* webpackPreload: true */
"./components/Header"
);
Header.render();
}
// Programmatic prefetch based on user behavior
class SmartPrefetcher {
constructor() {
this.prefetched = new Set();
this.observer = null;
}
prefetchOnHover(selector, chunkLoader) {
document.querySelectorAll(selector).forEach((el) => {
el.addEventListener(
"mouseenter",
() => {
const key = el.dataset.chunk;
if (!this.prefetched.has(key)) {
this.prefetched.add(key);
chunkLoader(key);
}
},
{ once: true }
);
});
}
prefetchOnVisible(selector, chunkLoader) {
this.observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const key = entry.target.dataset.chunk;
if (!this.prefetched.has(key)) {
this.prefetched.add(key);
chunkLoader(key);
this.observer.unobserve(entry.target);
}
}
});
});
document.querySelectorAll(selector).forEach((el) => {
this.observer.observe(el);
});
}
}
// Usage
const prefetcher = new SmartPrefetcher();
const loaders = {
chart: () => import("./features/chart"),
editor: () => import("./features/editor"),
settings: () => import("./features/settings"),
};
prefetcher.prefetchOnHover("[data-chunk]", (key) => loaders[key]?.());Module Federation for Micro-Frontends
// webpack.config.js - Host Application
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: "host",
remotes: {
dashboard: "dashboard@https://cdn.example.com/dashboard/remoteEntry.js",
settings: "settings@https://cdn.example.com/settings/remoteEntry.js",
},
shared: {
react: { singleton: true, requiredVersion: "^18.0.0" },
"react-dom": { singleton: true, requiredVersion: "^18.0.0" },
},
}),
],
};
// Loading remote modules dynamically
async function loadRemoteModule(scope, module) {
await __webpack_init_sharing__("default");
const container = window[scope];
await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
return factory();
}
// Usage with error boundaries
async function loadDashboard() {
try {
const { DashboardWidget } = await import("dashboard/Widget");
return DashboardWidget;
} catch (error) {
console.error("Remote module failed to load:", error);
const { FallbackWidget } = await import("./fallbacks/DashboardFallback");
return FallbackWidget;
}
}Bundle Analysis and Optimization
// Custom chunk analysis utility
class BundleAnalyzer {
constructor() {
this.chunks = new Map();
this.loadTimes = new Map();
}
trackChunkLoad(chunkName, loader) {
return async (...args) => {
const start = performance.now();
try {
const module = await loader(...args);
const loadTime = performance.now() - start;
this.loadTimes.set(chunkName, [
...(this.loadTimes.get(chunkName) || []),
loadTime,
]);
this.chunks.set(chunkName, {
loaded: true,
lastLoadTime: loadTime,
loadCount: (this.chunks.get(chunkName)?.loadCount || 0) + 1,
});
return module;
} catch (error) {
this.chunks.set(chunkName, {
loaded: false,
error: error.message,
failCount: (this.chunks.get(chunkName)?.failCount || 0) + 1,
});
throw error;
}
};
}
getReport() {
const report = {};
for (const [name, times] of this.loadTimes) {
const sorted = [...times].sort((a, b) => a - b);
report[name] = {
loads: times.length,
avgMs: (times.reduce((a, b) => a + b, 0) / times.length).toFixed(2),
medianMs: sorted[Math.floor(sorted.length / 2)].toFixed(2),
p95Ms: sorted[Math.floor(sorted.length * 0.95)].toFixed(2),
};
}
return report;
}
}
const analyzer = new BundleAnalyzer();
// Wrap dynamic imports with tracking
const loadChart = analyzer.trackChunkLoad("chart", () =>
import("./features/chart")
);
const loadEditor = analyzer.trackChunkLoad("editor", () =>
import("./features/editor")
);| Strategy | Use Case | Benefit | Trade-off |
|---|---|---|---|
| Entry point splitting | Multi-page apps | Smallest initial per page | Build config complexity |
| Vendor chunking | All apps | Long-term caching | Extra HTTP requests |
| Route splitting | SPAs | Fast initial load | Navigation delay |
| Component splitting | Large features | On-demand loading | Loading states needed |
| Prefetch on hover | Navigation links | Perceived instant load | Wasted bandwidth sometimes |
| Module federation | Micro-frontends | Independent deployments | Runtime dependency risks |
Rune AI
Key Insights
- Dynamic imports are the foundation: Use
import()to load modules on demand, returning Promises that resolve to the module namespace object - Vendor chunking enables long-term caching: Separate node_modules into their own chunks so application code changes do not invalidate vendor cache
- Prefetch on hover eliminates perceived delays: Load chunks when users hover over navigation links, giving 200-400ms head start before the click
- Analyze before optimizing: Use bundle analysis tools to identify the largest modules and most impactful splitting opportunities before configuring chunk strategies
- Shared modules prevent duplication: Configure splitChunks cacheGroups with minChunks to extract modules used across multiple entry points into a shared chunk
Frequently Asked Questions
When should I split a chunk?
How do I handle loading failures for dynamic imports?
What is the difference between prefetch and preload?
How do I prevent duplicate modules across chunks?
Does code splitting affect SEO?
Conclusion
Code splitting transforms monolithic bundles into optimized chunks loaded on demand. Dynamic imports provide the foundation, while webpack's splitChunks configuration enables vendor separation, shared module deduplication, and granular chunking. Smart prefetching based on hover and viewport visibility eliminates perceived loading delays. For route-level patterns, see Implementing Route-Level Code Splitting in JS. For lazy loading techniques, see Lazy Loading in JavaScript: Complete Tutorial.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.