|$ curl https://forge-ai.dev/api/markdown?path=docs/js/bundling
$cat docs/javascript-bundling.md
updated Recently·18 min read·published

JavaScript Bundling

JavaScriptBuild ToolsBundlingAdvanced🎯Free Tools
Why Bundle JavaScript?

JavaScript bundling is the process of combining multiple source files into a single (or few) output files for the browser. Bundlers resolve import/require graphs, transform modern syntax for older browsers, optimize output through minification and tree-shaking, and split code into chunks for efficient loading.

Without bundlers, serving many small JS files caused network overhead (HTTP/1.1 connection limits, request latency). While HTTP/2 multiplexing alleviates this, bundlers remain essential for transformation (JSX, TypeScript), optimization (dead code elimination), and code splitting (lazy loading).

How Bundlers Work

All bundlers follow the same core pipeline: entry point resolution, dependency graph construction, transformation (loaders/plugins), and output generation. The dependency graph is built by tracing imports from the entry file, creating a module graph that maps every dependency relationship.

bundler-pipeline.js
JavaScript
1// Simplified bundler pipeline
2const entry = './src/index.js';
3
4// 1. Parse entry point
5const entryAST = parse(entry);
6const deps = extractImports(entryAST); // ['./utils.js', './components/App.js']
7
8// 2. Build dependency graph recursively
9const graph = {
10 './src/index.js': {
11 code: readFile('./src/index.js'),
12 deps: { './utils.js': './src/utils.js' }
13 },
14 './src/utils.js': {
15 code: readFile('./src/utils.js'),
16 deps: {}
17 }
18};
19
20// 3. Bundle all modules into a single scope
21// Wrap each module in a function to isolate scope
22const bundle = `(function(modules) {
23 const cache = {};
24 function require(id) {
25 if (cache[id]) return cache[id];
26 const [fn, mapping] = modules[id];
27 const localRequire = (name) => require(mapping[name]);
28 const module = { exports: {} };
29 fn(localRequire, module, module.exports);
30 cache[id] = module.exports;
31 return module.exports;
32 }
33 require('./src/index.js');
34})({
35 './src/index.js': [function(require, module, exports) {
36 const utils = require('./utils.js');
37 // ... module code
38 }, { './utils.js': './src/utils.js' }],
39 './src/utils.js': [function(require, module, exports) {
40 // ... module code
41 }, {}]
42});`
43
44// 4. Apply optimizations (minification, tree-shaking)
45// 5. Output the final file(s)
Tree Shaking

Tree shaking eliminates dead code — exports that are imported but never used. It relies on ES module static analysis (import/export are evaluated at parse time, not runtime). Webpack, Rollup, and esbuild all support tree shaking, but it works best with side-effect-free modules.

tree-shaking.js
JavaScript
1// utils.js — exports multiple functions
2export function used() { return 'kept'; }
3export function unused() { return 'removed'; } // Tree-shaken away!
4
5// app.js — only imports what it needs
6import { used } from './utils.js';
7console.log(used());
8
9// After tree shaking, the bundle only contains 'used'
10
11// Package.json signals (helps bundlers optimize)
12{
13 "sideEffects": false, // All modules are side-effect-free
14 "module": "dist/index.esm.js" // Point to ES module build
15}
16
17// Bad — side effects prevent tree shaking
18import './polyfills.js'; // Side effect (modifies global)
19import { configure } from './config'; // configure may have side effects
20
21// Good — side-effect-free imports
22import { debounce } from 'lodash-es'; // Tree-shakeable lodash
23import { map, filter } from './utils'; // Only map and filter are kept
Code Splitting

Code splitting divides your bundle into smaller chunks that load on demand. Dynamic imports (import()) are the primary splitting mechanism. Routes, heavy components, and rarely-used libraries are ideal splitting candidates.

code-splitting.js
JavaScript
1// Dynamic import — creates a separate chunk
2const Chart = () => import('./Chart.js');
3// Webpack/Rollup output: 1.chunk.js (contains Chart module)
4
5// React lazy loading
6import { lazy, Suspense } from 'react';
7const AdminPanel = lazy(() => import('./AdminPanel.jsx'));
8
9function App() {
10 return (
11 <Suspense fallback={<Loading />}>
12 <Route path="/admin" element={<AdminPanel />} />
13 </Suspense>
14 );
15}
16
17// Vendor splitting — separate vendor and application code
18// webpack.config.js
19splitChunks: {
20 cacheGroups: {
21 vendor: {
22 test: /[\\/]node_modules[\\/]/,
23 name: 'vendor',
24 chunks: 'all',
25 },
26 },
27},
28
29// Result:
30// vendor.chunk.js — React, lodash, etc. (rarely changes, cached)
31// main.chunk.js — Application code
32// admin.chunk.js — Admin panel (loaded on demand)
Key Takeaways
  • Bundlers resolve the module graph, transform syntax, and optimize output for production
  • Tree shaking eliminates unused exports — works best with ESM and side-effect-free modules
  • Code splitting via dynamic imports reduces initial bundle size and improves load time
  • Vendor splitting separates framework code from application code for better caching
  • esbuild and Rollup are preferred for library bundling; Webpack/Vite for applications

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.