build-pipelines-bundling
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBuild 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:
javascript
// 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历史上,浏览器无法处理现代JavaScript开发模式:
javascript
// 你的代码:多个带导入的小文件
// src/
// ├── index.js(导入App)
// ├── App.js(导入Header、Main、Footer)
// ├── Header.js(导入Logo、Nav)
// ├── Nav.js(导入NavLink)
// └── ... 100+ 个文件
// 问题1:HTTP/1.1 只能并行加载6个文件
// 100个文件 = 17次往返 = 速度慢
// 问题2:浏览器不支持import/export(直到ES模块出现)
import { Component } from './Component.js'; // 无法生效!
// 问题3:npm包存放在node_modules中
import React from 'react'; // 浏览器无法解析这个路径!
// 解决方案:将所有内容打包为更少的文件
// dist/
// ├── index.html
// ├── main.js(你的所有代码 + 依赖)
// └── main.cssThe 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源代码 构建流水线 输出
─────────────────────────────────────────────────────────────────────────────
src/ dist/
├── index.tsx ───┐ ┌─► index.html
├── App.tsx │ ┌──────────────────────────────┐ │
├── components/ ├───►│ 1. 解析导入路径 │ ├─► main.[hash].js
│ ├── Header.tsx │ │ 2. 转换代码(TS、JSX等) │ │
│ └── Button.tsx │ │ 3. 打包模块 │───►├─► vendor.[hash].js
├── styles/ │ │ 4. 优化代码(压缩等) │ │
│ └── main.css │ │ 5. 输出文件 │ ├─► main.[hash].css
└── assets/ │ └──────────────────────────────┘ │
└── logo.png ────┘ └─► assets/logo.[hash].pngBundler 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 |
| 打包工具 | 速度 | 配置复杂度 | 最佳适用场景 |
|---|---|---|---|
| Webpack | 较慢 | 复杂但功能强大 | 大型应用、遗留项目 |
| Vite | 开发环境快速 | 极简 | 现代应用、开发者体验优先 |
| esbuild | 最快 | 配置有限 | 构建步骤、类库开发 |
| Rollup | 中等 | 插件优先 | 类库开发 |
| Parcel | 快速 | 零配置 | 快速原型开发 |
| Turbopack | 快速 | 兼容Webpack | Next.js项目 |
Core Bundling Concepts
核心打包概念
Module Resolution
模块解析
How bundlers find your imports:
javascript
// 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打包工具如何找到你的导入项:
javascript
// 相对导入 - 从当前文件路径解析
import { Button } from './components/Button';
// → src/components/Button.js
// 裸导入 - 从node_modules解析
import React from 'react';
// → node_modules/react/index.js
// 别名导入 - 通过配置解析
import { api } from '@/lib/api';
// → src/lib/api.js(@映射到src/)
// 解析顺序(Node.js风格):
// 1. 精确路径:./Button.js
// 2. 添加扩展名:./Button → ./Button.js, ./Button.ts, ./Button.tsx
// 3. 索引文件:./Button → ./Button/index.js
// 4. Package.json的"main"或"exports"字段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打包工具会构建所有依赖的图谱:
入口:src/index.tsx
│
▼
┌─────────┐
│ index │
└────┬────┘
│ 导入
▼
┌─────────┐ ┌─────────┐
│ App │────►│ React │
└────┬────┘ └─────────┘
│ 导入
┌────┴────┐
▼ ▼
┌────────┐ ┌────────┐
│ Header │ │ Footer │
└───┬────┘ └────────┘
│ 导入
▼
┌─────────┐
│ Logo │
└─────────┘
// 打包工具遍历此图谱:
// 1. 从入口点开始
// 2. 解析文件,找到导入项
// 3. 递归处理每个导入项
// 4. 构建完整的依赖图谱
// 5. 按正确顺序输出打包文件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)不使用代码分割:
┌─────────────────────────────────────────┐
│ main.js (2MB) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌───────────┐ │
│ │首页 │ │关于页│ │博客 │ │ 控制台 │ │
│ └─────┘ └─────┘ └─────┘ └───────────┘ │
└─────────────────────────────────────────┘
用户访问 /home → 下载2MB(包含未使用的控制台代码)
使用代码分割:
┌──────────────┐
│ main.js (50KB)│ ← 核心应用、路由
└──────────────┘
│
├─► home.js (30KB) ← 访问/home时加载
├─► about.js (20KB) ← 访问/about时加载
├─► blog.js (40KB) ← 访问/blog时加载
└─► dashboard.js (500KB) ← 仅访问/dashboard时加载
用户访问 /home → 下载80KB(main + home)Split Strategies
分割策略
1. Route-Based Splitting
javascript
// 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 chunk2. Component-Based Splitting
javascript
// 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
javascript
// 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)1. 基于路由的分割
javascript
// React 懒加载
import { lazy, Suspense } from 'react';
// 每个路由成为单独的分块
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>
);
}
// 构建输出:
// main.js - 核心应用
// pages-Home-[hash].js - 首页分块
// pages-About-[hash].js - 关于页分块
// pages-Dashboard-[hash].js - 控制台分块2. 基于组件的分割
javascript
// 按需加载重型组件
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)}>显示图表</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart /> {/* 仅在需要时加载 */}
</Suspense>
)}
</div>
);
}3. 第三方依赖分割
javascript
// Webpack 配置
optimization: {
splitChunks: {
cacheGroups: {
// 将node_modules分离为vendor分块
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
// 分离大型类库
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
},
},
},
}
// 输出:
// main.js - 你的代码
// vendors.js - 所有node_modules代码
// react.js - react + react-dom(单独缓存)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入口分块:
- 应用的起始点
- 通常每个“页面”或入口点对应一个
异步分块:
- 由动态导入创建:import('./module')
- 按需加载
公共/共享分块:
- 被多个分块使用的代码
- 提取出来避免重复
第三方依赖分块:
- 来自node_modules的第三方代码
- 更改频率低 = 缓存效果更好Optimal Chunking
最优分块
javascript
// 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,
},
},
},
}javascript
// Webpack splitChunks配置
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000, // 最小分块大小(20KB)
maxSize: 244000, // 尽量保持在244KB以下
minChunks: 1, // 模块被共享的最小次数
maxAsyncRequests: 30, // 异步分块的最大并行请求数
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2, // 被使用2次及以上则分割
priority: -20,
reuseExistingChunk: true,
},
},
},
}Tree Shaking
Tree Shaking
Removing unused code from the bundle.
从包中移除未使用的代码。
How It Works
工作原理
javascript
// 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 removedjavascript
// utils.js - 导出多个函数
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 - 仅导入部分导出项
import { usedFunction, USED_CONSTANT } from './utils';
console.log(usedFunction(), USED_CONSTANT);
// Tree Shaking后:
// 包中仅包含usedFunction和USED_CONSTANT
// unusedFunction和UNUSED_CONSTANT被移除Requirements for Tree Shaking
Tree Shaking的要求
javascript
// ✓ 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
]
}javascript
// ✓ 有效:ES模块(静态结构)
import { specific } from 'library';
export function myFunction() {}
// ✗ 无效:CommonJS(动态结构)
const library = require('library');
module.exports = myFunction;
// ✗ 无效:动态导入特定导出项
const { specific } = await import('library'); // 可以对导入进行Tree Shaking
// 但类库内部必须使用ES模块
// 副作用:导入时会执行的代码
// package.json
{
"sideEffects": false // 所有文件都是纯函数,可安全进行Tree Shaking
}
// 或指定哪些文件有副作用:
{
"sideEffects": [
"*.css", // CSS导入有副作用
"./src/polyfills.js" // 此文件导入时会执行代码
]
}Minification
代码压缩
Reducing code size without changing behavior.
在不改变行为的前提下减小代码体积。
Techniques
技术手段
javascript
// 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)javascript
// 原始代码:
function calculateTotalPrice(items) {
let totalPrice = 0;
for (let i = 0; i < items.length; i++) {
totalPrice += items[i].price * items[i].quantity;
}
return totalPrice;
}
// 压缩后:
function calculateTotalPrice(e){let t=0;for(let l=0;l<e.length;l++)t+=e[l].price*e[l].quantity;return t}
// 应用的技术:
// 1. 移除空白字符
// 2. 缩短变量名(混淆)
// 3. 消除死代码
// 4. 常量折叠:1 + 2 → 3
// 5. 布尔值简化:!!x → x(在布尔上下文中)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 |
| 工具 | 速度 | 压缩效果 | 适用场景 |
|---|---|---|---|
| Terser | 较慢 | 最佳 | 生产环境构建 |
| esbuild | 最快 | 良好 | 开发环境、快速构建 |
| SWC | 非常快 | 良好 | Next.js、基于Rust的工具 |
| UglifyJS | 较慢 | 良好 | 遗留项目 |
Source Maps
Source Maps
Mapping minified code back to source for debugging.
javascript
// 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!将压缩后的代码映射回源代码以方便调试。
javascript
// 压缩代码(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..."
}
// 浏览器开发者工具:
// 显示带有有意义名称的原始源代码:
function throwValidationError(message) {
throw new Error("Invalid: " + message);
}
// 还有正确的行号!Source Map Types
Source Map类型
javascript
// 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 mapsjavascript
// Webpack devtool选项:
// 开发环境:
devtool: 'eval-source-map' // 快速重建,完整Source Map
devtool: 'eval-cheap-source-map' // 更快,仅行映射
// 生产环境:
devtool: 'source-map' // 完整Source Map(单独文件)
devtool: 'hidden-source-map' // 生成Source Map但不关联
devtool: false // 不生成Source MapAsset Handling
资源处理
Processing non-JavaScript assets.
处理非JavaScript资源。
Images
图片
javascript
// 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 hashjavascript
// Webpack(使用资源模块)
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于8KB则内联为Data URL
}
}
}
// Vite(自动处理)
import logo from './logo.png'; // 返回URL
import icon from './icon.svg?raw'; // 返回SVG内容
// 输出:
// 小图片 → 内联为Data URL
// 大图片 → 复制到dist目录并添加哈希CSS
CSS
javascript
// 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)javascript
// CSS处理流水线:
// 1. PostCSS(自动前缀、未来CSS语法)
// 2. CSS Modules(作用域类名)
// 3. 压缩(cssnano)
// 4. 提取(单独的.css文件)
// Vite自动处理这些
import styles from './Button.module.css';
function Button() {
return <button className={styles.button}>点击</button>;
}
// 输出:
// .button → .Button_button_x7h3j(带作用域)Content Hashing
内容哈希
Cache busting with content-based filenames.
javascript
// 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)使用基于内容的文件名实现缓存击穿。
javascript
// 不使用哈希:
// main.js - 浏览器会无限期缓存
// 更新代码 → 浏览器仍使用旧的缓存版本!
// 使用内容哈希:
// main.a1b2c3d4.js - 哈希基于内容
// 更新代码 → 新哈希 → 新文件名 → 浏览器获取新版本
// Webpack配置:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
}
// 优势:
// - 未更改的文件保持相同哈希 = 持续缓存
// - 更改的文件获得新哈希 = 下载新版本
// - 可设置长缓存头(不可变)Build Performance Optimization
构建性能优化
Caching
缓存
javascript
// Webpack persistent caching
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename], // Invalidate on config change
},
}
// First build: 30 seconds
// Subsequent builds: 5 seconds (cache hit)javascript
// Webpack持久化缓存
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename], // 配置更改时失效
},
}
// 首次构建:30秒
// 后续构建:5秒(缓存命中)Parallelization
并行化
javascript
// 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)javascript
// Webpack thread-loader处理耗时的加载器
{
test: /\.tsx?$/,
use: [
'thread-loader', // 在工作池运行
'babel-loader',
],
}
// esbuild/SWC默认并行(基于Rust/Go)Excluding node_modules
排除node_modules
javascript
// Don't transform node_modules (already compiled)
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
}javascript
// 不转换node_modules(已编译)
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
}Build Analysis
构建分析
Understanding your bundle contents.
bash
undefined理解包的内容。
bash
undefinedWebpack Bundle Analyzer
Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
Add to webpack config:
添加到Webpack配置:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
plugins: [
new BundleAnalyzerPlugin()
]
Generates interactive treemap of bundle contents
生成包内容的交互式树形图
undefinedundefinedsource-map-explorer
source-map-explorer
npx source-map-explorer dist/main.js
npx source-map-explorer dist/main.js
Vite
Vite
npx vite-bundle-visualizer
---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 buildjavascript
// 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
javascript
// 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
javascript
// 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
javascript
// 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
javascript
// 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
javascript
// 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 includedStep 6: Concatenate Modules
javascript
// 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
javascript
// 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让我们追踪运行时的具体过程:
npm run buildjavascript
// 你的源文件:
// 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';
}步骤1:解析入口点
javascript
// 打包工具将index.js解析为AST(抽象语法树)
{
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' } }]
},
// ... AST的其余部分
]
}步骤2:解析依赖
javascript
// 对每个导入项,解析为实际文件路径
// './utils.js'
// → 当前目录:/project/src/
// → 解析结果:/project/src/utils.js ✓
// 'react'
// → 不是相对路径,检查node_modules
// → /project/node_modules/react/package.json
// → "main": "index.js"
// → 解析结果:/project/node_modules/react/index.js ✓步骤3:构建依赖图谱
javascript
// 图谱结构(简化版)
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及其依赖
};步骤4:转换代码
javascript
// 每个文件通过加载器/插件处理
// TypeScript → JavaScript
// TSX/JSX → React.createElement调用
// 现代JS → 兼容JS(Babel)
// 输入(TSX):
const Button: React.FC = () => <button>Click</button>;
// 输出(JS):
const Button = () => React.createElement("button", null, "Click");步骤5:Tree Shaking
javascript
// 分析实际使用的代码
// utils.js导出:['greet', 'unused']
// index.js从utils导入:['greet']
//
// 'unused'从未被导入
// 标记为移除
// Tree Shaking后,仅包含'greet'步骤6:合并模块
javascript
// 将所有模块合并为包格式
// IIFE包装器(立即调用函数表达式)
(function(modules) {
// 模块缓存
var installedModules = {};
// 模块加载器
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;
}
// 从入口点开始
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 + '!';
};
// 注意:unused()已消失!
},
// ... react模块
});步骤7:压缩
javascript
// Terser/esbuild压缩
// 压缩前:
function greet(name) {
return 'Hello, ' + name + '!';
}
// 压缩后:
function greet(n){return"Hello, "+n+"!"}
// 或进一步优化:
const greet=n=>"Hello, "+n+"!";步骤8:带哈希输出
dist/
├── index.html
├── main.7f8a2b3c.js ← 基于内容的哈希
├── main.7f8a2b3c.js.map ← Source Map
└── index.html ← 引用带哈希的文件Module Formats: A History Lesson
模块格式:历史回顾
Understanding why we have multiple module systems:
javascript
// 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!理解为什么会有多种模块系统:
javascript
// 1. 无模块(2009年前)
// 所有内容都在全局作用域
// script.js
var myApp = {};
myApp.utils = {
greet: function(name) { return 'Hello, ' + name; }
};
// 问题:全局命名空间污染,依赖顺序至关重要
// 2. COMMONJS(2009,Node.js)
// 同步,为服务器设计
// utils.js
module.exports.greet = function(name) { return 'Hello, ' + name; };
// index.js
const { greet } = require('./utils');
// 问题:同步require()在浏览器中无效
// 3. AMD - 异步模块定义(2011,RequireJS)
// 异步加载适用于浏览器
define(['./utils'], function(utils) {
console.log(utils.greet('World'));
});
// 问题:冗长,非标准
// 4. UMD - 通用模块定义(2014)
// 适用于所有环境(浏览器全局、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); // 浏览器全局
}
}(this, function(dep) {
return { greet: function(name) { return 'Hello, ' + name; } };
}));
// 问题:每个文件都需要复杂的包装器
// 5. ES模块(2015,ES6)
// 官方JavaScript标准
// 静态结构支持Tree Shaking
export function greet(name) { return 'Hello, ' + name; }
import { greet } from './utils.js';
// 现在浏览器和Node.js都原生支持了!Why Vite is Fast: Native ES Modules
为什么Vite这么快:原生ES模块
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)
// ...传统打包工具(Webpack)与Vite的对比:
WEBPACK开发环境:
你的代码 Webpack 浏览器
────────────────────────────────────────────────────────────────
src/
├── index.js ─┐
├── App.js ├─► 打包所有文件 ─► bundle.js ─────► 加载包
├── Header.ts │
└── 100+更多文件 ─┘
时间:解析 + 转换 + 打包所有文件 = 10-30秒
更改1个文件 → 重新打包所有文件 = 2-5秒
VITE开发环境:
你的代码 Vite 浏览器
────────────────────────────────────────────────────────────────
src/
├── index.js ──────────────────────────────────► 按需请求每个
├── App.js ─► 按需转换 ─► 直接提供 ──► 文件,通过原生ES
├── Header.ts 服务 模块导入
└── 100+更多文件 ─► (仅在请求时转换)
时间:仅转换请求的文件 = 300-500ms
更改1个文件 → 仅转换该文件 = <100ms
为什么有效:
<!-- 浏览器通过原生ES模块请求 -->
<script type="module" src="/src/index.js"></script>
// index.js(直接提供,未打包)
import { App } from './App.js'; // 浏览器发起另一个请求
// 浏览器网络面板:
// GET /src/index.js
// GET /src/App.js (来自index.js的导入)
// GET /src/Header.ts (来自App.js的导入)
// ...Dynamic Imports: How Code Splitting Works
动态导入:代码分割的原理
The magic behind :
import()javascript
// 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 demandWhat the bundler does:
javascript
// 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 rendersimport()javascript
// 静态导入(构建时解析)
import { heavy } from './heavy.js';
// 始终包含在主包中
// 动态导入(运行时解析)
const heavy = await import('./heavy.js');
// 创建单独的分块,按需加载打包工具的处理:
javascript
// 你的代码:
button.onclick = async () => {
const { HeavyComponent } = await import('./HeavyComponent.js');
render(HeavyComponent);
};
// 打包工具输出:
// 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(单独文件)
(self["webpackChunk"] = self["webpackChunk"] || []).push([
["HeavyComponent"],
{
"./HeavyComponent.js": (module, exports) => {
exports.HeavyComponent = function() { /* ... */ };
}
}
]);
// 运行时:
// 1. 用户点击按钮
// 2. __webpack_require__.e创建<script>标签
// 3. 浏览器获取HeavyComponent.a1b2c3.chunk.js
// 4. 脚本执行并注册模块
// 5. Promise解析并返回导出项
// 6. 组件渲染Magic Comments: Controlling Chunk Behavior
魔法注释:控制分块行为
javascript
// 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, weakjavascript
// 为分块命名(用于调试/分析)
const Admin = () => import(/* webpackChunkName: "admin" */ './Admin');
// 输出:admin.a1b2c3.js
// 预取(主内容加载后在后台加载)
const Settings = () => import(/* webpackPrefetch: true */ './Settings');
// 添加:<link rel="prefetch" href="settings.chunk.js">
// 预加载(立即加载,很快需要)
const Critical = () => import(/* webpackPreload: true */ './Critical');
// 添加:<link rel="preload" href="critical.chunk.js">
// 控制加载模式
const Lib = () => import(/* webpackMode: "lazy-once" */ `./lib/${name}`);
// 模式:lazy(默认)、lazy-once、eager、weakTree Shaking Deep Dive: Static Analysis
Tree Shaking深入理解:静态分析
Why ES modules can be tree shaken but CommonJS cannot:
javascript
// 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 runtimeThe side effects problem:
javascript
// 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为什么ES模块可以Tree Shaking而CommonJS不行:
javascript
// ES模块 - 静态结构
// 导入/导出在解析时确定(执行前)
import { a, b } from './utils'; // 始终导入a和b
export { x, y }; // 始终导出x和y
// 打包工具可以静态分析:
// "此文件导出x和y"
// "此文件导入a和b"
// 无需执行代码!
// CommonJS - 动态结构
// 导入/导出在运行时确定
const utils = require('./utils'); // 导出什么?取决于运行时
module.exports = something; // 'something'是什么?取决于运行时
// 破坏静态分析的示例:
if (condition) {
module.exports = optionA;
} else {
module.exports = optionB;
}
const name = 'foo';
module.exports[name] = value; // 动态属性
require('./' + dynamicPath); // 路径运行时才知道副作用问题:
javascript
// utils.js
console.log('Utils loaded!'); // 副作用 - 导入时执行
export function used() { return 'used'; }
export function unused() { return 'unused'; }
// 即使'unused'从未被调用,我们仍导入了该文件
// console.log会执行!
// 打包工具无法完全移除该文件
// 解决方案:标记为无副作用
// package.json
{
"sideEffects": false
}
// 现在打包工具知道:如果没有导出项被使用,就跳过整个文件Scope Hoisting: Module Concatenation
作用域提升:模块合并
Modern bundlers can merge modules for smaller bundles:
javascript
// 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)现代打包工具可以合并模块以减小包体积:
javascript
// 无作用域提升:
// 每个模块都被函数包装
(function(modules) {
function __require__(id) { /* ... */ }
// 模块0
(function(module, exports, __require__) {
var utils = __require__(1);
console.log(utils.greet('World'));
});
// 模块1
(function(module, exports) {
exports.greet = function(name) { return 'Hello, ' + name; };
});
})();
// 有作用域提升(模块合并):
// 模块合并到一个作用域
(function() {
// 来自utils.js(内联)
function greet(name) { return 'Hello, ' + name; }
// 来自index.js
console.log(greet('World'));
})();
// 优势:
// - 包体积更小(无模块包装器)
// - 执行更快(无函数调用开销)
// - 压缩效果更好(变量可一起重命名)Chunk Splitting Algorithms
分块分割算法
How bundlers decide what goes in which chunk:
javascript
// 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打包工具如何决定哪些内容进入哪个分块:
javascript
// WEBPACK的SPLITCHUNKS算法(简化版)
// 对每个模块,跟踪:
// - 包含它的分块
// - 模块大小
// - 是否来自node_modules
// 决策过程:
for (module of allModules) {
const chunks = module.usedInChunks;
// 规则1:被多个分块共享?
if (chunks.length >= minChunks) {
// 候选提取
// 规则2:大到值得单独请求?
if (module.size >= minSize) {
// 规则3:不会创建过多并行请求?
if (currentAsyncRequests < maxAsyncRequests) {
// 提取到公共分块!
createCommonChunk(module);
}
}
}
}
// 结果:
// 共享代码在公共分块
// 路由特定代码在路由分块
// 第三方依赖代码在vendor分块
// 在缓存效率与请求数量之间取得平衡Long-Term Caching Strategy
长期缓存策略
Optimal caching requires careful chunk design:
javascript
// 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)最优缓存需要精心设计分块:
javascript
// 问题:任何更改都会使整个包失效
// main.js包含:应用代码 + 第三方依赖代码
// 更改应用代码的一行 → 新哈希 → 用户重新下载所有内容
// 解决方案:按更改频率分离
// 1. 运行时分块(极小,很少更改)
output: {
filename: '[name].[contenthash].js',
},
optimization: {
runtimeChunk: 'single', // Webpack运行时代码在单独文件
}
// 2. 第三方依赖分块(中等,npm更新时更改)
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
}
// 3. 应用分块(频繁更改)
// 默认行为 - 你的代码
// 结果:
// runtime.a1b1b1.js (1KB) - 几乎从不更改
// vendors.c2c2c2.js (200KB) - npm更新时更改
// main.d3d3d3.js (50KB) - 每次部署时更改
// page-home.e4e4e4.js (10KB) - 首页更改时更改
// 用户已缓存vendors.js
// 你部署了首页的更改
// 用户仅需下载: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缓慢操作(按成本排序):
1. TYPESCRIPT编译
- 类型检查开销大
- 解决方案:使用transpileOnly,单独运行tsc
2. BABEL转换
- 解析 + 转换每个文件
- 解决方案:排除node_modules,缓存结果
3. 压缩
- 复杂的AST分析
- 解决方案:使用esbuild/SWC代替Terser
4. Source Map生成
- 创建映射数据
- 解决方案:开发环境使用更简单的Source Map类型
5. 大型依赖
- 解析庞大的node_modules
- 解决方案:外部化,使用预打包版本
优化清单:
□ 启用持久化缓存(cache: { type: 'filesystem' })
□ 将node_modules排除在babel/typescript处理之外
□ 使用thread-loader进行并行处理
□ 使用esbuild进行压缩
□ 开发环境使用'eval-source-map'
□ 分析包以找出意外的大型依赖The Future: VoidZero and the Unified Rust Toolchain
未来:VoidZero与统一Rust工具链
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如今的JavaScript工具链在许多工具之间碎片化:
当前状态(2024):
解析: Babel、TypeScript、SWC、esbuild(各有自己的解析器)
转换: Babel、SWC、esbuild、TypeScript(重复工作)
打包: Webpack、Rollup、esbuild、Parcel(不同算法)
压缩: Terser、esbuild、SWC(又有更多解析器)
lint: ESLint(慢,基于JavaScript)
格式化: Prettier(慢,基于JavaScript)
类型检查: TypeScript(难以并行化)
问题:
1. 每个工具都单独解析代码 = 冗余工作
2. 不同工具有不同的bug/行为
3. JavaScript工具本质上很慢(单线程、解释型)
4. 跨工具的配置复杂
5. 错误消息和源位置不一致VoidZero's Vision
VoidZero的愿景
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 messagesVoidZero由Evan You(Vue.js和Vite的创建者)创立,正在构建基于Rust的统一JavaScript工具链:
VOIDZERO统一工具链:
┌─────────────────────────────────────────┐
│ OXC(核心) │
│ 基于Rust的JavaScript/TypeScript │
│ 解析器、Lint、转换器、解析器 │
└─────────────────┬───────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Rolldown │ │ oxc-linter │ │ oxc-transform │
│ 打包工具 │ │ (ESLint替代) │ │ (Babel替代) │
└────────┬────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Vite │
│ 开发 + 构建 │
└─────────────────┘
共享优势:
- 所有操作使用单个解析器
- 一致的AST表示
- 原生速度(Rust、并行化)
- 统一配置
- 连贯的错误消息Oxc (Oxidation Compiler)
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 everythingOxc是一组用Rust编写的高性能JavaScript/TypeScript工具:
OXC组件:
┌─────────────────────────────────────────────────────────────────┐
│ oxc_parser │
│ - 解析JavaScript、TypeScript、JSX、TSX │
│ - 比SWC快3倍,比Babel快5倍 │
│ - 为所有下游工具生成相同的AST │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ oxc_linter │ │ oxc_transformer│ │ oxc_minifier │
│ │ │ │ │ │
│ 比ESLint快50-100倍│ │ TypeScript →JS │ │ 压缩输出代码 │
│ │ │ JSX → JS │ │ │
└───────────────┘ └───────────────┘ └───────────────┘
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ oxc_resolver │ │ oxc_sourcemap │ │ oxc_prettier │
│ │ │ │ │ (计划中) │
│ 模块解析 │ │ Source Map生成 │ │ 代码格式化 │
│ │ │ │ │ │
└───────────────┘ └───────────────┘ └───────────────┘性能对比:
解析基准测试(大型代码库):
工具 时间 相对速度
────────────────────────────────────
oxc_parser 45ms 1.0x(基准)
swc_parser 150ms 3.3倍慢
esbuild 180ms 4.0倍慢
babel 900ms 20倍慢
typescript 1200ms 26倍慢
Lint基准测试(ESLint规则):
工具 时间
────────────────────────────────────
oxc_linter 0.5s
ESLint 50s (100倍慢)
为什么这么快?
1. Rust:无垃圾回收暂停
2. 并行化:使用所有CPU核心
3. 零拷贝解析:最小内存分配
4. SIMD:CPU向量指令用于字符串操作
5. 单遍:解析一次,分析所有内容Rolldown: The Rollup Replacement
Rolldown:Rollup的替代者
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 stepsRolldown是基于Rust的打包工具,设计为Rollup的直接替代:
ROLLDOWN VS ROLLUP:
┌─────────────────────────────────────────────────────────────────┐
│ ROLLUP │
│ │
│ 语言: JavaScript │
│ 速度: 中等(单线程JS) │
│ 插件: 丰富的生态系统(成熟) │
│ 输出: 出色的Tree Shaking和代码质量 │
│ 适用场景: 类库、ES模块输出 │
│ │
│ 局限性: │
│ - 无法并行化(JavaScript) │
│ - 大型项目 = 构建缓慢 │
│ - 对大代码库内存占用高 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ROLLDOWN │
│ │
│ 语言: Rust │
│ 速度: 比Rollup快10-30倍 │
│ 插件: 兼容Rollup(大多数插件可用) │
│ 输出: 与Rollup相同质量 │
│ 适用场景: 应用 + 类库 │
│ │
│ 优势: │
│ - 完全并行化(Rust + Rayon) │
│ - 基于Oxc(共享解析器/解析逻辑) │
│ - esbuild级速度 + Rollup级输出质量 │
│ - 原生代码分割 │
│ - 内置转换(无需单独Babel) │
└─────────────────────────────────────────────────────────────────┘Rolldown架构:
┌─────────────────────────────────────────────────────────────────┐
│ Rolldown构建流程 │
└─────────────────────────────────────────────────────────────────┘
入口点
│
▼
┌─────────────────┐
│ oxc_resolver │ ← 解析所有导入(并行化)
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_parser │ ← 解析所有文件(并行化)
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_transformer │ ← 转换TS/JSX(并行化)
└────────┬────────┘
│
▼
┌─────────────────┐
│ 链接与打包 │ ← 作用域提升、Tree Shaking
└────────┬────────┘
│
▼
┌─────────────────┐
│ oxc_minifier │ ← 压缩输出(并行化)
└────────┬────────┘
│
▼
输出分块
所有步骤共享相同的AST表示
步骤之间无需重新解析How Vite Will Use Rolldown
Vite将如何使用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 buildsVite目前在开发和生产环境使用不同工具:
VITE当前状态(2024):
开发环境:
- 原生ES模块(快速)
- esbuild进行依赖预打包
- esbuild进行TypeScript/JSX转换
生产环境:
- Rollup进行打包
- Terser或esbuild进行压缩
问题:不同工具 = 行为差异
- 开发和生产环境可能有细微差异
- 配置分散在不同工具中
- 无法在模式之间共享优化
VITE未来(使用Rolldown):
开发环境:
- 原生ES模块(快速)
- Rolldown进行依赖预打包
- Oxc进行TypeScript/JSX转换
生产环境:
- Rolldown进行打包
- Oxc压缩器进行压缩
优势:开发和生产环境使用相同工具
- 行为一致
- 统一配置
- 模式之间共享缓存
- 生产构建更快The Complete VoidZero Ecosystem
完整的VoidZero生态系统
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────┘┌─────────────────────────────────────────────────────────────────┐
│ VOIDZERO生态系统 │
└─────────────────────────────────────────────────────────────────┘
用户面向:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Vite │ │ Vitest │ │ VitePress │
│ 构建工具 │ │ 测试运行器 │ │ 文档生成器 │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
基础设施:
│
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Rolldown │ │ oxc-linter │ │ Lightning │
│ 打包工具 │ │ Linter │ │ CSS(CSS处理) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└──────────────────────┼──────────────────────┘
│
核心基础:
│
▼
┌─────────────────────┐
│ OXC │
│ 解析器、解析器、 │
│ 转换器等。 │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ NAPI-RS │
│ Rust ↔ Node.js │
│ 绑定 │
└─────────────────────┘Why Rust for JavaScript Tooling?
为什么用Rust做JavaScript工具链?
javascript
// 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 SIMDjavascript
// JavaScript工具链的局限性:
// 1. 单线程
// JavaScript只能使用一个CPU核心进行计算
for (const file of files) {
parse(file); // 顺序执行,无法并行化
}
// 2. 垃圾回收暂停
// GC会定期暂停所有操作
parseHugeFile(); // 分配内存
// ... GC暂停 ... 构建冻结50-200ms
// 3. 解释型开销
// 即使有JIT,也比原生代码慢
const ast = parse(source); // 比原生慢约10倍
// Rust的优势:
// 1. 真正的并行化
// Rust可以安全使用所有CPU核心
files.par_iter().map(|file| parse(file)).collect() // 8核机器上快8倍
// 2. 无GC暂停
// 内存在编译时管理,确定性
// 构建期间无随机冻结
// 3. 原生速度
// 编译为机器码
// 解析/转换速度快10-100倍
// 4. 内存安全
// Rust的所有权系统防止:
// - 内存泄漏
// - 释放后使用
// - 数据竞争
// 且不牺牲性能
// 5. SIMD/向量化
// Rust可以使用CPU向量指令
// 字符串操作速度快4-8倍Migration Path: Rollup to Rolldown
迁移路径:从Rollup到Rolldown
Rolldown aims for Rollup compatibility:
javascript
// 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 differencesRolldown目标是兼容Rollup:
javascript
// 当前ROLLUP配置:
// 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(),
],
});
// 未来ROLLDOWN配置(基本兼容):
// rolldown.config.js
import { defineConfig } from 'rolldown';
import resolve from '@rollup/plugin-node-resolve'; // 可用!
import commonjs from '@rollup/plugin-commonjs'; // 可用!
// typescript()不需要 - Rolldown通过Oxc原生处理TS
export default defineConfig({
input: 'src/index.ts',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
resolve(),
commonjs(),
// 内置:TypeScript、JSX、压缩
],
});
// 兼容性状态:
// ✓ 大多数Rollup插件可用(通过兼容层)
// ✓ 相同配置格式
// ✓ 相同输出质量
// ✗ 部分插件需要更新(底层AST操作)
// ✗ 插件钩子有一些差异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)┌────────────┬──────────┬──────────┬──────────┬──────────┬──────────┐
│ │ Webpack │ Rollup │ esbuild │ Rolldown │ Turbopack│
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 语言 │ JS │ JS │ Go │ Rust │ Rust │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 速度 │ 较慢 │ 中等 │ 快速 │ 快速 │ 快速 │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ Tree Shake │ 良好 │ 最佳 │ 良好 │ 最佳 │ 良好 │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 代码分割 │ 最佳 │ 良好 │ 基础 │ 良好 │ 良好 │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 插件 │ 庞大 │ 大型 │ 有限 │ 兼容Rollup* │ 兼容Webpack* │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 配置 │ 复杂 │ 简单 │ 简单 │ 简单 │ 底层级│
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 成熟度 │ 成熟 │ 成熟 │ 成熟 │ 预览版 │ 测试版 │
├────────────┼──────────┼──────────┼──────────┼──────────┼──────────┤
│ 使用者 │ 遗留项目 │ Vite、类库 │ Vite、许多项目 │ Vite(未来) │ Next.js │
└────────────┴──────────┴──────────┴──────────┴──────────┴──────────┘
* Rolldown:兼容Rollup插件
* Turbopack:兼容Webpack插件(计划中)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 tools2012: Webpack诞生(JS,功能全面但慢)
└─► 主导构建工具多年
2017: Parcel诞生(JS,零配置)
└─► 普及了零配置打包
2018: Rollup流行(JS,专注Tree Shaking)
└─► 成为类库标准
2020: esbuild发布(Go,快100倍)
└─► 证明原生速度可行
2021: Vite 2.0发布(esbuild + Rollup)
└─► 结合快速开发环境与高质量生产构建
2022: Turbopack发布(Rust,Vercel)
└─► Next.js专用,Webpack继任者
2023: Oxc开发加速(Rust,VoidZero)
└─► 统一工具链愿景浮现
2024: Rolldown预览版(Rust,VoidZero)
└─► Vite的Rollup替代者
未来:统一工具链
└─► 一个解析器、一个解析器、一个转换器
└─► 开发到生产行为一致
└─► 比当前工具快10-100倍When to Use What (2024 Recommendations)
2024年工具选择建议
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)构建类库:
├─► 中小型:Rollup(成熟,出色的Tree Shaking)
├─► 大型:稳定后用Rolldown,或用esbuild追求速度
└─► TypeScript:tsup(esbuild包装器)或unbuild(Rollup包装器)
构建应用:
├─► 新项目:Vite(最佳开发者体验,使用Rollup + esbuild)
├─► Next.js:Turbopack(实验性)或Webpack(稳定)
├─► 大型遗留项目:Webpack(生态系统、稳定性)
└─► 性能关键:考虑稳定后的Vite + Rolldown
构建单仓库:
├─► Turborepo + Vite(缓存 + 快速构建)
├─► Nx + 任何打包工具(任务编排)
└─► pnpm工作区 + Vite(简单、快速)
Lint:
├─► 当前:ESLint(全面的规则)
├─► 未来:oxc-linter(规则覆盖足够时)
└─► 备选:Biome(Rust编写,快速,集成化)
格式化:
├─► 当前:Prettier(事实上的标准)
├─► 备选:Biome(更快,集成lint+format)
└─► 未来:Oxc prettier(计划中)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.
实现说明:以下模式和代码示例代表了一种经过验证的构建打包工具集成方法。不同打包工具的插件API不同——Rollup/Vite使用基于钩子的系统,Webpack使用tapable,esbuild有更简单的API。这里展示的方向聚焦于Rollup/Vite模式(现代框架最常用)。根据你需要支持的打包工具以及是构建插件还是自定义打包工具进行调整。
Writing Bundler Plugins
编写打包工具插件
javascript
// 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>`
);
},
};
}javascript
// VITE/ROLLUP插件结构
function myPlugin(options = {}) {
return {
name: 'my-plugin',
// 配置解析时调用
configResolved(config) {
this.config = config;
},
// 转换源代码
transform(code, id) {
if (!id.endsWith('.special')) return null;
// 转换代码
const transformed = processSpecialFile(code);
return {
code: transformed,
map: null, // Source Map
};
},
// 解析导入路径
resolveId(source, importer) {
if (source.startsWith('virtual:')) {
return '\0' + source; // 前缀\0表示虚拟模块
}
return null; // 让其他插件处理
},
// 加载虚拟模块
load(id) {
if (id.startsWith('\0virtual:')) {
return `export default ${JSON.stringify(getVirtualContent(id))}`;
}
return null;
},
// 构建钩子
buildStart() {
console.log('构建开始...');
},
buildEnd(error) {
if (error) console.error('构建失败:', error);
},
// 生成包输出
generateBundle(options, bundle) {
// 修改或添加到包
this.emitFile({
type: 'asset',
fileName: 'manifest.json',
source: JSON.stringify(generateManifest(bundle)),
});
},
};
}
// Vite特定钩子
function vitePlugin() {
return {
name: 'vite-specific',
// 开发服务器中间件
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/__my-plugin') {
res.end(JSON.stringify({ status: 'ok' }));
return;
}
next();
});
},
// HMR处理
handleHotUpdate({ file, server, modules }) {
if (file.endsWith('.custom')) {
// 自定义HMR逻辑
server.ws.send({
type: 'custom',
event: 'custom-update',
data: { file },
});
return []; // 不执行常规HMR
}
},
// 转换index.html
transformIndexHtml(html) {
return html.replace(
'</head>',
`<script>window.__VERSION__="${Date.now()}"</script></head>`
);
},
};
}Implementing Code Splitting Logic
实现代码分割逻辑
javascript
// 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 [];
}
}javascript
// 自定义代码分割策略
class ChunkSplitter {
constructor(options = {}) {
this.options = {
minChunkSize: options.minChunkSize || 20000,
maxChunkSize: options.maxChunkSize || 250000,
vendorPattern: options.vendorPattern || /node_modules/,
};
this.chunks = new Map();
}
// 分析模块图谱
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: [],
});
}
// 构建反向边
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;
}
// 确定分块
splitChunks(graph, entries) {
const chunks = [];
// 创建第三方依赖分块
const vendorModules = [...graph.nodes.values()]
.filter(n => n.isVendor);
if (vendorModules.length > 0) {
chunks.push({
name: 'vendor',
modules: vendorModules.map(n => n.id),
});
}
// 为每个入口创建分块
for (const entry of entries) {
const reachable = this.getReachableModules(graph, entry);
const nonVendor = reachable.filter(id => !graph.nodes.get(id)?.isVendor);
// 分割大分块
const subChunks = this.splitBySize(nonVendor, graph);
chunks.push(...subChunks.map((mods, i) => ({
name: `${entry}-${i}`,
modules: mods,
})));
}
// 查找共享分块
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) {
// 查找被多个分块使用的模块
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);
}
}
// 提取频繁共享的模块
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
构建模块转换器
javascript
// 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 };
}javascript
// 基于AST的模块转换器
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);
// 转换导入
walk.simple(ast, {
ImportDeclaration(node) {
const source = node.source.value;
// 解析裸导入
if (!source.startsWith('.') && !source.startsWith('/')) {
const resolved = resolveNodeModule(source);
s.overwrite(
node.source.start,
node.source.end,
`"${resolved}"`
);
}
},
// 转换动态导入
ImportExpression(node) {
const source = node.source;
if (source.type === 'Literal') {
const resolved = resolvePath(source.value);
s.overwrite(source.start, source.end, `"${resolved}"`);
}
},
// 转换导出以支持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 }),
};
}
}
// 导入分析
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
javascript
// 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;
}
}javascript
// Source Map生成器
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() {
// 排序映射
this.mappings.sort((a, b) =>
a.generatedLine - b.generatedLine ||
a.generatedColumn - b.generatedColumn
);
// 将映射编码为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];
// 新行
while (previousGeneratedLine < mapping.generatedLine) {
result += ';';
previousGeneratedLine++;
previousGeneratedColumn = 0;
}
if (i > 0 && this.mappings[i - 1].generatedLine === mapping.generatedLine) {
result += ',';
}
// 编码段
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
- 查看meta-frameworks-overview了解框架构建设置
- 查看rendering-patterns了解包如何影响渲染
- 查看hydration-patterns了解代码分割与水合