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

手动代码分割 #

使用 entryNameschunkNames 控制输出:

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