如何用 Chrome DevTools 定位 Long Task:一份从零到实战的排查笔记

如何用 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

为方便讲解,我们先实现了一个典型的卡顿页面:

  1. 页面有一个 canvas 小球左右匀速运动,并实时显示 FPS。
  2. 提供"制造卡顿"按钮,点击后在主线程执行同步的重计算:
    1. 大数组 sort(O(n log n))
    2. 加上 while 忙等 + 数值迭代循环,确保单次执行 300--700ms。
  3. 连续执行 5 次,动画明显掉帧,点击响应也变慢。

这个 demo 有两个关键点:

  1. 可视化:肉眼能看到动画卡顿,方便和性能曲线一一对应。
  2. 可定位:核心耗时集中在一个明确函数 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 面板进行一次完整录制的标准步骤:

  1. 打开 Performance 面板,点击 Record 开始录制。
  2. 回到页面,点击"制造卡顿"按钮。
  3. 等到动画恢复流畅后点击 Stop 停止录制。
  4. 观察几个关键轨道:
    • 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;
  }
}

小结:

  1. Self 高、Total≈Self → 重活都在函数本体里(如大循环、忙等、同步解析)。我们的 blockMainThread 就是这种情况。
  2. Self 低、Total 高 → 它是"调度/包装层",真正热点在子函数里(例如一个封装函数里面调了排序、布局、GC 等)。

在 Bottom‑up 中:

  1. 先按 Total 或 Self 排序,看前几名热点函数。
  2. 若 Self≈Total,优先怀疑这个函数本体的实现。
  3. 若 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 调试方法:

  1. 打开 performance 面板,开始录制
  2. 打开目标页面
  3. 页面准备好,数据加载后,停止录制。

在我的项目中可以看到页面 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了。

总结

通过这次实战,我们成功定位并解决了页面卡顿的问题。主要步骤包括:

  1. 使用 Chrome DevTools 的 Performance 面板捕获页面的长任务(Long Tasks)。
  2. 通过 Bottom-up 和 Call tree 面板分析长任务的具体原因,定位到耗时函数。
  3. 结合代码逻辑,找出导致函数多次调用的根本原因。
  4. 进行代码优化,重新测试确认问题解决。

通过这种系统的方法,我们不仅解决了当前的性能问题,还为未来的性能优化提供了宝贵的经验。希望这篇文章能帮助大家更好地理解和应用 Chrome DevTools 来提升网页性能。

如果觉得有用的话麻烦"👍 赞"支持一下,谢谢!

相关推荐
恋猫de小郭9 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅15 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606116 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了16 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅16 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅16 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅17 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment17 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅17 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊17 小时前
jwt介绍
前端