JavaScript 性能优化实战(易懂版)
JavaScript 性能优化实战(易懂版)
适用对象:前端与全栈工程师。原则:先测量、再优化、可回归。
目录
- TL;DR 快速清单
-
- 度量与预算
-
- 网络与缓存优化
-
- 构建与代码分割
-
- 运行时与调度
-
- DOM 渲染优化
-
- 内存与泄漏
-
- 并行与重活卸载
-
- Node.js 端优化
-
- 诊断剧本
- 附录:工具与参考
TL;DR 快速清单
- 指标:LCP < 2.5s,INP < 200ms,CLS < 0.1
- 传输:HTTP/2/3 + CDN + Brotli;预连接/预加载关键资源
- 缓存:静态资源指纹 + immutable;HTML 短缓存 + ETag
- 包体:首屏 JS ≤ 150–200KB gzip;路由级代码分割;移除大依赖
- 渲染:transform/opacity 动画;列表虚拟化;被动事件 + 节流/防抖
- 调度:rAF/Idle/切片;避免长任务阻塞主线程
- 内存:清理监听/计时器;WeakMap;LRU 缓存
- 并行:Web Worker + Transferable;OffscreenCanvas
- 回归:web-vitals RUM + Lighthouse CI + Bundle 预算
1. 度量与预算
核心指标与采集(RUM)
// 使用 web-vitals/attribution 采集 LCP/INP/CLS(含归因)
import { onLCP, onINP, onCLS } from 'web-vitals/attribution';const send = (m) => {navigator.sendBeacon('/vitals', JSON.stringify({name: m.name, value: m.value, id: m.id,path: location.pathname, ts: Date.now(),attribution: m.attribution}));
};onLCP(send, { reportAllChanges: true });
onINP(send, { reportAllChanges: true });
onCLS(send, { reportAllChanges: true });
性能预算建议
- 首屏 JS(可执行)≤ 150–200KB gzip
- LCP 资源(图/字体)≤ 100KB;关键请求数 ≤ 6
- CI 设置:Lighthouse 得分门槛 + bundle 体积上限
2. 网络与缓存优化
HTML 预连接/预加载与图片懒加载
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<link rel="preload" as="style" href="/styles.css">
<link rel="preload" as="script" href="/entry.js" crossorigin>
<img src="hero.avif" srcset="hero@2x.avif 2x" alt="hero" loading="lazy" decoding="async">
按用户意图预取
// 悬停时预取下一步页面资源
const prefetch = (href, as) => {const l = document.createElement('link');l.rel = 'prefetch'; l.href = href; if (as) l.as = as;document.head.appendChild(l);
};
document.addEventListener('mouseover', e => {const more = e.target.closest('#more');if (more) prefetch('/page/product.js', 'script');
}, { passive: true });
Service Worker:静态 Cache First + API Stale-While-Revalidate
// sw.js
const STATIC = 'static-v1';
self.addEventListener('install', e => {e.waitUntil(caches.open(STATIC).then(c => c.addAll(['/', '/index.html', '/styles.css', '/entry.js'])));self.skipWaiting();
});
self.addEventListener('activate', e => {e.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k !== STATIC).map(k => caches.delete(k)))).then(() => self.clients.claim()));
});
self.addEventListener('fetch', e => {const req = e.request; if (req.method