性能优化最大的坑是凭感觉优化。正确顺序永远是:定指标 → 采集基线 → 定位瓶颈 → 优化 → 对比验证。
本文覆盖 Web 与 RN 两条线。理论依据见 浏览器渲染原理。
Core Web Vitals 是业界共识的三个维度:
| 指标 | 含义 | 良好阈值 | 反映什么 |
|---|---|---|---|
| LCP | 最大内容绘制 | < 2.5s | 加载性能:主要内容多久可见 |
| INP | 交互到下次绘制 | < 200ms | 交互性能:点击后多久有响应 |
| CLS | 累积布局偏移 | < 0.1 | 视觉稳定性:内容是否乱跳 |
辅助指标:TTFB(< 800ms,服务端响应)、FCP(首次绘制)、TBT(主线程阻塞总时长)。
INP 在 2024 年 3 月正式取代 FID。面试如果还在说 FID,说明资料是旧的。
采集方式(线上真实数据优于实验室数据):
import { onLCP, onINP, onCLS } from "web-vitals";
onLCP((m) => report("lcp", m.value));
onINP((m) => report("inp", m.value));
onCLS((m) => report("cls", m.value));
实验室数据用 Lighthouse / Performance 面板,用于复现和定位。
LCP 慢只有四个原因,按排查顺序:
Cache-Control、ETag)| 手段 | 收益 |
|---|---|
代码分割(路由级 import()) | 首屏只加载当前路由 |
| Tree shaking | 删掉未使用代码,见 构建工具 |
图片用 webp/avif + srcset | 通常占页面体积 60%+ |
字体子集化 + font-display: swap | 避免字体阻塞文本渲染 |
| gzip / brotli | 文本资源普遍减 70% |
<!-- 非关键 JS 延后 -->
<script src="analytics.js" defer></script>
<!-- 关键 CSS 内联,其余异步 -->
<link rel="preload" href="/critical.css" as="style" />
<!-- 提前建连,省掉 DNS + TCP + TLS 的三次往返 -->
<link rel="preconnect" href="https://api.example.com" crossorigin />
<link rel="dns-prefetch" href="https://cdn.example.com" />
LCP 元素本身要优先:如果 LCP 是首屏大图,给它加 fetchpriority="high",别用 loading="lazy"(lazy 会推迟 LCP)。
renderToPipeableStream)让首屏更早吐出INP 差的根因通常是主线程被长任务占住。判定标准:单次任务超过 50ms 即为长任务。
定位:Performance 面板看主线程火焰图,找最长的黄色块;或用 PerformanceObserver 线上采集:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) report("longtask", entry.duration);
}
}).observe({ type: "longtask", buffered: true });
优化手段:
scheduler.yield()(或 setTimeout 兜底)让出主线程。async function processInChunks(items: string[]) {
for (let i = 0; i < items.length; i++) {
handle(items[i]);
if (i % 50 === 49) await scheduler.yield(); // 让浏览器有机会响应输入
}
}
offsetHeight 和写 style。// ✗ 每次都强制重排
for (const el of els) { el.style.width = el.offsetWidth + 10 + "px"; }
// ✓ 先批量读,再批量写
const widths = els.map((el) => el.offsetWidth);
els.forEach((el, i) => { el.style.width = widths[i] + 10 + "px"; });
aspect-ratiosize-adjust 匹配回退字体度量top/left 做动画,用 transform(合成层,不触发布局)泄漏的典型来源:未解绑的事件监听、未清理的定时器、闭包持有大对象、Detached DOM(DOM 删了但 JS 还引用着)。
排查:DevTools Memory 面板拍两次堆快照(操作前 / 操作后),对比 Detached HTMLElement 与不断增长的构造函数实例数。
// 组件卸载时务必清理
useEffect(() => {
const id = setInterval(tick, 1000);
window.addEventListener("resize", onResize);
return () => {
clearInterval(id);
window.removeEventListener("resize", onResize);
};
}, []);
RN 没有 LCP/INP,关注点是启动、滚动、交互。
| 指标 | 含义 | 优化方向 |
|---|---|---|
| 冷启动时间 | 进程冷启到首屏可交互 | 减少 native 初始化、Hermes 预编译、拆包 |
| TTI | 可交互时间 | 延后非关键 JS 执行、首屏组件精简 |
| 列表 FPS | 滚动流畅度 | FlatList 优化、避免内联函数、图片尺寸匹配 |
| 包体积 | 安装包大小 | 资源裁剪、按需加载 |
详细实践见 优化 APP 启动速度。核心思路与 Web 一致:先测量分段耗时,再针对最长的一段优化。
performance.mark 打点)preload / prefetch / preconnect / dns-prefetch 的区别?