如何用 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 来提升网页性能。

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

相关推荐
用户22264598943412 小时前
CSS单位全解析:从像素到视口的响应式设计
前端
Mapmost2 小时前
【实景三维】还再为渲染发愁?手把手教你大场景如何实现“精细”与“流畅”平衡!
前端
钱多多8102 小时前
Vue版本降级操作指南(解决依赖冲突与版本不一致问题)
前端·javascript·vue.js·前端框架
San302 小时前
深度解析 React 组件化开发:从 Props 通信到样式管理的进阶指南
前端·javascript·react.js
AAA阿giao2 小时前
深度解析 React 项目架构:从文件结构到核心 API 的全面拆解
前端·javascript·react.js
LYFlied2 小时前
Vue3虚拟DOM更新机制源码深度解析
前端·算法·面试·vue·源码解读
1024肥宅2 小时前
综合项目实践:小型框架/库全链路实现
前端·面试·mvvm
文心快码BaiduComate2 小时前
Spec模式赋能百度网盘场景提效
前端·程序员·前端框架
QT 小鲜肉2 小时前
【Linux命令大全】001.文件管理之find命令(实操篇)
linux·运维·前端·chrome·笔记