esbuild 高级特性 #
代码分割 #
代码分割允许将代码拆分成多个文件,实现按需加载和缓存优化。
基本概念 #
text
┌─────────────────────────────────────────────────────────────┐
│ 代码分割流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 入口A │ │ 入口B │ │ 入口C │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 共享模块 │ │
│ └─────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ chunk-A │ │ chunk-B │ │ vendor │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
动态导入 #
使用 import() 语法实现动态加载:
javascript
// src/index.js
document.getElementById('load-chart').addEventListener('click', async () => {
const { createChart } = await import('./chart.js');
createChart(document.getElementById('chart'));
});
document.getElementById('load-editor').addEventListener('click', async () => {
const { createEditor } = await import('./editor.js');
createEditor(document.getElementById('editor'));
});
javascript
// src/chart.js
export function createChart(container) {
console.log('Chart loaded');
}
// src/editor.js
export function createEditor(container) {
console.log('Editor loaded');
}
构建配置:
javascript
const esbuild = require('esbuild');
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
format: 'esm',
splitting: true,
});
输出结果:
text
dist/
├── index.js # 主入口
├── chart-[hash].js # 动态加载的 chart 模块
└── editor-[hash].js # 动态加载的 editor 模块
多入口代码分割 #
javascript
esbuild.build({
entryPoints: {
main: 'src/main.js',
admin: 'src/admin.js',
settings: 'src/settings.js',
},
bundle: true,
outdir: 'dist',
format: 'esm',
splitting: true,
});
共享模块自动提取:
javascript
// src/main.js
import { formatDate } from './utils.js';
import { Button } from './components.js';
// src/admin.js
import { formatDate } from './utils.js';
import { Modal } from './components.js';
// src/settings.js
import { formatDate } from './utils.js';
// 构建后 utils.js 会被提取为共享 chunk
手动代码分割 #
使用 entryNames 和 chunkNames 控制输出:
javascript
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
format: 'esm',
splitting: true,
entryNames: '[dir]/[name]-[hash]',
chunkNames: 'chunks/[name]-[hash]',
});
代码分割策略 #
1. 路由级别分割 #
javascript
// router.js
const routes = {
home: () => import('./pages/home.js'),
about: () => import('./pages/about.js'),
dashboard: () => import('./pages/dashboard.js'),
settings: () => import('./pages/settings.js'),
};
async function loadPage(route) {
const module = await routes[route]();
return module.default;
}
2. 功能模块分割 #
javascript
// 按功能分割
const features = {
analytics: () => import('./features/analytics.js'),
chat: () => import('./features/chat.js'),
notifications: () => import('./features/notifications.js'),
};
async function enableFeature(name) {
if (features[name]) {
const module = await features[name]();
module.init();
}
}
3. 第三方库分割 #
javascript
// 手动分离第三方库
// src/vendor.js
export { debounce } from 'lodash-es';
export { format } from 'date-fns';
export { v4 as uuid } from 'uuid';
// src/index.js
import { debounce, format, uuid } from './vendor.js';
// 或者使用 external 配合单独打包
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
external: ['lodash-es', 'date-fns', 'uuid'],
});
Tree-shaking #
Tree-shaking 是消除未使用代码的优化技术。
工作原理 #
text
┌─────────────────────────────────────────────────────────────┐
│ Tree-shaking 流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 源代码 │
│ ┌────────────────────────────────────────┐ │
│ │ export function used() { ... } │ │
│ │ export function unused() { ... } │ │
│ │ export function alsoUnused() { ... } │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 静态分析 │
│ ┌────────────────────────────────────────┐ │
│ │ import { used } from './module.js' │ │
│ │ // 只使用了 used 函数 │ │
│ └────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 打包结果 │
│ ┌────────────────────────────────────────┐ │
│ │ function used() { ... } │ │
│ │ // unused 和 alsoUnused 被移除 │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
基本示例 #
javascript
// utils.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
export function divide(a, b) {
return a / b;
}
// main.js
import { add } from './utils.js';
console.log(add(1, 2));
// 打包后只有 add 函数,其他函数被移除
副作用标记 #
在 package.json 中标记副作用:
json
{
"name": "my-library",
"sideEffects": false
}
或指定有副作用的文件:
json
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
纯函数注释 #
使用 /* @__PURE__ */ 标记:
javascript
// 这个调用会被移除(如果结果未使用)
const result = /* @__PURE__ */ expensiveComputation();
// 这个类会被移除(如果未使用)
export const MyHelper = /* @__PURE__ */ class {
helper() {}
};
// 构造函数调用
const instance = /* @__PURE__ */ new ExpensiveClass();
Tree-shaking 最佳实践 #
1. 使用 ES 模块 #
javascript
// ✅ 好 - ES 模块支持 Tree-shaking
import { debounce } from 'lodash-es';
// ❌ 差 - CommonJS 不支持 Tree-shaking
const { debounce } = require('lodash');
2. 具名导出 #
javascript
// ✅ 好 - 具名导出
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// ❌ 差 - 默认导出对象
export default {
add(a, b) { return a + b; },
subtract(a, b) { return a - b; },
};
3. 避免副作用 #
javascript
// ❌ 有副作用 - 模块加载时执行
export const data = fetchData();
// ✅ 无副作用 - 使用时才执行
export function getData() {
return fetchData();
}
增量构建 #
增量构建只重新编译变化的部分,大幅提升构建速度。
rebuild API #
javascript
const esbuild = require('esbuild');
async function setup() {
const ctx = await esbuild.context({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
});
// 首次构建
const result1 = await ctx.rebuild();
console.log('First build done');
// 文件变化后再次构建(更快)
const result2 = await ctx.rebuild();
console.log('Second build done');
// 清理
await ctx.dispose();
}
setup();
Watch 模式 #
javascript
const ctx = await esbuild.context({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
sourcemap: true,
});
await ctx.watch();
console.log('Watching for changes...');
// 停止监听
process.on('SIGINT', async () => {
await ctx.dispose();
process.exit(0);
});
增量构建与插件 #
javascript
const cache = new Map();
const cachingPlugin = {
name: 'cache',
setup(build) {
build.onLoad({ filter: /\.js$/ }, async (args) => {
const fs = require('fs').promises;
const stat = await fs.stat(args.path);
const key = `${args.path}:${stat.mtimeMs}`;
if (cache.has(key)) {
console.log('Cache hit:', args.path);
return cache.get(key);
}
const contents = await fs.readFile(args.path, 'utf8');
const result = { contents, loader: 'js' };
cache.set(key, result);
return result;
});
},
};
服务端渲染 (SSR) #
esbuild 可以用于服务端渲染场景。
基本配置 #
javascript
// 服务端构建
const serverBuild = esbuild.build({
entryPoints: ['src/server.js'],
bundle: true,
platform: 'node',
target: 'node18',
outfile: 'dist/server.js',
external: ['express', 'koa'],
define: {
'process.env.IS_SERVER': 'true',
},
});
// 客户端构建
const clientBuild = esbuild.build({
entryPoints: ['src/client.js'],
bundle: true,
platform: 'browser',
outfile: 'dist/client.js',
define: {
'process.env.IS_SERVER': 'false',
},
});
await Promise.all([serverBuild, clientBuild]);
同构代码 #
javascript
// shared/utils.js
export function formatDate(date) {
return new Intl.DateTimeFormat('zh-CN').format(date);
}
export function isServer() {
return typeof window === 'undefined';
}
export function getBaseUrl() {
if (isServer()) {
return process.env.BASE_URL || 'http://localhost:3000';
}
return window.location.origin;
}
SSR 插件 #
javascript
const ssrPlugin = {
name: 'ssr',
setup(build) {
const isServer = build.initialOptions.platform === 'node';
build.onResolve({ filter: /^browser:/ }, (args) => {
if (isServer) {
return { path: args.path, namespace: 'empty' };
}
return { path: args.path.replace('browser:', ''), external: false };
});
build.onResolve({ filter: /^server:/ }, (args) => {
if (!isServer) {
return { path: args.path, namespace: 'empty' };
}
return { path: args.path.replace('server:', ''), external: false };
});
build.onLoad({ filter: /.*/, namespace: 'empty' }, () => ({
contents: 'export default {}',
loader: 'js',
}));
},
};
Source Map #
Source Map 类型 #
javascript
// 外部文件(默认)
esbuild.build({
sourcemap: true,
// 或 sourcemap: 'external'
outfile: 'dist/bundle.js',
});
// 生成 bundle.js 和 bundle.js.map
// 内联
esbuild.build({
sourcemap: 'inline',
outfile: 'dist/bundle.js',
});
// Source Map 内联到文件末尾
// 链接
esbuild.build({
sourcemap: 'linked',
outfile: 'dist/bundle.js',
});
// 返回 Source Map 内容
Source Map 配置 #
javascript
esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
sourcemap: true,
sourcesContent: true, // 包含源代码(默认 true)
sourceRoot: '/src', // 源代码根目录
});
调试配置 #
javascript
esbuild.build({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
sourcemap: true,
sourcesContent: process.env.NODE_ENV === 'development',
});
元数据分析 #
生成元数据 #
javascript
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outdir: 'dist',
metafile: true,
});
const metafile = result.metafile;
分析元数据 #
javascript
const esbuild = require('esbuild');
async function analyze() {
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
metafile: true,
outfile: 'dist/bundle.js',
});
// 文本分析
const text = await esbuild.analyzeMetafile(result.metafile, {
verbose: true,
});
console.log(text);
// JSON 分析
const json = await esbuild.analyzeMetafile(result.metafile, {
format: 'json',
});
console.log(JSON.parse(json));
}
analyze();
元数据结构 #
javascript
{
inputs: {
'src/index.js': {
bytes: 1024,
imports: [
{ path: 'src/utils.js', kind: 'import-statement' }
],
},
'src/utils.js': {
bytes: 512,
imports: [],
},
},
outputs: {
'dist/bundle.js': {
bytes: 2048,
entryPoint: 'src/index.js',
inputs: {
'src/index.js': { bytesInOutput: 800 },
'src/utils.js': { bytesInOutput: 400 },
},
imports: [],
exports: [],
},
},
}
自定义分析 #
javascript
function analyzeBundle(metafile) {
const outputs = metafile.outputs;
const analysis = [];
for (const [file, info] of Object.entries(outputs)) {
const inputFiles = Object.keys(info.inputs);
const totalInputBytes = Object.values(info.inputs)
.reduce((sum, i) => sum + i.bytesInOutput, 0);
analysis.push({
file,
outputBytes: info.bytes,
inputCount: inputFiles.length,
inputBytes: totalInputBytes,
compressionRatio: (info.bytes / totalInputBytes).toFixed(2),
});
}
return analysis;
}
并行构建 #
多入口并行 #
javascript
const esbuild = require('esbuild');
async function buildAll() {
const configs = [
{
entryPoints: ['src/main.js'],
outfile: 'dist/main.js',
},
{
entryPoints: ['src/admin.js'],
outfile: 'dist/admin.js',
},
{
entryPoints: ['src/worker.js'],
outfile: 'dist/worker.js',
},
];
await Promise.all(
configs.map(config =>
esbuild.build({
...config,
bundle: true,
minify: true,
})
)
);
console.log('All builds complete');
}
buildAll();
多格式输出 #
javascript
async function buildLibrary() {
const formats = ['esm', 'cjs', 'iife'];
await Promise.all(
formats.map(format =>
esbuild.build({
entryPoints: ['src/index.js'],
outfile: `dist/index.${format}.js`,
format,
bundle: true,
globalName: format === 'iife' ? 'MyLib' : undefined,
})
)
);
}
缓存策略 #
文件系统缓存 #
javascript
const fs = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const cacheDir = '.esbuild-cache';
async function getFileHash(filePath) {
const content = await fs.readFile(filePath);
return crypto.createHash('md5').update(content).digest('hex');
}
const cachePlugin = {
name: 'fs-cache',
setup(build) {
build.onLoad({ filter: /\.(js|ts|jsx|tsx)$/ }, async (args) => {
const hash = await getFileHash(args.path);
const cacheFile = path.join(cacheDir, `${hash}.json`);
try {
const cached = JSON.parse(await fs.readFile(cacheFile, 'utf8'));
console.log('Cache hit:', args.path);
return cached;
} catch {
const content = await fs.readFile(args.path, 'utf8');
const result = { contents: content, loader: 'js' };
await fs.mkdir(cacheDir, { recursive: true });
await fs.writeFile(cacheFile, JSON.stringify(result));
return result;
}
});
},
};
内存缓存 #
javascript
const memoryCache = new Map();
const memoryCachePlugin = {
name: 'memory-cache',
setup(build) {
build.onLoad({ filter: /\.(js|ts)$/ }, async (args) => {
const cacheKey = `${args.path}:${build.initialOptions.format}`;
if (memoryCache.has(cacheKey)) {
return memoryCache.get(cacheKey);
}
const fs = require('fs').promises;
const contents = await fs.readFile(args.path, 'utf8');
const result = { contents, loader: 'js' };
memoryCache.set(cacheKey, result);
return result;
});
},
};
错误处理 #
构建错误 #
javascript
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
write: false, // 不写入文件,手动处理
});
if (result.errors.length > 0) {
for (const error of result.errors) {
console.error(`Error: ${error.text}`);
if (error.location) {
console.error(` at ${error.location.file}:${error.location.line}:${error.location.column}`);
}
}
process.exit(1);
}
// 手动写入
const fs = require('fs').promises;
for (const file of result.outputFiles) {
await fs.mkdir(path.dirname(file.path), { recursive: true });
await fs.writeFile(file.path, file.contents);
}
警告处理 #
javascript
const result = await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
});
for (const warning of result.warnings) {
console.warn(`Warning: ${warning.text}`);
}
日志级别 #
javascript
esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/bundle.js',
logLevel: 'warning', // silent, error, warning, info, debug
logLimit: 10, // 限制日志数量
});
下一步 #
现在你已经掌握了 esbuild 的高级特性,接下来学习 最佳实践 了解生产环境的使用技巧!
最后更新:2026-03-28