性能优化
性能优化 = 度量 + 策略 + 验证。常见误区:上来就上手段,不量数据。正确姿势:先量基线 → 找瓶颈 → 优化 → 验证 → 沉淀。
优化地图
性能优化
├─ 构建时(开发者体验 + 产物大小)
│ ├─ 构建速度
│ ├─ 产物体积
│ └─ 缓存策略
├─ 资源加载(首屏)
│ ├─ 网络层(CDN / HTTP/2 / 预连接)
│ ├─ 资源大小(压缩 / 格式 / 按需)
│ └─ 加载顺序(preload / prefetch / 关键 CSS)
└─ 运行时(交互体验)
├─ 渲染性能(长任务 / 重排重绘 / 虚拟列表)
├─ JS 性能(防抖节流 / Web Worker / 算法)
└─ 体验优化(骨架屏 / 懒加载 / SSR)
一、构建速度
1. 通用方案
| 手段 | 效果 | 适用 |
|---|---|---|
| 持久化缓存 | 二次构建提速 50%+ | 通用 |
| 多核并行(thread-loader) | 大型项目显著 | webpack4/5 |
| 替换为更快的工具 | 量级提升 | 通用 |
// webpack5 cache
module.exports = {
cache: { type: "filesystem" },
};
// 用 swc/esbuild 替换 babel/tsc
// 速度对比:babel > swc/esbuild(5~10x)
2. 减小 loader 处理范围
module: {
rules: [
{
test: /\.js$/,
// 关键:跳过 node_modules
exclude: /node_modules/,
use: "swc-loader",
},
],
}
3. 升级到 Vite(如果可以)
- 开发用 esbuild + 原生 ESM,启动 < 1s
- 生产用 Rollup,产物质量有保障
二、产物体积
1. 分析工具
# webpack
pnpm i -D webpack-bundle-analyzer
# vite
pnpm i -D rollup-plugin-visualizer
2. 常用手段
- 代码分割:路由级懒加载 + 第三方库抽离
splitChunks: {
chunks: "all",
cacheGroups: {
vendors: { test: /[\\/]node_modules[\\/]/, name: "vendors", priority: 10 },
},
}
- Tree Shaking:使用 ES Module +
sideEffects: false - 按需引入:UI 库按需加载(
babel-plugin-import、unplugin-vue-components) - 图片优化:压缩、WebP/AVIF、图标用 SVG sprite
- 第三方库瘦身:用
lodash-es替代lodash、dayjs替代moment
3. Gzip / Brotli
构建时生成 .gz / .br,nginx 配合 gzip_static on; 直接读预压缩文件,省 CPU。
三、资源加载
1. 关键资源预加载
<!-- 关键字体 -->
<link rel="preload" href="/font.woff2" as="font" crossorigin>
<!-- 下一步路由的 chunk -->
<link rel="prefetch" href="/static/js/user-page.js">
<!-- 跨域关键资源 -->
<link rel="preconnect" href="https://api.example.com">
2. 关键 CSS 内联
首屏关键 CSS 内联到 <style>,非关键 CSS 异步加载:
<style>/* critical CSS 内联 */</style>
<link rel="preload" href="/static/css/main.css" as="style" onload="this.rel='stylesheet'">
3. 图片懒加载
<img loading="lazy" decoding="async" src="..." />
JS 用 IntersectionObserver 实现自定义懒加载。
四、运行时性能
1. 减少重排重绘
// ❌ 多次写入,多次重排
el.style.width = "100px";
el.style.height = "100px";
el.style.left = "10px";
// ✅ 一次性 class 切换,只触发一次
el.classList.add("box");
// ✅ 离线 DOM
const frag = document.createDocumentFragment();
// ... 批量操作
container.appendChild(frag);
2. 长列表虚拟滚动
只渲染可视区内的 DOM 节点。开源方案:react-window、vue-virtual-scroller、@tanstack/virtual。
3. 大数据渲染分片
// requestIdleCallback 切片渲染
function renderInChunks(list: any[], renderItem: (item: any) => void) {
let i = 0;
function step() {
const start = performance.now();
while (i < list.length && performance.now() - start < 5) {
renderItem(list[i++]);
}
if (i < list.length) requestIdleCallback(step);
}
requestIdleCallback(step);
}
4. Web Worker
把 CPU 密集型任务(解析、加密、图像处理)丢到 Worker:
// main.ts
const worker = new Worker(new URL("./heavy.worker.ts", import.meta.url), { type: "module" });
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);
五、缓存策略
1. HTTP 缓存
HTML: Cache-Control: no-cache # 每次协商缓存
静态资源: Cache-Control: public, max-age=31536000, immutable # 文件名带 hash
2. Service Worker(PWA)
// workbox
import { precacheAndRoute } from "workbox-precaching";
import { registerRoute } from "workbox-routing";
import { CacheFirst } from "workbox-strategies";
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(({ request }) => request.destination === "image", new CacheFirst());
3. 接口缓存
- 不可变数据:SWR 风格的
stale-while-revalidate - 频繁读、偶尔写:localStorage / IndexedDB
- 实时性要求高:放弃前端缓存
六、面试高频
- Q:首屏优化从哪些方面入手? A:网络(CDN、压缩、预连接)、资源(按需、图片优化、字体子集)、加载顺序(preload、关键 CSS 内联)、JS 体积(splitChunks、按需引入)、运行时(SSR、懒加载)。
- Q:长列表卡顿怎么优化? A:虚拟滚动(只渲染可视区)、分片渲染(requestIdleCallback / setTimeout)、Web Worker、避免内联函数和复杂对象。
- Q:Tree Shaking 失效的常见原因?
A:使用了 CJS、side effects 标注错误(package.json 缺
sideEffects: false或误标)、按需引入未生效。