Bundling combines multiple JavaScript modules into optimized files for production, while code splitting breaks them apart strategically so users only download what they need for the current page.
Removes whitespace, comments, and shortens variable names, typically reducing bundle size by 30-50% without changing behavior.
Dead code elimination for ES modules that removes unused exports. Requires static import/export syntax and sideEffects configuration.
Route-based (per page), component-based (React.lazy), and dynamic imports (import()) each target different loading scenarios.
Tools like webpack-bundle-analyzer and source-map-explorer visualize which dependencies consume the most space in your output.
Separating third-party libraries into their own chunk maximizes browser cache efficiency across deployments.
Modern web applications are built from hundreds or thousands of JavaScript modules. Without bundling, the browser would need to make a separate HTTP request for each module, creating massive overhead. Bundlers like Webpack, Vite (using Rollup/Rolldown), and esbuild solve this by combining modules into a smaller number of optimized output files called bundles or chunks.
Minification removes whitespace, comments, and shortens variable names without changing behavior. Tools like Terser (for Webpack) and esbuild perform this automatically in production builds. Minification alone typically reduces bundle size by 30-50%. Advanced minifiers also perform dead code elimination, constant folding, and function inlining. Source maps (.map files) preserve the mapping between minified and original code for debugging.
Tree shaking is dead code elimination for ES modules. When you import { useState } from 'react', a tree-shaking bundler analyzes the static import graph and excludes everything you did not import. This only works with ES module syntax (import/export), not CommonJS (require/module.exports), because static analysis requires statically determinable imports. Mark your package.json with "sideEffects": false to tell bundlers that unused exports can be safely removed.
Code splitting is the practice of breaking your bundle into smaller chunks that load on demand. There are three primary strategies:
Route-based splitting: Each page or route becomes its own chunk. In Next.js, this happens automatically — each page in the app/ directory is a separate entry point. Users visiting /about never download code for /dashboard.
Component-based splitting: Heavy components (rich text editors, chart libraries, modals) are loaded only when needed using React.lazy() and Suspense. This keeps the initial bundle small.
Dynamic imports: The import() expression returns a promise and tells the bundler to create a separate chunk. For example, const module = await import('./heavyCalculation') loads the module on demand at runtime.
Tools like webpack-bundle-analyzer, source-map-explorer, and @next/bundle-analyzer visualize your bundles as interactive treemaps, showing exactly which libraries consume the most space. Common findings include accidentally bundling moment.js (300KB+), lodash (70KB+ when not tree-shaken), or duplicate copies of the same library at different versions.
Modern bundlers support vendor chunk splitting — separating third-party libraries (which change rarely) from application code (which changes frequently). This maximizes cache efficiency because the vendor chunk's hash stays the same across deployments. Webpack's splitChunks configuration and Vite's manualChunks option control this behavior.
Minification reduces file size without changing code structure. Tree shaking removes unused exports at the module level. Code splitting breaks the bundle into multiple files loaded on demand. These are three complementary techniques that work together — a well-optimized production build uses all three.
Fun Fact
The term 'tree shaking' was popularized by the Rollup bundler in 2015. The metaphor imagines your dependency graph as a tree — you shake it, and the dead (unused) code falls out like dead leaves.