跳到主要内容

运行时卡顿排查

场景

用户反馈页面在操作过程中卡顿掉帧(如滚动卡顿、输入延迟、动画不流畅),你会怎么排查和优化?

分析思路

理解卡顿的本质

浏览器渲染目标是 60fps(每帧 ≈ 16.67ms)。如果一帧的工作耗时超过这个时间,就会掉帧,用户感觉到卡顿。

一帧 16.67ms 内要完成以上所有步骤。JS 执行时间过长是最常见的卡顿原因。

第一步:Performance 面板定位

DevTools → Performance → 录制操作 → 停止录制

关键观察点:

  1. Main 线程:黄色三角标记的是长任务(Long Task, > 50ms)
  2. 帧率行:红色帧 = 掉帧,绿色 = 流畅
  3. 火焰图:展开看具体是哪个函数耗时长
  4. Summary 面板:看 Scripting / Rendering / Painting 时间占比
// 编程方式监控长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn(`⚠️ Long Task detected: ${entry.duration.toFixed(0)}ms`, entry);
}
}
});
observer.observe({ type: 'longtask', buffered: true });

第二步:常见卡顿原因与解决方案

原因 1:JS 长任务阻塞主线程

❌ 同步处理大量数据
function processLargeList(items: DataItem[]) {
// 一次性处理 10000 条,主线程阻塞数百 ms
const result = items.map((item) => heavyCompute(item));
renderList(result);
}
✅ 时间切片(Time Slicing)
function processWithTimeSlicing(items: DataItem[], chunkSize = 100) {
let index = 0;

function processChunk() {
const chunk = items.slice(index, index + chunkSize);
chunk.forEach((item) => heavyCompute(item));
index += chunkSize;

if (index < items.length) {
requestIdleCallback(processChunk); // 空闲时处理下一批
} else {
renderList(items);
}
}

processChunk();
}
✅ 使用 scheduler.yield()(实验性 API)
async function processWithYield(items: DataItem[]) {
for (let i = 0; i < items.length; i++) {
heavyCompute(items[i]);

// 每处理 100 个,让出主线程
if (i % 100 === 0 && 'scheduler' in window) {
await (window as any).scheduler.yield();
}
}
}
✅ Web Worker 处理密集计算
// worker.ts
self.onmessage = (e: MessageEvent<DataItem[]>) => {
const result = e.data.map((item) => heavyCompute(item));
self.postMessage(result);
};

// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(largeDataSet);
worker.onmessage = (e) => {
renderList(e.data);
};

原因 2:频繁触发重排(Reflow)

❌ 强制同步布局(Layout Thrashing)
function resizeElements(elements: HTMLElement[]) {
elements.forEach((el) => {
// 读 → 写 → 读 → 写... 每次读都强制浏览器重排
const width = el.offsetWidth; // 读(触发重排)
el.style.width = width * 2 + 'px'; // 写
});
}
✅ 批量读,批量写
function resizeElements(elements: HTMLElement[]) {
// 先批量读取
const widths = elements.map((el) => el.offsetWidth);

// 再批量写入(只触发一次重排)
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
}
✅ 使用 requestAnimationFrame
function batchDOMUpdates(updates: Array<() => void>) {
requestAnimationFrame(() => {
updates.forEach((update) => update());
});
}

原因 3:滚动性能差

❌ scroll 事件未节流
window.addEventListener('scroll', () => {
// 每次滚动都执行:一秒可能触发 60+ 次
heavyScrollHandler();
});
✅ 节流 + passive
function throttle<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
let timer: ReturnType<typeof setTimeout> | null = null;
return ((...args: unknown[]) => {
if (!timer) {
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
}
}) as T;
}

window.addEventListener('scroll', throttle(scrollHandler, 16), { passive: true });
✅ CSS 优化滚动
.scroll-container {
/* 告诉浏览器内容可能随时滚动变化 */
will-change: transform;
/* 使用合成层 */
transform: translateZ(0);
/* 内容裁剪优化 */
contain: layout style paint;
}

原因 4:动画不流畅

❌ JS 定时器动画
setInterval(() => {
element.style.left = parseFloat(element.style.left) + 1 + 'px'; // 每帧触发重排
}, 16);
✅ 只用 transform 和 opacity
// JS
element.style.transform = `translateX(${x}px)`;

// CSS(更好)
// .animated { transition: transform 0.3s ease; }
✅ CSS 动画使用 GPU 加速属性
.smooth-animation {
/* 只使用 compositor-only 属性 */
transform: translateX(100px);
opacity: 0.8;
/* 避免 top/left/width/height 等触发重排的属性 */
}

原因 5:React / Vue 过度重渲染

React 重渲染排查
// 开发环境使用 React DevTools Profiler
// 或使用 why-did-you-render 库

// ✅ 用 React.memo 避免不必要的重渲染
const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
});

// ✅ 用 useMemo 缓存计算结果
function Dashboard({ data }: { data: DataItem[] }) {
const processedData = useMemo(() => {
return data.map((item) => expensiveTransform(item));
}, [data]);

return <Chart data={processedData} />;
}

第三步:排查流程总结


常见面试问题

Q1: 什么是长任务?如何检测?

答案

长任务是指执行时间超过 50ms 的任务。50ms 阈值来源于 RAIL 模型(Response 100ms + 一帧 16ms 的余量)。

检测方式:

  1. Performance 面板:黄色三角标记
  2. PerformanceObserver APIobserve({ type: 'longtask' })
  3. Long Animation Frames API(新标准):observe({ type: 'long-animation-frame' })

Q2: 滚动卡顿的常见原因有哪些?

答案

  1. scroll 事件处理太重:未节流、做了大量计算
  2. 未使用 { passive: true }:浏览器等待 preventDefault() 判断,延迟滚动
  3. DOM 节点过多:数千个列表项 → 用虚拟列表
  4. 触发重排重绘:滚动时读取 offsetTop 等 → Layout Thrashing
  5. 大面积重绘position: fixed 元素 + box-shadow
  6. 图片解码阻塞:大量图片同时加载 → 用 loading="lazy" + decoding="async"

Q3: 如何区分 CPU 瓶颈和 GPU 瓶颈?

答案

维度CPU 瓶颈GPU 瓶颈
Performance 表现Scripting/Rendering 时间长Painting/Composite 时间长
典型场景大量 JS 计算、DOM 操作合成层过多、大面积重绘
DevTools 工具Performance 面板 Main 线程Layers 面板查看合成层数量
优化方向代码分割、Worker、算法优化减少合成层、避免层爆炸

在 Performance 面板的 Summary 中看 Scripting vs Painting 时间比就能大致判断。

相关链接