构建产物分析与Tree-Shaking
1. Bundle Analyzer
可视化看构建产物里谁最大:
1.1 Vite
npx vite-bundle-visualizer
# 产出 stats.html
或插件:
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [
visualizer({ open: true, gzipSize: true })
]
}
1.2 Webpack
npx webpack-bundle-analyzer dist/stats.json
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = {
plugins: [new BundleAnalyzerPlugin({ analyzerMode: 'static' })]
}
1.3 Next.js
ANALYZE=true next build
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({})
2. Tree Shaking
编译器移除未使用的代码。
2.1 前提条件
- 使用 ES Module(
import/export),不是 CommonJS(require) - 包的
package.json声明"sideEffects": false - 不使用副作用 import(
import './styles.css'需声明)
2.2 sideEffects
// package.json
{
"sideEffects": false
}
// 或精确指定有副作用的文件
{
"sideEffects": ["*.css", "*.scss", "./src/polyfills.ts"]
}
2.3 检查 Tree Shaking 效果
# 看哪些模块被 tree-shaken
npx vite build --debug
# 或看 bundle analyzer 中是否包含不需要的模块
3. 代码分割
3.1 按路由
// React
const Dashboard = React.lazy(() => import('./pages/Dashboard'))
const Settings = React.lazy(() => import('./pages/Settings'))
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
3.2 按功能
// 用户点击才加载重编辑器
const loadEditor = () => import('./Editor')
button.onclick = async () => {
const { Editor } = await loadEditor()
// 使用 Editor
}
3.3 Vendor 拆分
// vite.config.ts
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@radix-ui/react-dialog', '@headlessui/react'],
'vendor-utils': ['lodash-es', 'date-fns'],
}
}
}
}
4. 依赖替换(减体积)
| 大包 | 轻量替代 |
|---|---|
| lodash | lodash-es(按需 import) |
| moment | dayjs / date-fns |
| axios | 原生 fetch + tiny wrapper |
| uuid | crypto.randomUUID() |
| classnames | clsx(1KB) |
4.1 import cost
VS Code 插件 Import Cost 实时显示每个 import 体积。
4.2 按需导入
// ✗ 全量引入
import _ from 'lodash'
_.get(obj, 'a.b')
// ✓ 按需
import get from 'lodash-es/get'
get(obj, 'a.b')
// 或用 babel-plugin-import / unplugin-auto-import
5. size-limit(CI 预算)
// package.json
{
"size-limit": [
{ "path": "dist/assets/index-*.js", "limit": "150 KB", "gzip": true },
{ "path": "dist/assets/vendor-*.js", "limit": "200 KB", "gzip": true },
{ "path": "dist/**/*.css", "limit": "50 KB", "gzip": true }
]
}
npx size-limit
# ✓ index.js: 120 KB (limit: 150 KB)
# ✗ vendor.js: 210 KB (limit: 200 KB) — EXCEEDED
CI 集成:
- run: npx size-limit
超出 = CI 失败 = PR 必须优化或调整预算。
5.1 bundlesize(替代)
{
"bundlesize": [
{ "path": "dist/assets/*.js", "maxSize": "200 kB" }
]
}
6. 分析常见大包
bundle analyzer 里常见"巨物":
| 包 | 典型大小 | 解决 |
|---|---|---|
| react-dom | 130KB | 无法替代,确保只一份 |
| @mui/material | 200KB+ | 按需 import |
| chart.js / echarts | 200-500KB | 按需注册组件 |
| monaco-editor | 5MB+ | Web Worker + 按需加载 |
| moment | 300KB(含 locale) | 换 dayjs |
| lodash | 70KB 全量 | lodash-es 按需 |
| aws-sdk | 几十 MB | @aws-sdk/client-* 按需 |
7. Source Map 分析
# source-map-explorer
npx source-map-explorer dist/assets/index-*.js
比 bundle analyzer 更精确(基于 sourcemap 逆向)。
8. 监控 bundle 增长
PR 自动评论 bundle size 变化:
# GitHub Action: compressed-size-action
- uses: preactjs/compressed-size-action@v2
with:
repo-token: $}} secrets.GITHUB_TOKEN }}
PR 评论里显示:
+12 KB gzip (index.js: 145 KB → 157 KB)
9. 常见反模式
- 全量 import lodash / antd:几百 KB 冗余
- 不做代码分割:首次加载 2MB JS
- 不看 bundle analyzer:不知道什么大
- moment 全 locale:300KB 里 250KB 是语言包
- 不设 size-limit:bundle 悄悄膨胀
- dynamic import 用在首屏关键组件:LCP 反而慢
- devDependencies 跑进生产:如 storybook、testing-library
- polyfill 全量引入:core-js 全集 100KB+。用
useBuiltIns: 'usage'