JavaScript 性能优化实战:从 3 秒到 300 ms 的压缩与缓存之旅
首发于 CSDN · 2025 年 11 月
原创:yu779
转载请注明来源与作者
1. 背景:为什么又要聊性能?
过去两年,我们团队把「首屏时间」从 3 s 压到 300 ms,真实业务、真实数据、真实收益 。
本文不讲"拍脑袋"的数字,直接上可落地的 10 个策略 ,并给出源码级对比 与线上 A/B 报告 。
读完你可以:
- 用 DevTools 3 分钟定位瓶颈;
- 让 vendors 包体积立减 60 %;
- 让二次访问直接 0 网络请求(离线可用)。
2. 指标:先定义"快"
| 指标 | 目标 | 工具 |
|---|---|---|
| FCP(First Contentful Paint) | < 1.0 s | Lighthouse |
| LCP(Largest Contentful Paint) | < 1.2 s | WebPageTest |
| TTI(Time to Interactive) | < 2.0 s | Chrome DevTools |
| 包体积(gzipped) | < 200 KB | webpack-bundle-analyzer |
3. 诊断:一条命令定位"真凶"
bash
npx lighthouse https://my-site.com --view --preset=desktop
重点关注:
- Opportunities → 按"节省体积"排序;
- Diagnostics → 看 Main-thread Blocking Time;
- Passed Audits → 反向排除已优化项。
4. 实战 10 策
| # | 策略 | 收益 | 成本 | 源码片段 |
|---|---|---|---|---|
| 1 | Tree-Shaking + sideEffects | -35 % | 5 min | 见 4.1 |
| 2 | SplitChunks 自动分包 | -20 % | 10 min | 见 4.2 |
| 3 | 动态 import() 懒加载 | -40 % | 20 min | 见 4.3 |
| 4 | HTTP/2 + preload | -0.3 s | 1 h | 见 4.4 |
| 5 | Brotli 替代 gzip | -25 % | 30 min | 见 4.5 |
| 6 | Service Worker 缓存 | 0 请求 | 1 h | 见 4.6 |
| 7 | 图片 WebP + 自适应尺寸 | -60 % | 40 min | 见 4.7 |
| 8 | 减少 polyfill | -15 % | 15 min | 见 4.8 |
| 9 | 长列表虚拟滚动 | FPS 55→60 | 30 min | 见 4.9 |
| 10 | Vite 预构建依赖 | HMR < 200 ms | 20 min | 见 4.10 |
4.1 Tree-Shaking:让"死"代码消失
背景:lodash 默认全量引入,体积 70 KB。
解决:改成按需 + 标记 sideEffects。
js
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // 启用 Tree-Shaking
sideEffects: false, // 告诉 webpack 可以安全删除
},
};
package.json
json
{
"sideEffects": ["*.css", "*.less"]
}
结果:lodash 从 70 KB → 24 KB(gz)。
4.2 SplitChunks:让 vendors 永不重复
js
// webpack.config.js
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
},
},
},
收益:多页应用共用 vendors.xxx.js,二次访问直接 304。
4.3 懒加载:路由级 + 组件级
js
// React 路由示例
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import(/* webpackChunkName: "chart" */ '@/components/HeavyChart'));
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
实测:首屏减少 42 KB(gz),LCP −320 ms。
4.4 HTTP/2 + preload:让关键资源插队
html
<!-- 放在 <head> 顶部 -->
<link rel="preload" href="/fonts/Inter.var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/js/app.xxx.js" as="script">
注意:HTTP/1.1 时代合并,HTTP/2 时代拆细+多路复用收益更大。
4.5 Brotli:一行 Nginx 配置,体积再减 25 %
conf
# /etc/nginx/conf.d/gzip.conf
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
4.6 Service Worker:离线也能秒开
用 Workbox 零成本接入:
bash
npm i workbox-webpack-plugin -D
js
// webpack.config.js
const { GenerateSW } = require('workbox-webpack-plugin');
plugins.push(
new GenerateSW({
runtimeCaching: [{
urlPattern: /^https:\/\/api\.mycdn\.com/,
handler: 'StaleWhileRevalidate',
options: { cacheName: 'api-cache', expiration: { maxEntries: 200 } },
}],
})
);
效果:二次访问 0 网络请求,TTI 从 1.8 s → 280 ms。
4.7 图片:WebP + 自适应尺寸
html
<picture>
<source srcset="/img/hero-800.webp" media="(max-width: 600px)" type="image/webp">
<source srcset="/img/hero-1600.webp" media="(min-width: 601px)" type="image/webp">
<img src="/img/hero-1600.jpg" alt="hero" loading="lazy" width="1600" height="600">
</picture>
收益:同画质下体积 −60 %;Lighthouse「Serve images in next-gen formats」直接满分。
4.8 减少 polyfill:core-js 按需 + browserslist
js
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3,
targets: { browsers: ['>0.2%', 'not dead', 'not op_mini all'] },
}],
],
};
结果:core-js 从 35 KB → 8 KB(gz)。
4.9 长列表虚拟滚动:react-window 示例
js
import { FixedSizeList } from 'react-window';
<FixedSizeList height={600} itemCount={10000} itemSize={50}>
{({ index, style }) => <div style={style}>Row {index}</div>}
</FixedSizeList>
FPS:从 38 提升到 60,卡顿消失。
4.10 Vite 预构建:让依赖秒级更新
Vite 默认把 node_modules 预构建成 ESM + HTTP 缓存,HMR 200 ms 内。
技巧:把稳定依赖锁定到 optimizeDeps.include,避免重复扫描。
js
// vite.config.ts
export default {
optimizeDeps: {
include: ['lodash-es', 'axios', 'dayjs'],
},
};
5. 监控:把"快"变成常态
- Lighthouse CI 接入 GitHub Actions,MR 阶段自动跑分;
- Web-Vitals 上报到 Prometheus + Grafana,告警阈值:LCP > 1.5 s;
- Bundle-Analyzer 每次构建后输出 diff,体积增长 > 5 % 自动评论提醒。
6. 结果:数据说话
| 版本 | 包体积(gz) | FCP | LCP | TTI | 离线可用 |
|---|---|---|---|---|---|
| v1.0 | 487 KB | 2.1 s | 2.8 s | 3.0 s | × |
| v2.0 | 192 KB | 0.8 s | 1.0 s | 1.2 s | √ |
注:同一页面、同一服务器、同一时间段 A/B 测试,样本 20 k PV。
7. 常见误区 Top 3
- "一把梭"上 HTTP/3 → 先确认内核、CDN、证书链全支持,否则反而 2-RTT 握手退化;
- "迷信压缩比" → Brotli 11 级压缩耗时 800 ms,得不偿失,6 级是甜点;
- "SW 缓存越多越好" → 缓存策略不清会导致用户永远拿不到新版,版本号 + 主动清理是关键。
总结:性能是"减"出来的
优化不是加东西,而是敢删、会删、删得安全。
把本文 10 策做成 checklist,下次发版前跑一遍,90 分钟换 3 倍性能提升,不香吗?