1)先定目标:用指标说话,而不是感觉
页面级关键指标(Core Web Vitals)
LCP(最大内容渲染):理想 ≤ 2.5sINP(互动性):理想 ≤ 200msCLS(稳定性):理想 ≤ 0.1
其它实用指标:TTFB、FCP、TTI、TBT、长任务(Long Task > 50ms)占比、内存占用、JS 解析与执行时间、包体体积、请求数量。
原则
设定性能预算(Performance Budget):如“首屏 JS ≤ 170KB gz、图片 ≤ 300KB、请求数 ≤ 30、LCP ≤ 2.5s”。既做 合成监控(Lighthouse/WebPageTest),也做 真实用户监控 RUM(web-vitals + 自建上报),关注 p75/p95。
2)诊断工具与方法
浏览器 Performance 面板:看火焰图,找主线程瓶颈(脚本执行、样式计算、布局、绘制、合成)。Coverage(代码覆盖率):识别未用代码。Lighthouse:一键体检与建议。WebPageTest:细分网络、阻塞、H2/H3、优先级。bundle 分析:
/
webpack-bundle-analyzer
/
rollup-plugin-visualizer
。RUM 上报:用 PerformanceObserver 捕获 Web Vitals 与长任务。
source-map-explorer
// web-vitals + PerformanceObserver(RUM 示例)
import { onLCP, onINP, onCLS } from 'web-vitals';
function report(metric) {
navigator.sendBeacon('/rum', JSON.stringify(metric));
}
onLCP(report);
onINP(report);
onCLS(report);
// 监控长任务
new PerformanceObserver((list) => {
list.getEntries().forEach(e => {
if (e.duration > 50) report({ name: 'longtask', duration: e.duration, startTime: e.startTime });
});
}).observe({ entryTypes: ['longtask'] });
3)网络与资源加载:先把“路”修好
3.1 HTTP/H2/H3 与连接优化
启用 HTTP/2 或 HTTP/3,减少队头阻塞。TLS1.3,配置 OCSP Stapling。DNS/TLS 预热:
、
<link rel="dns-prefetch" href="//cdn.example.com">
。
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
3.2 资源优先级与资源提示
preload 关键字体/首屏 CSS/首屏 hero 图:
<link rel="preload" as="style" href="/critical.css">
<link rel="preload" as="image" href="/images/hero.avif" imagesrcset="/images/hero.avif 1x, /images/hero@2x.avif 2x">
<link rel="modulepreload" href="/src/entry.js">
fetchpriority(优先级提示):
<img src="/hero.avif" fetchpriority="high" >
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high">
prefetch 次级路由/后续资源(低优先级空闲加载)。
3.3 缓存策略
静态资源:
+ 内容哈希。HTML:
Cache-Control: public, max-age=31536000, immutable
或短缓存 + CDN 边缘渲染(若 SSR)。Service Worker:离线与缓存兜底;谨慎失效策略。
no-store
3.4 压缩与图片
Brotli 优先,fallback gzip。
图片用 AVIF/WebP,配合
与
srcset/sizes
:
loading="lazy"
<img
src="/img/cover.avif"
srcset="/img/cover-480.avif 480w, /img/cover-960.avif 960w"
sizes="(max-width: 600px) 480px, 960px"
loading="lazy"
decoding="async"
>
3.5 CSS/JS 加载策略
Critical CSS 内联(首屏关键 CSS ≤ ~7KB),其余延迟加载。JS:
(原生 ESM 支持并默认 defer),非关键脚本
type="module"
/
defer
。按路由/组件拆分,动态
async
,避免首屏加载全站 JS。
import()
4)构建与包体积:让浏览器少干活
4.1 Tree-shaking 与 sideEffects
使用 ESM(import/export),确保库声明 sideEffects 字段,避免错误摇树。避免
、动态
require()
、带副作用的入口。
eval
4.2 依赖与重复
避免整包引入(如
),改为
import _ from 'lodash'
或用
import debounce from 'lodash/debounce'
。移除大体积冗余(如 moment 全量本地化),用 date-fns/dayjs 替代或按需载入 locale。
lodash-es
4.3 Babel/Polyfill 精准投放
设好 browserslist,用
+
@babel/preset-env
+
useBuiltIns: 'usage'
。
core-js@3
现代浏览器走 现代产物(module),老浏览器走 legacy(nomodule)分流:
<script type="module" src="/bundle.modern.js"></script>
<script nomodule src="/bundle.legacy.js"></script>
4.4 构建器选择与优化
Vite/Esbuild/Rollup/webpack 新版本都更快。开启 minify(terser/esbuild)、scope hoisting、splitChunks、long-term caching。使用分析器定位大包与重复依赖。
5)运行时性能:理解引擎,写出“容易被热身”的代码
5.1 V8 等 JIT 优化要点(通用)
对象形状(Hidden Class/Shapes)稳定:同字段顺序构造对象,避免在热路径给对象动态加字段。避免数组稀疏(有“洞”)与混用类型(number/string 混装)。不要用
(会降级字典模式),改为
delete obj.prop
或重建新对象。避免热路径使用
obj.prop = undefined
,用剩余参数
arguments
。尽量让函数 单态/少态(monomorphic/polymorphic) 调用,减少 IC 多态退化。
(...args)
// 坏:动态增加字段 & delete
function makeUser(id) {
const u = {};
u.id = id;
if (Math.random() > 0.5) u.flag = true; // 形状不稳定
return u;
}
// 好:一次性定义稳定形状
function makeUser(id, flag = false) {
return { id, flag }; // 字段顺序固定
}
5.2 热路径避免异常机制与装箱
try/catch 放在冷路径;避免频繁装箱/拆箱(如把数字放进
乱用)。字符串拼接现代引擎已很快,但在循环里构建大型字符串 prefer array join 或模板累积并 batch。
Map<string, any>
5.3 Map/Set 与数据结构选择
动态键查找用 Map/Set,需要顺序可复现时用 Array + 索引。计算密集型可考虑 TypedArray 与 WebAssembly(如有瓶颈)。
6)DOM、样式与渲染:避开“布局抖动”,把动画交给合成器
6.1 读写分离,避免 Layout Thrashing
// 坏:交替读写,触发多次回流
box.style.width = '200px';
const h = box.offsetHeight; // 强制同步布局
box.style.height = (h + 10) + 'px';
// 好:先读后写 & 批处理
const h = box.offsetHeight;
requestAnimationFrame(() => {
box.style.width = '200px';
box.style.height = (h + 10) + 'px';
});
6.2 动画与合成
使用 transform/opacity 做动画;避免
。必要时加
top/left/width/height
(谨慎、短时使用,避免内存膨胀)。大量内容用 虚拟列表(windowing/virtualization)。
will-change: transform;
6.3 事件与滚动
提升滚动性能。节流/防抖高频事件;滚动观测用 IntersectionObserver(做懒加载/曝光统计)。
passive: true
window.addEventListener('scroll', onScroll, { passive: true });
const onInput = debounce((e) => search(e.target.value), 200);
input.addEventListener('input', onInput);
7)并行与分工:把重活丢给 Worker/Worklet
Web Worker:CPU 密集计算、JSON 大对象解析、图像处理。Comlink 简化 Worker 通信,避免手写
。OffscreenCanvas 在 Worker 中渲染;Audio/Animation/Paint Worklet 把特定任务交给更合适的线程。requestIdleCallback 在空闲时做非关键任务(如预取、缓存填充)。
postMessage
// Worker 主线程
import { wrap } from 'comlink';
const worker = new Worker(new URL('./heavy.worker.js', import.meta.url), { type: 'module' });
const api = wrap(worker);
const res = await api.heavyCompute(data); // 像调用本地函数一样
8)内存与 GC:防漏、防抖、可回收
常见泄漏:全局/单例强引用、闭包持有 DOM、未清理的事件监听/定时器、缓存不失效。用 WeakMap/WeakRef/FinalizationRegistry 管理对临时对象的引用。定期用 Heap Snapshot/Allocation Timeline 排查峰值与未释放对象。
// 使用 WeakMap 避免无界缓存
const cache = new WeakMap();
function render(node, data) {
if (!cache.has(node)) cache.set(node, computeExpensive(data));
node.textContent = cache.get(node);
}
9)框架实践要点
React
列表加稳定 key;使用
/
React.memo
/
useMemo
控制重渲染。避免在 render 中创建新对象/函数(或用 memo 化)。大列表用 react-window / react-virtualized。路由级 代码拆分:
useCallback
+
React.lazy
。关注 Server Components / Streaming SSR(降低客户端 JS)。
Suspense
const List = React.memo(function List({ items }) {
return items.map(item => <Row key={item.id} item={item} />);
});
Vue
合理使用
(缓存)而非
computed
;
watch
渲染静态块。大组件拆分与异步组件;
v-once
缓存路由视图。深层响应式对象可用
keep-alive
降低代理成本。
shallowRef/markRaw
Svelte/其它
编译期优化已很强,仍需拆分路由与懒加载、虚拟列表与 Worker。
10)Node.js 侧(若含 SSR/中间层)
保持事件循环轻盈:CPU 密集任务交给
或外部服务。充分利用 流(streaming),避免
worker_threads
大文件。HTTP keep-alive,连接池,Gzip/Brotli。监控与剖析:
readFileSync
、
clinic.js
、
—prof
,定位阻塞。缓存:内存(LRU)、Redis、边缘缓存。对 SSR 采用 分片渲染/Streaming。
—trace-gc
11)案例:把“卡顿搜索页”救活(从 2.9s LCP → 1.6s,INP p75 450ms → 120ms)
背景
首屏加载 850KB(gz)JS,搜索输入时主线程卡顿;列表 2000 条。
优化步骤
拆包:路由级分包 + 搜索页图表另成异步 chunk;moment→dayjs;去掉未用的组件库图标集。→ JS 降到 320KB。图片与资源优先级:hero 图 AVIF +
+ preload;字体子集化。→ LCP -400ms。输入防抖 + Worker 过滤:输入
fetchpriority=high
;数据过滤搬到 Worker。→ INP -250ms。虚拟列表:react-window 渲染 2k→30 可视项。→ TBT 显著下降。读写分离:滚动与高亮样式统一 rAF 批处理。→ 长任务占比下降。
debounce 150ms
结果
LCP p75:2.9s → 1.6sINP p75:450ms → 120ms包体:850KB → 320KB
核心代码片段:
// 1) 输入防抖
const onInput = debounce((q) => worker.search(q), 150);
// 2) Worker 侧搜索
expose({
search(query) {
// 重计算在 Worker,主线程更顺滑
return fastFuseSearch(data, query);
}
});
// 3) 虚拟列表(以 react-window 为例)
const Row = ({ index, style }) => <Item style={style} data={items[index]} />;
<List height={600} itemCount={items.length} itemSize={48} width="100%">
{Row}
</List>
12)常见误区
只看 Lighthouse 分数:请同时做 RUM,关注真实用户 p75/p95。过度预加载:把所有资源都
会挤占关键资源的带宽与优先级。为优化而优化:未测先“抠微优化”往往收益极小;先找瓶颈。无限缓存不失效:immutable 必须配合内容哈希。在热路径频繁创建新函数/对象:造成 GC 压力与 diff 频繁。
preload
13)落地清单
规划
设定性能预算(LCP/INP/CLS、体积、请求数)。 搭建 RUM 上报(web-vitals + 自建端点)。
网络
H2/H3 + TLS1.3;preconnect CDN。 关键资源 preload / modulepreload;合理 prefetch。 静态资源长缓存 + 内容哈希;Brotli。
构建
ESM + Tree-shaking;
正确标注。 按路由/组件分包;动态 import。 精准 polyfill;modern/legacy 分流。 分析器清理大依赖与重复包。
sideEffects
运行时/渲染
对象形状稳定;避免稀疏数组与
。 高频事件节流/防抖;被动监听。 rAF 批处理读写;动画走 transform/opacity。 虚拟列表;长任务迁移至 Worker。 图片 AVIF/WebP +
delete
+
srcset/sizes
。
loading="lazy"
内存/Node
WeakMap/解除监听/清理定时器。 Node 流式处理与 worker_threads;SSR streaming。
14)可复制代码片段合集
(1)首屏关键 CSS 内联 + 非关键延后
<style>/* critical css */</style>
<link rel="preload" as="style" href="/above-the-fold.css" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/above-the-fold.css"></noscript>
(2)按需加载路由
// React 示例
const ProductPage = React.lazy(() => import('./pages/ProductPage'));
<Route path="/product/:id" element={<Suspense fallback={<Spinner/>}><ProductPage/></Suspense>} />
(3)节流与防抖
export const throttle = (fn, wait = 100) => {
let last = 0, timer;
return (...args) => {
const now = Date.now();
if (now - last >= wait) {
last = now; fn(...args);
} else {
clearTimeout(timer);
timer = setTimeout(() => { last = Date.now(); fn(...args); }, wait - (now - last));
}
};
};
export const debounce = (fn, wait = 200) => {
let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); };
};
(4)Performance 标记
performance.mark('search-start');
// ... 执行搜索
performance.mark('search-end');
performance.measure('search', 'search-start', 'search-end');
console.table(performance.getEntriesByName('search'));
(5)IntersectionObserver 懒加载
const io = new IntersectionObserver((entries, obs) => {
for (const e of entries) if (e.isIntersecting) {
const img = e.target; img.src = img.dataset.src; obs.unobserve(img);
}
}, { rootMargin: '200px' });
document.querySelectorAll('img[data-src]').forEach(img => io.observe(img));
(6)rAF 批处理读写
let pending = false, writes = [], reads = [];
export function scheduleRead(fn){ reads.push(fn); schedule(); }
export function scheduleWrite(fn){ writes.push(fn); schedule(); }
function schedule(){
if (pending) return; pending = true;
requestAnimationFrame(() => {
const r = reads.slice(); reads.length = 0;
const w = writes.slice(); writes.length = 0;
for (const fn of r) fn();
for (const fn of w) fn();
pending = false;
});
}
结语
性能优化没有银弹,但有 顺序与方法:测量 → 定预算 → 找瓶颈 → 先网络&包体 → 再运行时&渲染 → 并行与内存 → 监控回归。把这套流程嵌进你的交付与 CI(如体积阈值、Lighthouse CI、RUM 警报),性能就会变成“工程能力”,而不是一次性的突击。
暂无评论内容