如何用 Chrome DevTools 定位 Long Task:一份从零到实战的排查笔记
在上篇我们已经介绍过如何记录和分析页面性能指标,但是只是发现网页卡顿现象还不够,我们还需要进一步定位卡顿的具体原因,才能有针对性地进行优化。
首先我们得了解为什么网页会卡顿。
网页卡顿通常是由于主线程被长时间占用,导致浏览器无法及时响应用户的交互操作。这些长时间影响主线程的运行任务,会被 Chrome DevTools 提供了 Performance 面板捕捉到,来帮助我们识别这些长任务(Long Tasks),从而找出性能瓶颈。
什么是 Long Task?
Long Task 是指在主线程上执行时间超过 50 毫秒的任务。当主线程被长时间占用时,浏览器无法及时响应用户的交互操作,导致页面卡顿。通过识别和优化这些 Long Task,可以显著提升页面的响应速度和用户体验。
环境准备
windows 10 + Chrome 143.0.7499.147
1. 搭一个可复现的 Long Task Demo
为方便讲解,我们先实现了一个典型的卡顿页面:
- 页面有一个 canvas 小球左右匀速运动,并实时显示 FPS。
- 提供"制造卡顿"按钮,点击后在主线程执行同步的重计算:
- 大数组 sort(O(n log n))
- 加上 while 忙等 + 数值迭代循环,确保单次执行 300--700ms。
- 连续执行 5 次,动画明显掉帧,点击响应也变慢。
这个 demo 有两个关键点:
- 可视化:肉眼能看到动画卡顿,方便和性能曲线一一对应。
- 可定位:核心耗时集中在一个明确函数 blockMainThread 里,便于在 DevTools 中识别。
下面是一个简化版的 Demo 代码示例(完整代码可以参考项目中的 page-long-task.html):
html
<button id="btn-stutter">制造卡顿(5 次)</button>
<canvas id="c" width="400" height="200"></canvas>
<script>
const canvas = document.getElementById("c");
const ctx = canvas.getContext("2d");
let x = 20,
dir = 1;
// 简单小球动画,用来观察掉帧
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(x, canvas.height / 2, 18, 0, Math.PI * 2);
ctx.fill();
x += dir * 3;
if (x > canvas.width - 20) dir = -1;
if (x < 20) dir = 1;
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
// 核心:人为制造一个 300--600ms 的长任务
function blockMainThread(minMs = 300, maxMs = 600) {
const target = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
const start = performance.now();
const arr = new Array(12000).fill(0).map(() => Math.random());
arr.sort((a, b) => a - b);
while (performance.now() - start < target) {
let v = 0;
for (let i = 0; i < 3000; i++) {
v += Math.sqrt(i) % 1.2345;
}
}
}
document.getElementById("btn-stutter").onclick = () => {
for (let i = 0; i < 5; i++) {
blockMainThread(350, 650);
}
};
</script>
2. 用 Performance 面板捕获长任务
在 Chrome 打开 Performance 面板进行一次完整录制的标准步骤:
- 打开 Performance 面板,点击 Record 开始录制。
- 回到页面,点击"制造卡顿"按钮。
- 等到动画恢复流畅后点击 Stop 停止录制。
- 观察几个关键轨道:
- Frames:是否有红线和 FPS 明显下降。
- Main:是否出现宽度大于 50 ms 的长紫/黄块(Long Task)。
- Interactions:是否能对齐上用户的点击等交互事件。
任意单个主线程任务耗时 超过 ~50 ms,都会被视为 Long Task,它会阻塞渲染和输入事件。
我们看下我们 performance 录制的结果:

上面结果中,我们可以在 Bottom-up 面板中,时间主要是消耗在 blockMainThread,并且指向代码位置
3. 解读 Bottom-up 面板
3.1 Bottom‑up 是什么
- Bottom‑up 顾名思义,从高到低按"谁最耗时"聚合排序,从热点函数开始向上汇总。 对比:
- Call tree:从入口(事件、任务)往下看调用顺序。
- Bottom‑up:从最耗时的叶子函数往上看"被谁调用"。
它非常适合用来回答:"这一段时间里,CPU 主要花在了哪些函数上?"
3.2 Self time vs Total time
- Self time:函数"自身"执行的独占时间(不含子调用)。
- Total time:函数自身 + 所有子调用的累计时间
例如上面的例子,我们可以看到 blockMainThread 函数 Self Time 和 Total Time 占用了这次长任务的绝大部分时间。

- 长任务耗时:2614.4ms
- blockMainThread 耗时: 2605ms
blockMainThread 几乎和长任务时间一致了,是导致卡顿产生的主要原因。
在新版的 chrome 中,甚至可以看到每段代码的具体耗时,点击 blockMainThread 右侧的源码就可以看到:

耗时主要是消耗在了这段代码
js
while (performance.now() - start < target) {
let v = 0;
for (let i = 0; i < 3000; i++) {
v += Math.sqrt(i) % 1.2345;
}
}
小结:
- Self 高、Total≈Self → 重活都在函数本体里(如大循环、忙等、同步解析)。我们的 blockMainThread 就是这种情况。
- Self 低、Total 高 → 它是"调度/包装层",真正热点在子函数里(例如一个封装函数里面调了排序、布局、GC 等)。
在 Bottom‑up 中:
- 先按 Total 或 Self 排序,看前几名热点函数。
- 若 Self≈Total,优先怀疑这个函数本体的实现。
- 若 Self 低而 Total 高,展开看子节点或切到 Call tree 继续追踪。
4. 解读 Call tree 面板
Call tree 是什么
以树形结构查看函数的子调用和耗时,面板展示信息基本和 Bottom-up 一致。也可以发现页面耗时在哪里发生,例如上面的截图我们通过一步步展开,查看 self time 耗时高的事件是什么

实战
上面我们用 demo 演示了如何调试和发现导致网页卡顿的基本操作,实际上项目会比 demo 要复杂一些,接下来我会以实际项目更近一步演示如何发现卡顿以及解决。
背景
公司有一个 SaaS 项目经常被用户吐槽很卡,在我们经过了一系列的埋点后,发现页面确实卡顿率高达 40%
目标
找出页面为什么引起卡顿的 long tasks,并且优化。
环境介绍
- windows 10
- Chrome 143.0.7499.147
- 项目 react 16.9 + element-react 1.4.34
这个项目使用的 element-react 组件库已经非常旧了,并且已经很久都不更新了,初步怀疑是 element-react 组件导致的卡顿
1. 利用 performance 进行收集数据
根据上面 long-task 调试方法:
- 打开 performance 面板,开始录制
- 打开目标页面
- 页面准备好,数据加载后,停止录制。
在我的项目中可以看到页面 main time-line 中确实有非常多的 long-task

2. 根据 performance 分析卡顿
在 Bottom-up 面板中,选择一个较大 long task,我们可以发现其中耗时较大的事件

其中 mask,measure 时间耗时非常高,long task 1458.6ms, mask 自己耗时占 730ms,measure 自己耗时 315ms,但是这个事件无法展开,也无法定位到源码,那怎么产生这两个事件呢?
经过查阅 chrome文档,才得知无法展开的事件属于浏览器内建(native)API,调用栈常被显示为"[unattributed]"或只到内建层,尤其在没有 SourceMap、被第三方库间接调用、或被 Ignore List/Blackbox 处理时,就看不到上层用户代码。
在 bottom-up 中,确实还有大量的其他内建 api 耗时,比如,Minor GC,appendChild。
上方截图中 mask,measure 是 performance api,怀疑是我们使用 performance 记录时,chrome 帮我们加的标记,所以先忽略继续看下一个耗时大的事件,而下一个 self time 耗时大的是 Recalculate style,并且是可以代码定位的,点击代码定位可以看到代码:

初步定位引起到卡顿的原因是 element-react -> table 组件 -> getScrollBarWidth 中计算 dom 宽度
真相大白
虽然定位到 getScrollBarWidth 方法引起的,并且具体是计算 dom 宽度引起的卡顿,但是只是执行计算理应不会导致页面卡顿,是不是还是其他原因导致的卡顿?
继续分析,发现 getScrollBarWidth 方法是在 table 组件的 constructor 方法中调用的
ts
export default class TableLayout extends Component<
TableLayoutProps,
TableLayoutState
> {
static childContextTypes = {
layout: PropTypes.any,
};
constructor(props: TableLayoutProps) {
super(props);
this.state = {
height: props.height || props.maxHeight || null, // Table's height or maxHeight prop
gutterWidth: getScrollBarWidth(), // ar
// ...
};
this.resizeListener = throttle(50, () => {
this.scheduleLayout();
});
}
}
按道理来说,constructor 方法是在组件初始化时调用的,应该是调用了一次 getScrollBarWidth 而已,不会导致卡顿,那为什么还会卡顿呢?
会不会是因为在组件更新时,getScrollBarWidth 被多次调用导致的卡顿?
抱着试试看的心态,我们在 element-table 组件中加了日志,
ts
componentDidMount() {
console.log('%c [ componentDidMount ]-41', 'font-size:13px; background:pink; color:#bf2c9f;')
}
componentWillUnmount() {
console.log('%c [ componentWillUnmount ]-86', 'font-size:13px; background:pink; color:#bf2c9f;')
}
发现确实是在组件更新时被多次调用了

被重载了200次...,相当于执行了200次 getScrollBarWidth中的计算节点宽度。
但是为什么会被重载200次呢?我继续分析 element-table 组件,既然是在constructor中调用到getScrollBarWidth方法,那应该是再往上追溯,应该是在调用elemenet-table组件时的业务逻辑导致重复渲染了多次,element-table 组件是无辜的,不要冤枉element。
然后我在业务代码中调用table组件时,代码是这样写的:
tsx
render() {
return (
<Table
// ....
key={this.key++}
colData={this.colData}
datas={list}
/>
);
}
问题很显然了,key 属性每次 render 时都会自增,导致 element-table 组件被重新渲染了 200 次,而 getScrollBarWidth 方法是在 constructor 中调用的。
解决以及重新记录数据
解决方法方法就很简单了,将 key 去掉。猜测以前加key应该是有些场景,table数据不更新导致table没有重新渲染,所以加了key来强制重新渲染,开发人员已经离职无从考证了,但是现在我们已经找到了问题的根源,就直接去掉 key 属性,重新运行项目。
重新执行 performance,发现已经没有Recalculate style了,long task 去掉mask 和 measure已经不算是long task了。
总结
通过这次实战,我们成功定位并解决了页面卡顿的问题。主要步骤包括:
- 使用 Chrome DevTools 的 Performance 面板捕获页面的长任务(Long Tasks)。
- 通过 Bottom-up 和 Call tree 面板分析长任务的具体原因,定位到耗时函数。
- 结合代码逻辑,找出导致函数多次调用的根本原因。
- 进行代码优化,重新测试确认问题解决。
通过这种系统的方法,我们不仅解决了当前的性能问题,还为未来的性能优化提供了宝贵的经验。希望这篇文章能帮助大家更好地理解和应用 Chrome DevTools 来提升网页性能。
如果觉得有用的话麻烦"👍 赞"支持一下,谢谢!