
构建优化:乘数效应
从 JavaScript 包中移除的每个千字节都会产生乘数效应:更小的下载量、更快的解析、更快的执行。在移动网络上,200KB 和 600KB 包之间的差异可能决定用户是留下还是离开。
本指南涵盖了最重要的技术,并附有具体的前后对比示例。

理解 JavaScript 包成本
JavaScript 是最昂贵的资源类型:每个字节都必须下载、解压缩、解析、编译和执行。图片下载成本高但解析成本低。JS 在每一步都很昂贵。
100KB 的 JavaScript(gzip 后约 30KB)的成本:
1. 网络:下载 30KB
2. 解压缩:CPU + 内存
3. 解析:中端手机上约 80ms
4. 编译:中端手机上约 120ms
5. 执行:取决于代码复杂度
100KB 的图片:只有下载成本
基准:初始加载的 JS gzip 后 < 200KB 是大多数应用的合理目标。
Webpack 代码分割
代码分割将你的包分割成按需加载的块:
// webpack.config.js
module.exports = {
entry: {
main: './src/index.ts',
},
optimization: {
splitChunks: {
chunks: 'all', // 分割异步和同步块
cacheGroups: {
// 供应商块:很少变化的库
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
// React 特定块
react: {
test: /[\/]node_modules[\/](react|react-dom|react-router)[\/]/,
name: 'react-vendor',
chunks: 'all',
priority: 20, // 更高优先级 = 优先
},
// 大型、不常用的库
charts: {
test: /[\/]node_modules[\/](chart.js|recharts|d3)[\/]/,
name: 'charts-vendor',
chunks: 'async', // 仅在动态导入时
priority: 20,
},
// 多个入口点之间共享的公共代码
common: {
name: 'common',
minChunks: 2, // 至少在 2 个块中使用
priority: 5,
reuseExistingChunk: true,
},
},
},
// 内容哈希用于长期缓存
chunkIds: 'deterministic',
moduleIds: 'deterministic',
},
output: {
filename: '[name].[contenthash].js', // main.abc123.js
chunkFilename: '[name].[contenthash].js', // vendors.def456.js
},
}
动态导入:基于路由的分割
// React Router 配合路由级代码分割
import { lazy, Suspense } from 'react'
// 每个路由成为一个单独的块
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() =>
import(/* webpackChunkName: "dashboard" */ './pages/Dashboard')
)
const AdminPanel = lazy(() =>
import(/* webpackChunkName: "admin" */ './pages/AdminPanel')
)
// 悬停时预加载(减少感知加载时间)
function NavLink({ to, prefetch, children }) {
const handleMouseEnter = () => {
if (prefetch) {
// Webpack 魔法注释:开始下载但不渲染
import(/* webpackPrefetch: true */ `./pages/${to}`)
}
}
return (
<Link to={to} onMouseEnter={handleMouseEnter}>
{children}
</Link>
)
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
)
}

Tree Shaking:移除死代码
Tree shaking 消除未使用的导出。但它需要正确的配置:
// package.json — 表明你的代码无副作用
{
"sideEffects": false // 所有内容都可以 tree-shake
// 或者指定有副作用的文件:
"sideEffects": [
"*.css",
"src/polyfills.js"
]
}
// webpack.config.js — tree shaking 仅在 production 模式下工作
module.exports = {
mode: 'production', // 启用 TerserPlugin(压缩)+ tree shaking
optimization: {
usedExports: true, // 标记使用的导出
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
dead_code: true, // 移除死代码
pure_funcs: ['console.log'], // 移除特定函数调用
},
},
}),
],
},
}
常见的 Tree Shaking 失败
// ❌ 默认导出阻止工具函数的 tree shaking
// utils.js
export default {
formatDate,
formatCurrency,
formatBytes,
}
// ❌ 导入整个模块:
import utils from './utils'
utils.formatDate(date) // formatCurrency 和 formatBytes 仍然被打包!
// ✅ 命名导出启用 tree shaking
// utils.js
export function formatDate(date) { /* ... */ }
export function formatCurrency(amount) { /* ... */ }
export function formatBytes(bytes) { /* ... */ }
// ✅ 只导入你需要的内容
import { formatDate } from './utils'
// formatCurrency 和 formatBytes 被 tree-shake 掉
// ❌ CommonJS (require) 不可 tree-shake
const { formatDate } = require('./utils') // 整个模块被包含
// ✅ ES modules (import) 可 tree-shake
import { formatDate } from './utils'
包分析
# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
# 在 webpack.config.js 中
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static', // 生成 HTML 报告
openAnalyzer: true,
reportFilename: 'bundle-report.html',
})
].filter(Boolean),
}
# 运行:
ANALYZE=true webpack build
# 对于 Vite:
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer({
filename: 'stats.html',
open: true,
gzipSize: true,
brotliSize: true,
})
],
})
# Source Map Explorer(适用于任何打包工具)
npm install --save-dev source-map-explorer
npx source-map-explorer dist/main.*.js
在分析器中查找什么:
- 你不知道被包含的大型库(例如,当你只使用一个函数时,lodash 被完整包含)
- 重复的模块(同一个库被打包多次)
- 生产包中的开发环境代码
- 意外过大的供应商块

Vite 构建优化
// vite.config.ts
import { defineConfig, splitVendorChunkPlugin } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
react(),
splitVendorChunkPlugin(), // 自动分割供应商/应用块
],
build: {
target: 'es2020', // 现代浏览器 — 更小的输出
minify: 'esbuild', // 快速压缩 (esbuild) 或彻底 (terser)
sourcemap: true, // 用于调试(不要部署到公共环境)
rollupOptions: {
output: {
// 手动块分割
manualChunks: (id) => {
if (id.includes('node_modules')) {
// React 生态系统一起
if (id.includes('react') || id.includes('react-dom')) {
return 'react'
}
// 重型库作为自己的块
if (id.includes('monaco-editor')) return 'monaco'
if (id.includes('chart.js') || id.includes('recharts')) return 'charts'
if (id.includes('@tanstack')) return 'tanstack'
// 其他所有:供应商
return 'vendors'
}
},
// 带内容哈希的资源命名
entryFileNames: 'assets/[name]-[hash].js',
chunkFileNames: 'assets/[name]-[hash].js',
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
// 如果块超过此大小则警告
chunkSizeWarningLimit: 500, // KB
},
// 依赖预打包
optimizeDeps: {
include: ['react', 'react-dom'], // 预打包以加快开发速度
exclude: ['@vite/client', '@vite/env'], // 不预打包
},
})
压缩与资源优化
// Webpack 中的 Gzip/Brotli 压缩
const CompressionPlugin = require('compression-webpack-plugin')
const zlib = require('zlib')
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /.(js|css|html|svg)$/,
threshold: 10240, // 仅压缩大于 10KB 的文件
minRatio: 0.8, // 仅当节省 20% 以上时压缩
}),
new CompressionPlugin({
algorithm: 'brotliCompress',
test: /.(js|css|html|svg)$/,
compressionOptions: { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } },
filename: '[path][base].br',
deleteOriginalAssets: false,
}),
]
// Nginx:提供预压缩文件
// nginx.conf
// gzip_static on;
// brotli_static on;
衡量构建性能
# Webpack 构建时间分析
npx webpack --profile --json > stats.json
# 上传到 https://webpack.github.io/analyse/
# 哪些 loader/plugin 慢?
npm install --save-dev speed-measure-webpack-plugin
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
module.exports = smp.wrap(webpackConfig)
# Vite 构建计时
vite build --reporter=verbose
# 跟踪包大小随时间变化(CI)
npx bundlewatch --config .bundlewatchrc.js
# 如果包超过定义阈值,CI 失败
// .bundlewatchrc.js
{
"files": [
{ "path": "dist/assets/main-*.js", "maxSize": "200kB" },
{ "path": "dist/assets/vendors-*.js", "maxSize": "300kB" },
{ "path": "dist/assets/*.css", "maxSize": "50kB" }
],
"ci": {
"trackBranches": ["main"],
"repoBranchIsBaseline": true
}
}
构建优化是一项投资,在应用的整个生命周期中,每次页面加载都会带来回报。基于路由的代码分割、tree shaking、供应商块分离和压缩的组合,通常可以将初始包大小比未优化的构建减少 50-70%。
→ 使用整数进制转换器在二进制、八进制、十进制和十六进制之间转换数字。