build-pipelines-bundling
Build Pipelines and Bundling
Overview
Build pipelines transform your source code into optimized assets for browsers. Understanding this process is essential for performance optimization and debugging.
Why Bundling Exists
Browsers historically couldn't handle modern JavaScript development patterns:
// YOUR CODE: Many small files with imports
// src/
// ├── index.js (imports App)
// ├── App.js (imports Header, Main, Footer)
// ├── Header.js (imports Logo, Nav)
// ├── Nav.js (imports NavLink)
// └── ... 100+ files
// PROBLEM 1: HTTP/1.1 could only load 6 files in parallel
// 100 files = 17 round trips = SLOW
// PROBLEM 2: Browsers didn't support import/export (until ES modules)
import { Component } from './Component.js'; // Didn't work!
// PROBLEM 3: npm packages live in node_modules
import React from 'react'; // Browser can't resolve this path!
// SOLUTION: Bundle everything into fewer files
// dist/
// ├── index.html
// ├── main.js (all your code + dependencies)
// └── main.css
The Build Pipeline
SOURCE CODE BUILD PIPELINE OUTPUT
─────────────────────────────────────────────────────────────────────────────
src/ dist/
├── index.tsx ───┐ ┌─► index.html
├── App.tsx │ ┌──────────────────────────────┐ │
├── components/ ├───►│ 1. Resolve imports │ ├─► main.[hash].js
│ ├── Header.tsx │ │ 2. Transform (TS, JSX, etc) │ │
│ └── Button.tsx │ │ 3. Bundle modules │───►├─► vendor.[hash].js
├── styles/ │ │ 4. Optimize (minify, etc) │ │
│ └── main.css │ │ 5. Output files │ ├─► main.[hash].css
└── assets/ │ └──────────────────────────────┘ │
└── logo.png ────┘ └─► assets/logo.[hash].png
Bundler Comparison
| Bundler | Speed | Configuration | Best For |
|---|---|---|---|
| Webpack | Slower | Complex, powerful | Large apps, legacy |
| Vite | Fast (dev) | Minimal | Modern apps, DX |
| esbuild | Fastest | Limited | Build step, library |
| Rollup | Medium | Plugin-focused | Libraries |
| Parcel | Fast | Zero-config | Quick prototypes |
| Turbopack | Fast | Webpack-compatible | Next.js |
Core Bundling Concepts
Module Resolution
How bundlers find your imports:
// Relative imports - resolved from current file
import { Button } from './components/Button';
// → src/components/Button.js
// Bare imports - resolved from node_modules
import React from 'react';
// → node_modules/react/index.js
// Alias imports - resolved via config
import { api } from '@/lib/api';
// → src/lib/api.js (@ mapped to src/)
// RESOLUTION ORDER (Node-style):
// 1. Exact path: ./Button.js
// 2. Add extensions: ./Button → ./Button.js, ./Button.ts, ./Button.tsx
// 3. Index files: ./Button → ./Button/index.js
// 4. Package.json "main" or "exports" field
Dependency Graph
Bundlers build a graph of all dependencies:
Entry: src/index.tsx
│
▼
┌─────────┐
│ index │
└────┬────┘
│ imports
▼
┌─────────┐ ┌─────────┐
│ App │────►│ React │
└────┬────┘ └─────────┘
│ imports
┌────┴────┐
▼ ▼
┌────────┐ ┌────────┐
│ Header │ │ Footer │
└───┬────┘ └────────┘
│ imports
▼
┌─────────┐
│ Logo │
└─────────┘
// Bundler walks this graph:
// 1. Start at entry point
// 2. Parse file, find imports
// 3. Recursively process each import
// 4. Build complete dependency graph
// 5. Output bundle in correct order
Code Splitting
Breaking your bundle into smaller pieces loaded on demand.
Why Code Split?
WITHOUT CODE SPLITTING:
┌─────────────────────────────────────────┐
│ main.js (2MB) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌───────────┐ │
│ │Home │ │About│ │Blog │ │ Dashboard │ │
│ └─────┘ └─────┘ └─────┘ └───────────┘ │
└─────────────────────────────────────────┘
User visits /home → Downloads 2MB (includes unused Dashboard code)
WITH CODE SPLITTING:
┌──────────────┐
│ main.js (50KB)│ ← Core app, router
└──────────────┘
│
├─► home.js (30KB) ← Loaded on /home
├─► about.js (20KB) ← Loaded on /about
├─► blog.js (40KB) ← Loaded on /blog
└─► dashboard.js (500KB) ← Loaded only on /dashboard
User visits /home → Downloads 80KB (main + home)
Split Strategies
1. Route-Based Splitting
// React with lazy loading
import { lazy, Suspense } from 'react';
// Each route becomes a separate chunk
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
);
}
// Build output:
// main.js - core app
// pages-Home-[hash].js - home chunk
// pages-About-[hash].js - about chunk
// pages-Dashboard-[hash].js - dashboard chunk
2. Component-Based Splitting
// Heavy components loaded on demand
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const MarkdownEditor = lazy(() => import('./components/MarkdownEditor'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart /> {/* Loaded only when needed */}
</Suspense>
)}
</div>
);
}
3. Vendor Splitting
// Webpack config
optimization: {
splitChunks: {
cacheGroups: {
// Separate node_modules into vendor chunk
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
// Separate large libraries
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
},
},
},
}
// Output:
// main.js - your code
// vendors.js - all node_modules
// react.js - react + react-dom (cached separately)
Chunking Strategies
Chunk Types
ENTRY CHUNKS:
- Starting points of your application
- Usually one per "page" or entry point
ASYNC CHUNKS:
- Created by dynamic imports: import('./module')
- Loaded on demand
COMMON/SHARED CHUNKS:
- Code used by multiple chunks
- Extracted to avoid duplication
VENDOR CHUNKS:
- Third-party code from node_modules
- Changes less frequently = better caching
Optimal Chunking
// Webpack splitChunks configuration
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000, // Minimum chunk size (20KB)
maxSize: 244000, // Try to keep under 244KB
minChunks: 1, // Minimum times a module is shared
maxAsyncRequests: 30, // Max parallel requests for async chunks
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2, // Split if used 2+ times
priority: -20,
reuseExistingChunk: true,
},
},
},
}
Tree Shaking
Removing unused code from the bundle.
How It Works
// utils.js - exports multiple functions
export function usedFunction() {
return 'I am used';
}
export function unusedFunction() {
return 'I am never imported anywhere';
}
export const USED_CONSTANT = 42;
export const UNUSED_CONSTANT = 999;
// app.js - only imports some exports
import { usedFunction, USED_CONSTANT } from './utils';
console.log(usedFunction(), USED_CONSTANT);
// AFTER TREE SHAKING:
// Bundle only contains usedFunction and USED_CONSTANT
// unusedFunction and UNUSED_CONSTANT are removed
Requirements for Tree Shaking
// ✓ WORKS: ES modules (static structure)
import { specific } from 'library';
export function myFunction() {}
// ✗ DOESN'T WORK: CommonJS (dynamic structure)
const library = require('library');
module.exports = myFunction;
// ✗ DOESN'T WORK: Dynamic imports of specific exports
const { specific } = await import('library'); // Can tree shake the import
// But the library must use ES modules internally
// SIDE EFFECTS: Code that runs on import
// package.json
{
"sideEffects": false // All files are pure, safe to tree shake
}
// Or specify which files have side effects:
{
"sideEffects": [
"*.css", // CSS imports have side effects
"./src/polyfills.js" // This file runs code on import
]
}
Minification
Reducing code size without changing behavior.
Techniques
// ORIGINAL CODE:
function calculateTotalPrice(items) {
let totalPrice = 0;
for (let i = 0; i < items.length; i++) {
totalPrice += items[i].price * items[i].quantity;
}
return totalPrice;
}
// AFTER MINIFICATION:
function calculateTotalPrice(e){let t=0;for(let l=0;l<e.length;l++)t+=e[l].price*e[l].quantity;return t}
// TECHNIQUES APPLIED:
// 1. Whitespace removal
// 2. Variable name shortening (mangling)
// 3. Dead code elimination
// 4. Constant folding: 1 + 2 → 3
// 5. Boolean simplification: !!x → x (in boolean context)
Minifiers
| Tool | Speed | Compression | Use Case |
|---|---|---|---|
| Terser | Slow | Best | Production builds |
| esbuild | Fastest | Good | Development, fast builds |
| SWC | Very fast | Good | Next.js, Rust-based |
| UglifyJS | Slow | Good | Legacy projects |
Source Maps
Mapping minified code back to source for debugging.
// MINIFIED CODE (main.js):
function a(e){throw new Error("Invalid: "+e)}
// SOURCE MAP (main.js.map):
{
"version": 3,
"sources": ["src/validation.ts"],
"names": ["throwValidationError", "message"],
"mappings": "AAAA,SAASA,EAAoBC..."
}
// BROWSER DEVTOOLS:
// Shows original source with meaningful names:
function throwValidationError(message) {
throw new Error("Invalid: " + message);
}
// With correct line numbers!
Source Map Types
// Webpack devtool options:
// Development:
devtool: 'eval-source-map' // Fast rebuild, full source maps
devtool: 'eval-cheap-source-map' // Faster, line-only mapping
// Production:
devtool: 'source-map' // Full source maps (separate file)
devtool: 'hidden-source-map' // Source maps generated but not linked
devtool: false // No source maps
Asset Handling
Processing non-JavaScript assets.
Images
// Webpack (with asset modules)
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // Inline if < 8KB
}
}
}
// Vite (automatic)
import logo from './logo.png'; // Returns URL
import icon from './icon.svg?raw'; // Returns SVG content
// Output:
// Small images → Inlined as data URLs
// Large images → Copied to dist with hash
CSS
// CSS Processing Pipeline:
// 1. PostCSS (autoprefixer, future CSS)
// 2. CSS Modules (scoped class names)
// 3. Minification (cssnano)
// 4. Extraction (separate .css files)
// Vite handles this automatically
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>Click</button>;
}
// Output:
// .button → .Button_button_x7h3j (scoped)
Content Hashing
Cache busting with content-based filenames.
// WITHOUT HASHING:
// main.js - browser caches indefinitely
// Update code → browser still uses old cached version!
// WITH CONTENT HASHING:
// main.a1b2c3d4.js - hash based on content
// Update code → new hash → new filename → browser fetches new version
// Webpack configuration:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
}
// Benefits:
// - Unchanged files keep same hash = cached
// - Changed files get new hash = fresh download
// - Long cache headers possible (immutable)
Build Performance Optimization
Caching
// Webpack persistent caching
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename], // Invalidate on config change
},
}
// First build: 30 seconds
// Subsequent builds: 5 seconds (cache hit)
Parallelization
// Webpack thread-loader for expensive loaders
{
test: /\.tsx?$/,
use: [
'thread-loader', // Run in worker pool
'babel-loader',
],
}
// esbuild/SWC are parallel by default (Rust/Go)
Excluding node_modules
// Don't transform node_modules (already compiled)
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
}
Build Analysis
Understanding your bundle contents.
# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
# Add to webpack config:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
# Generates interactive treemap of bundle contents
# source-map-explorer
npx source-map-explorer dist/main.js
# Vite
npx vite-bundle-visualizer
Deep Dive: Understanding Bundling From First Principles
What Bundlers Actually Do: Step by Step
Let's trace through exactly what happens when you run npm run build:
// YOUR SOURCE FILES:
// src/index.js
import { greet } from './utils.js';
import React from 'react';
console.log(greet('World'));
// src/utils.js
export function greet(name) {
return `Hello, ${name}!`;
}
export function unused() {
return 'Never called';
}
Step 1: Parse Entry Point
// Bundler parses index.js into AST (Abstract Syntax Tree)
{
type: 'Program',
body: [
{
type: 'ImportDeclaration',
source: { value: './utils.js' },
specifiers: [{ imported: { name: 'greet' } }]
},
{
type: 'ImportDeclaration',
source: { value: 'react' },
specifiers: [{ imported: { name: 'default' }, local: { name: 'React' } }]
},
// ... rest of AST
]
}
Step 2: Resolve Dependencies
// For each import, resolve to actual file path
// './utils.js'
// → Current dir: /project/src/
// → Resolved: /project/src/utils.js ✓
// 'react'
// → Not relative, check node_modules
// → /project/node_modules/react/package.json
// → "main": "index.js"
// → Resolved: /project/node_modules/react/index.js ✓
Step 3: Build Dependency Graph
// Graph structure (simplified)
const graph = {
'/project/src/index.js': {
dependencies: [
'/project/src/utils.js',
'/project/node_modules/react/index.js'
],
code: '...',
exports: [],
imports: ['greet', 'React']
},
'/project/src/utils.js': {
dependencies: [],
code: '...',
exports: ['greet', 'unused'],
imports: []
},
// ... react and its dependencies
};
Step 4: Transform Code
// Each file goes through loaders/plugins
// TypeScript → JavaScript
// TSX/JSX → React.createElement calls
// Modern JS → Compatible JS (Babel)
// Input (TSX):
const Button: React.FC = () => <button>Click</button>;
// Output (JS):
const Button = () => React.createElement("button", null, "Click");
Step 5: Tree Shake
// Analyze what's actually used
// utils.js exports: ['greet', 'unused']
// index.js imports from utils: ['greet']
//
// 'unused' is never imported anywhere
// Mark for removal
// After tree shaking, only 'greet' is included
Step 6: Concatenate Modules
// Combine all modules into bundle format
// IIFE wrapper (Immediately Invoked Function Expression)
(function(modules) {
// Module cache
var installedModules = {};
// Module loader
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = { exports: {} };
modules[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// Start at entry point
return __webpack_require__('./src/index.js');
})({
'./src/index.js': function(module, exports, __webpack_require__) {
var utils = __webpack_require__('./src/utils.js');
var React = __webpack_require__('react');
console.log(utils.greet('World'));
},
'./src/utils.js': function(module, exports) {
exports.greet = function(name) {
return 'Hello, ' + name + '!';
};
// Note: unused() is gone!
},
// ... react modules
});
Step 7: Minify
// Terser/esbuild minification
// Before:
function greet(name) {
return 'Hello, ' + name + '!';
}
// After:
function greet(n){return"Hello, "+n+"!"}
// Or with further optimization:
const greet=n=>"Hello, "+n+"!";
Step 8: Output with Hashing
dist/
├── index.html
├── main.7f8a2b3c.js ← Hash based on content
├── main.7f8a2b3c.js.map ← Source map
└── index.html ← References hashed files
Module Formats: A History Lesson
Understanding why we have multiple module systems:
// 1. NO MODULES (Pre-2009)
// Everything in global scope
// script.js
var myApp = {};
myApp.utils = {
greet: function(name) { return 'Hello, ' + name; }
};
// Problem: Global namespace pollution, dependency order matters
// 2. COMMONJS (2009, Node.js)
// Synchronous, designed for servers
// utils.js
module.exports.greet = function(name) { return 'Hello, ' + name; };
// index.js
const { greet } = require('./utils');
// Problem: Synchronous require() doesn't work in browsers
// 3. AMD - Asynchronous Module Definition (2011, RequireJS)
// Async loading for browsers
define(['./utils'], function(utils) {
console.log(utils.greet('World'));
});
// Problem: Verbose, non-standard
// 4. UMD - Universal Module Definition (2014)
// Works everywhere (browser global, CommonJS, AMD)
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dep'], factory); // AMD
} else if (typeof module === 'object') {
module.exports = factory(require('dep')); // CommonJS
} else {
root.myLib = factory(root.dep); // Browser global
}
}(this, function(dep) {
return { greet: function(name) { return 'Hello, ' + name; } };
}));
// Problem: Complex wrapper for every file
// 5. ES MODULES (2015, ES6)
// Official JavaScript standard
// Static structure enables tree shaking
export function greet(name) { return 'Hello, ' + name; }
import { greet } from './utils.js';
// Now supported natively in browsers and Node.js!
Why Vite is Fast: Native ES Modules
Traditional bundlers (Webpack) vs Vite approach:
WEBPACK DEVELOPMENT:
Your Code Webpack Browser
────────────────────────────────────────────────────────────────
src/
├── index.js ─┐
├── App.js ├─► Bundle ALL ─► bundle.js ─────► Load bundle
├── Header.js │ files
└── 100+ more ─┘ together
Time: Parse + transform + bundle ALL files = 10-30 seconds
Change 1 file → Rebundle everything = 2-5 seconds
VITE DEVELOPMENT:
Your Code Vite Browser
────────────────────────────────────────────────────────────────
src/
├── index.js ──────────────────────────────────► Request each
├── App.js ─► Transform ─► Serve directly ──► file when needed
├── Header.js on demand via native ES
└── 100+ more ─► (only if requested) module imports
Time: Transform only requested files = 300-500ms
Change 1 file → Transform only that file = <100ms
WHY THIS WORKS:
<!-- Browser requests via native ES modules -->
<script type="module" src="/src/index.js"></script>
// index.js (served directly, not bundled)
import { App } from './App.js'; // Browser makes another request
// Browser's network tab:
// GET /src/index.js
// GET /src/App.js (from import in index.js)
// GET /src/Header.js (from import in App.js)
// ...
Dynamic Imports: How Code Splitting Works
The magic behind import():
// STATIC IMPORT (resolved at build time)
import { heavy } from './heavy.js';
// Always included in main bundle
// DYNAMIC IMPORT (resolved at runtime)
const heavy = await import('./heavy.js');
// Creates a separate chunk, loaded on demand
What the bundler does:
// Your code:
button.onclick = async () => {
const { HeavyComponent } = await import('./HeavyComponent.js');
render(HeavyComponent);
};
// Bundler output:
// main.js
button.onclick = async () => {
const { HeavyComponent } = await __webpack_require__.e("HeavyComponent")
.then(__webpack_require__.bind(__webpack_require__, "./HeavyComponent.js"));
render(HeavyComponent);
};
// HeavyComponent.a1b2c3.chunk.js (separate file)
(self["webpackChunk"] = self["webpackChunk"] || []).push([
["HeavyComponent"],
{
"./HeavyComponent.js": (module, exports) => {
exports.HeavyComponent = function() { /* ... */ };
}
}
]);
// At runtime:
// 1. User clicks button
// 2. __webpack_require__.e creates <script> tag
// 3. Browser fetches HeavyComponent.a1b2c3.chunk.js
// 4. Script executes and registers module
// 5. Promise resolves with exports
// 6. Component renders
Magic Comments: Controlling Chunk Behavior
// Name the chunk (for debugging/analysis)
const Admin = () => import(/* webpackChunkName: "admin" */ './Admin');
// Output: admin.a1b2c3.js
// Prefetch (load in background after main content)
const Settings = () => import(/* webpackPrefetch: true */ './Settings');
// Adds: <link rel="prefetch" href="settings.chunk.js">
// Preload (load immediately, needed soon)
const Critical = () => import(/* webpackPreload: true */ './Critical');
// Adds: <link rel="preload" href="critical.chunk.js">
// Control loading mode
const Lib = () => import(/* webpackMode: "lazy-once" */ `./lib/${name}`);
// Modes: lazy (default), lazy-once, eager, weak
Tree Shaking Deep Dive: Static Analysis
Why ES modules can be tree shaken but CommonJS cannot:
// ES MODULES - STATIC STRUCTURE
// Imports/exports are determined at parse time (before execution)
import { a, b } from './utils'; // ALWAYS imports a and b
export { x, y }; // ALWAYS exports x and y
// The bundler can statically analyze:
// "This file exports x and y"
// "This file imports a and b"
// No code execution needed!
// COMMONJS - DYNAMIC STRUCTURE
// Imports/exports determined at runtime
const utils = require('./utils'); // What does this export? Depends on runtime
module.exports = something; // What is 'something'? Depends on runtime
// Examples that break static analysis:
if (condition) {
module.exports = optionA;
} else {
module.exports = optionB;
}
const name = 'foo';
module.exports[name] = value; // Dynamic property
require('./' + dynamicPath); // Path not known until runtime
The side effects problem:
// utils.js
console.log('Utils loaded!'); // SIDE EFFECT - runs on import
export function used() { return 'used'; }
export function unused() { return 'unused'; }
// Even if 'unused' is never called, we import the file
// The console.log runs!
// Bundler can't remove the file entirely
// SOLUTION: Mark as side-effect free
// package.json
{
"sideEffects": false
}
// Now bundler knows: if no exports are used, skip the entire file
Scope Hoisting: Module Concatenation
Modern bundlers can merge modules for smaller bundles:
// WITHOUT SCOPE HOISTING:
// Each module wrapped in function
(function(modules) {
function __require__(id) { /* ... */ }
// Module 0
(function(module, exports, __require__) {
var utils = __require__(1);
console.log(utils.greet('World'));
});
// Module 1
(function(module, exports) {
exports.greet = function(name) { return 'Hello, ' + name; };
});
})();
// WITH SCOPE HOISTING (Module Concatenation):
// Modules merged into one scope
(function() {
// From utils.js (inlined)
function greet(name) { return 'Hello, ' + name; }
// From index.js
console.log(greet('World'));
})();
// Benefits:
// - Smaller bundle (no module wrappers)
// - Faster execution (no function call overhead)
// - Better minification (variables can be renamed together)
Chunk Splitting Algorithms
How bundlers decide what goes in which chunk:
// WEBPACK'S SPLITCHUNKS ALGORITHM (Simplified)
// For each module, track:
// - Which chunks include it
// - Size of the module
// - Whether it's from node_modules
// Decision process:
for (module of allModules) {
const chunks = module.usedInChunks;
// Rule 1: Shared by multiple chunks?
if (chunks.length >= minChunks) {
// Candidate for extraction
// Rule 2: Big enough to be worth a separate request?
if (module.size >= minSize) {
// Rule 3: Won't create too many parallel requests?
if (currentAsyncRequests < maxAsyncRequests) {
// Extract to common chunk!
createCommonChunk(module);
}
}
}
}
// RESULT:
// Shared code in common chunks
// Route-specific code in route chunks
// Vendor code in vendor chunks
// Balance between: cache efficiency vs request count
Long-Term Caching Strategy
Optimal caching requires careful chunk design:
// PROBLEM: Any change invalidates entire bundle
// main.js contains: app code + vendor code
// Change 1 line of app code → new hash → users re-download everything
// SOLUTION: Separate by change frequency
// 1. Runtime chunk (tiny, rarely changes)
output: {
filename: '[name].[contenthash].js',
},
optimization: {
runtimeChunk: 'single', // Webpack runtime in separate file
}
// 2. Vendor chunk (medium, changes on npm update)
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
}
// 3. App chunks (changes frequently)
// Default behavior - your code
// RESULT:
// runtime.a1b1b1.js (1KB) - Almost never changes
// vendors.c2c2c2.js (200KB) - Changes on npm update
// main.d3d3d3.js (50KB) - Changes on each deploy
// page-home.e4e4e4.js (10KB) - Changes when home page changes
// User has cached vendors.js
// You deploy change to home page
// User only downloads: main.js + page-home.js (60KB vs 260KB)
Build Performance: What Makes Builds Slow?
SLOW OPERATIONS (in order of cost):
1. TYPESCRIPT COMPILATION
- Type checking is expensive
- Solution: Use transpileOnly, run tsc separately
2. BABEL TRANSFORMS
- Parsing + transforming every file
- Solution: Exclude node_modules, cache results
3. MINIFICATION
- Complex AST analysis
- Solution: Use esbuild/SWC instead of Terser
4. SOURCE MAP GENERATION
- Creating mapping data
- Solution: Use simpler source map types in dev
5. LARGE DEPENDENCIES
- Parsing huge node_modules
- Solution: Externalize, use pre-bundled versions
OPTIMIZATION CHECKLIST:
□ Enable persistent caching (cache: { type: 'filesystem' })
□ Exclude node_modules from babel/typescript
□ Use thread-loader for parallel processing
□ Use esbuild for minification
□ Use 'eval-source-map' in development
□ Analyze bundle to find unexpected large dependencies
The Future: VoidZero and the Unified Rust Toolchain
The Fragmentation Problem
Today's JavaScript tooling is fragmented across many tools:
CURRENT STATE (2024):
Parsing: Babel, TypeScript, SWC, esbuild (each has own parser)
Transforming: Babel, SWC, esbuild, TypeScript (duplicated work)
Bundling: Webpack, Rollup, esbuild, Parcel (different algorithms)
Minifying: Terser, esbuild, SWC (yet more parsers)
Linting: ESLint (slow, JavaScript-based)
Formatting: Prettier (slow, JavaScript-based)
Type Check: TypeScript (can't be parallelized easily)
PROBLEMS:
1. Each tool parses your code separately = redundant work
2. Different tools have different bugs/behaviors
3. JavaScript tools are inherently slow (single-threaded, interpreted)
4. Configuration complexity across many tools
5. Inconsistent error messages and source locations
VoidZero's Vision
VoidZero, founded by Evan You (creator of Vue.js and Vite), is building a unified JavaScript toolchain in Rust:
VOIDZERO UNIFIED TOOLCHAIN:
┌─────────────────────────────────────────┐
│ OXC (Core) │
│ Rust-based JavaScript/TypeScript │
│ Parser, Linter, Transformer, Resolver │
└─────────────────┬───────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Rolldown │ │ oxc-linter │ │ oxc-transform │
│ (Bundler) │ │ (ESLint alt) │ │ (Babel alt) │
└────────┬────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Vite │
│ (Dev + Build) │
└─────────────────┘
SHARED BENEFITS:
- Single parser for all operations
- Consistent AST representation
- Native speed (Rust, parallelized)
- Unified configuration
- Coherent error messages
Oxc (Oxidation Compiler)
Oxc is a collection of high-performance JavaScript/TypeScript tools written in Rust:
OXC COMPONENTS:
┌─────────────────────────────────────────────────────────────────┐
│ oxc_parser │
│ - Parses JavaScript, TypeScript, JSX, TSX │
│ - 3x faster than SWC, 5x faster than Babel │
│ - Produces identical AST for all downstream tools │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ oxc_linter │ │ oxc_transformer│ │ oxc_minifier │
│ │ │ │ │ │
│ 50-100x faster│ │ TypeScript →JS │ │ Compresses │
│ than ESLint │ │ JSX → JS │ │ output code │
│ │ │ Modern → Legacy│ │ │
└───────────────┘ └───────────────┘ └───────────────┘
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ oxc_resolver │ │ oxc_sourcemap │ │ oxc_prettier │
│ │ │ │ │ (planned) │
│ Module │ │ Source map │ │ │
│ resolution │ │ generation │ │ Code │
│ │ │ │ │ formatting │
└───────────────┘ └───────────────┘ └───────────────┘
Performance comparison:
PARSING BENCHMARK (large codebase):
Tool Time Relative
────────────────────────────────────
oxc_parser 45ms 1.0x (baseline)
swc_parser 150ms 3.3x slower
esbuild 180ms 4.0x slower
babel 900ms 20x slower
typescript 1200ms 26x slower
LINTING BENCHMARK (ESLint rules):
Tool Time
────────────────────────────────────
oxc_linter 0.5s
ESLint 50s (100x slower)
WHY SO FAST?
1. Rust: No garbage collection pauses
2. Parallelization: Uses all CPU cores
3. Zero-copy parsing: Minimal memory allocation
4. SIMD: CPU vectorization for string operations
5. Single pass: Parse once, analyze everything
Rolldown: The Rollup Replacement
Rolldown is a Rust-based bundler designed as a drop-in Rollup replacement:
ROLLDOWN VS ROLLUP:
┌─────────────────────────────────────────────────────────────────┐
│ ROLLUP │
│ │
│ Language: JavaScript │
│ Speed: Medium (single-threaded JS) │
│ Plugins: Rich ecosystem (established) │
│ Output: Excellent tree-shaking and code quality │
│ Use case: Libraries, ES module output │
│ │
│ LIMITATIONS: │
│ - Can't parallelize (JavaScript) │
│ - Large projects = slow builds │
│ - Memory-intensive for big codebases │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ROLLDOWN │
│ │
│ Language: Rust │
│ Speed: 10-30x faster than Rollup │
│ Plugins: Rollup-compatible (most plugins work) │
│ Output: Same quality as Rollup │
│ Use case: Applications + Libraries │
│ │
│ ADVANTAGES: │
│ - Fully parallelized (Rust + Rayon) │
│ - Built on Oxc (shared parser/resolver) │
│ - esbuild-level speed with Rollup-quality output │
│ - Native code splitting │
│ - Built-in transforms (no separate Babel needed) │
└─────────────────────────────────────────────────────────────────┘
Rolldown architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Rolldown Build Pipeline │
└─────────────────────────────────────────────────────────────────┘
Entry Points
│
▼
┌─────────────────┐
│ oxc_resolver │ ← Resolve all imports (parallelized)
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_parser │ ← Parse all files (parallelized)
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_transformer │ ← Transform TS/JSX (parallelized)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Link & Bundle │ ← Scope hoisting, tree shaking
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_minifier │ ← Minify output (parallelized)
└────────┬────────┘
│
▼
Output Chunks
EVERYTHING shares the same AST representation
NO re-parsing between steps
How Vite Will Use Rolldown
Vite currently uses different tools for dev vs production:
VITE TODAY (2024):
Development:
- Native ES modules (fast)
- esbuild for dependency pre-bundling
- esbuild for TypeScript/JSX transform
Production:
- Rollup for bundling
- Terser or esbuild for minification
PROBLEM: Different tools = different behaviors
- Dev and prod can have subtle differences
- Configuration split across tools
- Can't share optimizations between modes
VITE FUTURE (with Rolldown):
Development:
- Native ES modules (fast)
- Rolldown for dependency pre-bundling
- Oxc for TypeScript/JSX transform
Production:
- Rolldown for bundling
- Oxc minifier for minification
BENEFIT: Same tool for dev and prod
- Consistent behavior
- Unified configuration
- Shared caching between modes
- Even faster production builds
The Complete VoidZero Ecosystem
┌─────────────────────────────────────────────────────────────────┐
│ VOIDZERO ECOSYSTEM │
└─────────────────────────────────────────────────────────────────┘
USER FACING:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vite │ │ Vitest │ │ VitePress │
│ Build Tool │ │ Test Runner │ │ Documentation │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
INFRASTRUCTURE:
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Rolldown │ │ oxc-linter │ │ Lightning │
│ Bundler │ │ (Linter) │ │ CSS (CSS) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
CORE FOUNDATION:
│
▼
┌─────────────────────┐
│ OXC │
│ Parser, Resolver, │
│ Transformer, etc. │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ NAPI-RS │
│ Rust ↔ Node.js │
│ bindings │
└─────────────────────┘
Why Rust for JavaScript Tooling?
// JAVASCRIPT LIMITATIONS FOR TOOLING:
// 1. SINGLE-THREADED
// JavaScript can only use one CPU core for computation
for (const file of files) {
parse(file); // Sequential, can't parallelize
}
// 2. GARBAGE COLLECTION PAUSES
// GC stops the world periodically
parseHugeFile(); // Allocates memory
// ... GC pause ... build freezes for 50-200ms
// 3. INTERPRETED OVERHEAD
// Even with JIT, slower than native code
const ast = parse(source); // ~10x slower than native
// RUST ADVANTAGES:
// 1. TRUE PARALLELISM
// Rust can use all CPU cores safely
files.par_iter().map(|file| parse(file)).collect() // 8x speedup on 8 cores
// 2. NO GC PAUSES
// Memory managed at compile time, deterministic
// No random freezes during builds
// 3. NATIVE SPEED
// Compiles to machine code
// ~10-100x faster for parsing/transforming
// 4. MEMORY SAFETY
// Rust's ownership system prevents:
// - Memory leaks
// - Use after free
// - Data races
// Without sacrificing performance
// 5. SIMD/VECTORIZATION
// Rust can use CPU vector instructions
// String operations 4-8x faster with SIMD
Migration Path: Rollup to Rolldown
Rolldown aims for Rollup compatibility:
// CURRENT ROLLUP CONFIG:
// rollup.config.js
import { defineConfig } from 'rollup';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
export default defineConfig({
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
resolve(),
commonjs(),
typescript(),
],
});
// FUTURE ROLLDOWN CONFIG (mostly compatible):
// rolldown.config.js
import { defineConfig } from 'rolldown';
import resolve from '@rollup/plugin-node-resolve'; // Works!
import commonjs from '@rollup/plugin-commonjs'; // Works!
// typescript() not needed - Rolldown handles TS natively via Oxc
export default defineConfig({
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
resolve(),
commonjs(),
// Built-in: TypeScript, JSX, minification
],
});
// COMPATIBILITY STATUS:
// ✓ Most Rollup plugins work (via compatibility layer)
// ✓ Same configuration format
// ✓ Same output quality
// ✗ Some plugins need updates (low-level AST manipulation)
// ✗ Plugin hooks have some differences
Comparing Modern Bundlers
┌────────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ │ Webpack │ Rollup │ esbuild │ Rolldown │ Turbopack│
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Language │ JS │ JS │ Go │ Rust │ Rust │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Speed │ Slow │ Medium │ Fast │ Fast │ Fast │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Tree Shake │ Good │ Best │ Good │ Best │ Good │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Code Split │ Best │ Good │ Basic │ Good │ Good │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Plugins │ Huge │ Large │ Limited │ Rollup* │ Webpack* │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Config │ Complex │ Simple │ Simple │ Simple │ Low-level│
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Maturity │ Mature │ Mature │ Mature │ Alpha │ Beta │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Used By │ Legacy │ Vite, │ Vite, │ Vite │ Next.js │
│ │ projects │ libs │ many │ (future) │ │
└────────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
* Rolldown: Rollup plugin compatibility
* Turbopack: Webpack plugin compatibility (planned)
The Tooling Evolution Timeline
2012: Webpack created (JS, comprehensive but slow)
└─► Dominated build tooling for years
2017: Parcel created (JS, zero-config)
└─► Popularized zero-config bundling
2018: Rollup gains popularity (JS, tree-shaking focus)
└─► Became standard for libraries
2020: esbuild released (Go, 100x faster)
└─► Proved native speed was possible
2021: Vite 2.0 released (esbuild + Rollup)
└─► Combined fast dev + quality production
2022: Turbopack announced (Rust, Vercel)
└─► Next.js-focused, Webpack successor
2023: Oxc development accelerates (Rust, VoidZero)
└─► Unified toolchain vision emerges
2024: Rolldown alpha (Rust, VoidZero)
└─► Rollup replacement for Vite
FUTURE: Unified toolchain
└─► One parser, one resolver, one transformer
└─► Consistent behavior dev → prod
└─► 10-100x faster than current tools
When to Use What (2024 Recommendations)
BUILDING A LIBRARY:
├─► Small/Medium: Rollup (mature, great tree-shaking)
├─► Large: Rolldown when stable, or esbuild for speed
└─► TypeScript: tsup (esbuild wrapper) or unbuild (Rollup wrapper)
BUILDING AN APPLICATION:
├─► New project: Vite (best DX, uses Rollup + esbuild)
├─► Next.js: Turbopack (experimental) or Webpack (stable)
├─► Large legacy: Webpack (ecosystem, stability)
└─► Performance critical: Consider Vite with Rolldown (when stable)
BUILDING A MONOREPO:
├─► Turborepo + Vite (caching + fast builds)
├─► Nx + any bundler (task orchestration)
└─► pnpm workspaces + Vite (simple, fast)
LINTING:
├─► Today: ESLint (comprehensive rules)
├─► Future: oxc-linter (when rule coverage is sufficient)
└─► Consider: Biome (Rust-based, fast, integrated)
FORMATTING:
├─► Today: Prettier (de facto standard)
├─► Alternative: Biome (faster, combined lint+format)
└─► Future: Oxc prettier (planned)
For Framework Authors: Building Bundler Integrations
Implementation Note: The patterns and code examples below represent one proven approach to building bundler integrations. Plugin APIs differ across bundlers—Rollup/Vite use a hook-based system, Webpack uses tapable, and esbuild has a simpler API. The direction shown here focuses on the Rollup/Vite pattern (most common for modern frameworks). Adapt based on which bundlers you need to support and whether you're building plugins or a custom bundler.
Writing Bundler Plugins
// VITE/ROLLUP PLUGIN STRUCTURE
function myPlugin(options = {}) {
return {
name: 'my-plugin',
// Called when config is resolved
configResolved(config) {
this.config = config;
},
// Transform source code
transform(code, id) {
if (!id.endsWith('.special')) return null;
// Transform the code
const transformed = processSpecialFile(code);
return {
code: transformed,
map: null, // Source map
};
},
// Resolve import paths
resolveId(source, importer) {
if (source.startsWith('virtual:')) {
return '\0' + source; // Prefix with \0 for virtual modules
}
return null; // Let other plugins handle
},
// Load virtual modules
load(id) {
if (id.startsWith('\0virtual:')) {
return `export default ${JSON.stringify(getVirtualContent(id))}`;
}
return null;
},
// Build hooks
buildStart() {
console.log('Build starting...');
},
buildEnd(error) {
if (error) console.error('Build failed:', error);
},
// Generate bundle output
generateBundle(options, bundle) {
// Modify or add to bundle
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: JSON.stringify(generateManifest(bundle)),
});
},
};
}
// Vite-specific hooks
function vitePlugin() {
return {
name: 'vite-specific',
// Dev server middleware
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/__my-plugin') {
res.end(JSON.stringify({ status: 'ok' }));
return;
}
next();
});
},
// HMR handling
handleHotUpdate({ file, server, modules }) {
if (file.endsWith('.custom')) {
// Custom HMR logic
server.ws.send({
type: 'custom',
event: 'custom-update',
data: { file },
});
return []; // Don't do normal HMR
}
},
// Transform index.html
transformIndexHtml(html) {
return html.replace(
'</head>',
`<script>window.__VERSION__="${Date.now()}"</script></head>`
);
},
};
}
Implementing Code Splitting Logic
// CUSTOM CODE SPLITTING STRATEGY
class ChunkSplitter {
constructor(options = {}) {
this.options = {
minChunkSize: options.minChunkSize || 20000,
maxChunkSize: options.maxChunkSize || 250000,
vendorPattern: options.vendorPattern || /node_modules/,
};
this.chunks = new Map();
}
// Analyze module graph
analyzeGraph(modules) {
const graph = {
nodes: new Map(),
edges: new Map(),
};
for (const mod of modules) {
graph.nodes.set(mod.id, {
id: mod.id,
size: mod.code.length,
isVendor: this.options.vendorPattern.test(mod.id),
imports: mod.imports,
importedBy: [],
});
}
// Build reverse edges
for (const [id, node] of graph.nodes) {
for (const imp of node.imports) {
const target = graph.nodes.get(imp);
if (target) {
target.importedBy.push(id);
}
}
}
return graph;
}
// Determine chunks
splitChunks(graph, entries) {
const chunks = [];
// Create vendor chunk
const vendorModules = [...graph.nodes.values()]
.filter(n => n.isVendor);
if (vendorModules.length > 0) {
chunks.push({
name: 'vendor',
modules: vendorModules.map(n => n.id),
});
}
// Create chunks per entry
for (const entry of entries) {
const reachable = this.getReachableModules(graph, entry);
const nonVendor = reachable.filter(id => !graph.nodes.get(id)?.isVendor);
// Split large chunks
const subChunks = this.splitBySize(nonVendor, graph);
chunks.push(...subChunks.map((mods, i) => ({
name: `${entry}-${i}`,
modules: mods,
})));
}
// Find shared chunks
const sharedChunks = this.findSharedChunks(chunks, graph);
chunks.push(...sharedChunks);
return chunks;
}
getReachableModules(graph, entry) {
const visited = new Set();
const queue = [entry];
while (queue.length > 0) {
const id = queue.shift();
if (visited.has(id)) continue;
visited.add(id);
const node = graph.nodes.get(id);
if (node) {
queue.push(...node.imports);
}
}
return [...visited];
}
findSharedChunks(chunks, graph) {
// Find modules used by multiple chunks
const moduleUsage = new Map();
for (const chunk of chunks) {
for (const modId of chunk.modules) {
if (!moduleUsage.has(modId)) {
moduleUsage.set(modId, []);
}
moduleUsage.get(modId).push(chunk.name);
}
}
// Extract frequently shared modules
const shared = [];
for (const [modId, usedBy] of moduleUsage) {
if (usedBy.length >= 2) {
shared.push(modId);
}
}
if (shared.length > 0) {
return [{ name: 'shared', modules: shared }];
}
return [];
}
}
Building a Module Transformer
// AST-BASED MODULE TRANSFORMER
import * as acorn from 'acorn';
import * as walk from 'acorn-walk';
import MagicString from 'magic-string';
class ModuleTransformer {
transform(code, options = {}) {
const ast = acorn.parse(code, {
ecmaVersion: 'latest',
sourceType: 'module',
});
const s = new MagicString(code);
// Transform imports
walk.simple(ast, {
ImportDeclaration(node) {
const source = node.source.value;
// Resolve bare imports
if (!source.startsWith('.') && !source.startsWith('/')) {
const resolved = resolveNodeModule(source);
s.overwrite(
node.source.start,
node.source.end,
`"${resolved}"`
);
}
},
// Transform dynamic imports
ImportExpression(node) {
const source = node.source;
if (source.type === 'Literal') {
const resolved = resolvePath(source.value);
s.overwrite(source.start, source.end, `"${resolved}"`);
}
},
// Transform exports for HMR
ExportDefaultDeclaration(node) {
if (options.hmr) {
const decl = node.declaration;
s.appendLeft(decl.start, '__hmr_wrap(');
s.appendRight(decl.end, ')');
}
},
});
return {
code: s.toString(),
map: s.generateMap({ hires: true }),
};
}
}
// Import analysis
function analyzeImports(code) {
const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
const imports = [];
const exports = [];
walk.simple(ast, {
ImportDeclaration(node) {
imports.push({
source: node.source.value,
specifiers: node.specifiers.map(s => ({
type: s.type,
imported: s.imported?.name || 'default',
local: s.local.name,
})),
});
},
ExportNamedDeclaration(node) {
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
for (const decl of node.declaration.declarations) {
exports.push({ name: decl.id.name, type: 'named' });
}
} else if (node.declaration.id) {
exports.push({ name: node.declaration.id.name, type: 'named' });
}
}
},
ExportDefaultDeclaration() {
exports.push({ name: 'default', type: 'default' });
},
});
return { imports, exports };
}
Implementing Source Maps
// SOURCE MAP GENERATION
class SourceMapGenerator {
constructor() {
this.mappings = [];
this.sources = [];
this.sourcesContent = [];
this.names = [];
}
addSource(filename, content) {
const index = this.sources.indexOf(filename);
if (index !== -1) return index;
this.sources.push(filename);
this.sourcesContent.push(content);
return this.sources.length - 1;
}
addMapping(generated, original, sourceIndex, name) {
this.mappings.push({
generatedLine: generated.line,
generatedColumn: generated.column,
originalLine: original.line,
originalColumn: original.column,
sourceIndex,
nameIndex: name ? this.addName(name) : undefined,
});
}
addName(name) {
const index = this.names.indexOf(name);
if (index !== -1) return index;
this.names.push(name);
return this.names.length - 1;
}
generate() {
// Sort mappings
this.mappings.sort((a, b) =>
a.generatedLine - b.generatedLine ||
a.generatedColumn - b.generatedColumn
);
// Encode mappings to VLQ
const encodedMappings = this.encodeMappings();
return {
version: 3,
sources: this.sources,
sourcesContent: this.sourcesContent,
names: this.names,
mappings: encodedMappings,
};
}
encodeMappings() {
let result = '';
let previousGeneratedLine = 1;
let previousGeneratedColumn = 0;
let previousOriginalLine = 0;
let previousOriginalColumn = 0;
let previousSourceIndex = 0;
let previousNameIndex = 0;
for (let i = 0; i < this.mappings.length; i++) {
const mapping = this.mappings[i];
// New lines
while (previousGeneratedLine < mapping.generatedLine) {
result += ';';
previousGeneratedLine++;
previousGeneratedColumn = 0;
}
if (i > 0 && this.mappings[i - 1].generatedLine === mapping.generatedLine) {
result += ',';
}
// Encode segment
let segment = this.encodeVLQ(mapping.generatedColumn - previousGeneratedColumn);
previousGeneratedColumn = mapping.generatedColumn;
segment += this.encodeVLQ(mapping.sourceIndex - previousSourceIndex);
previousSourceIndex = mapping.sourceIndex;
segment += this.encodeVLQ(mapping.originalLine - previousOriginalLine);
previousOriginalLine = mapping.originalLine;
segment += this.encodeVLQ(mapping.originalColumn - previousOriginalColumn);
previousOriginalColumn = mapping.originalColumn;
if (mapping.nameIndex !== undefined) {
segment += this.encodeVLQ(mapping.nameIndex - previousNameIndex);
previousNameIndex = mapping.nameIndex;
}
result += segment;
}
return result;
}
encodeVLQ(value) {
const VLQ_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
let encoded = '';
let vlq = value < 0 ? ((-value) << 1) + 1 : value << 1;
do {
let digit = vlq & 0x1f;
vlq >>>= 5;
if (vlq > 0) digit |= 0x20;
encoded += VLQ_BASE64[digit];
} while (vlq > 0);
return encoded;
}
}
Related Skills
- See meta-frameworks-overview for framework build setups
- See rendering-patterns for how bundles affect rendering
- See hydration-patterns for code splitting and hydration