4、JavaScript执行阻塞:长任务(Long Task)的定义、长任务对UI的影响、如何识别长任务

咱们做UI调试,最头疼的是什么?

我个人觉得,是那种「页面突然卡住,点啥都没反应」的瞬间。你明明点了按钮,浏览器却像在装死。过几秒又突然活过来,把所有操作一股脑执行完。

这种体验,说白了就是 长任务(Long Task) 在作祟。今天咱们就把它扒个底朝天。

4.1 什么是长任务?

先给个官方定义:长任务是指任何在主线程上执行时间超过 50 毫秒的任务。

为什么是 50 毫秒?因为 RAIL 模型(Google 提出的性能模型)认为,用户对操作反馈的感知阈值就在 50ms 左右。超过这个时间,用户就会觉得「卡」。

核心公式:

一个任务 = 从任务队列取出 → 执行 → 返回控制权给浏览器

如果这个过程的耗时 > 50ms,它就是长任务。

长任务通常由以下情况引起:

  • 复杂计算:大量循环、数据处理、DOM 操作
  • 解析/编译:加载并执行大型 JavaScript 文件
  • 布局抖动:强制同步布局(比如在循环里读 offsetHeight 再写 style)
  • 垃圾回收:V8 引擎做 GC 时,会暂停所有 JS 执行

我的经验: 我曾经接手过一个报表页面,点击筛选按钮后页面要卡 3 秒。一查 Performance 面板,发现一个循环里反复读取 scrollTop 再设置样式,每次循环都触发了强制布局。这就是典型的「布局抖动」长任务。

4.2 长任务对UI的影响

你想想看,浏览器的主线程是单线程的。它既要执行你的 JavaScript,又要处理用户的点击、滚动、输入,还要负责渲染页面。

当主线程被一个长任务占住时,会发生什么?

  • 点击无响应:事件处理函数排着队,但主线程没空理它们
  • 滚动卡顿:滚动事件被阻塞,页面像在「跳帧」
  • 动画掉帧:requestAnimationFrame 无法按时执行,动画变成幻灯片
  • 输入延迟:键盘输入后,字符要等长任务结束才显示

我习惯用一个比喻来解释:主线程就像一条单车道公路。长任务就是一辆超长卡车横在路中间,后面的所有车(用户操作、渲染任务)都得等着。

用户操作 正常响应(< 50ms) 长任务阻塞(> 50ms)
点击按钮 立即反馈 点击无反应,几秒后突然执行
滚动页面 平滑滚动 卡顿、跳跃、掉帧
输入文字 即时显示 输入后延迟出现
动画播放 60fps 流畅 掉到 10-20fps,明显卡顿

注意: 长任务不仅影响当前操作,还会影响后续操作。因为浏览器的事件队列是先进先出的,一个长任务会拖慢后面所有任务的执行。

4.3 如何识别长任务?

识别长任务,我主要用三个工具。咱们一个一个说。

4.3.1 Chrome DevTools Performance 面板

这是最直观的方法。打开 Performance 面板,录制一段操作,然后看 Main 火焰图。

  • 每个长任务会显示为一块 红色或深色 的横条
  • 横条右上角会标注耗时(比如 120.5 ms)
  • 点击横条,下方会显示调用栈,告诉你哪个函数在「作恶」

我个人习惯先看 Task 那一行。如果看到连续几个超过 50ms 的任务,基本可以断定有性能问题。

4.3.2 Long Tasks API

这是浏览器原生提供的 API,可以直接在代码里监听长任务。

// 监听长任务
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log('发现长任务:', {
      duration: entry.duration,  // 耗时(ms)
      startTime: entry.startTime,
      name: entry.name,
      attribution: entry.attribution  // 哪些脚本导致的
    });
  });
});

observer.observe({ type: 'longtask', buffered: true });

这个 API 的好处是:可以在生产环境收集数据。我建议把它和 RUM(真实用户监控)结合起来,收集真实用户遇到的长任务情况。

避坑指南: 我曾经在生产环境直接用了 Long Tasks API 的 console.log,结果日志量太大,反而影响了性能。后来我改成只采样 1% 的用户,并且把数据上报到后端分析,才解决了问题。

4.3.3 Lighthouse 性能审计

Lighthouse 的 Total Blocking Time (TBT) 指标,就是基于长任务计算的。

TBT 的计算方式:

  • 找出所有长任务(> 50ms)
  • 每个长任务减去 50ms,得到「阻塞时间」
  • 把所有阻塞时间加起来,就是 TBT

比如一个 120ms 的长任务,阻塞时间 = 120 - 50 = 70ms。

Lighthouse 会告诉你 TBT 的分数,以及哪些脚本导致了长任务。

4.4 长任务识别流程图

下面这张图,是我自己总结的识别流程。你可以照着这个思路排查。

长任务识别流程图 用户反馈页面卡顿 打开 Performance 面板录制 有红色长任务? 是 点击长任务查看 调用栈和耗时 否 检查其他原因(网络/渲染) 用 Long Tasks API 生产验证 定位并优化长任务 提示 火焰图中红色横条 就是长任务 点击可看调用栈

4.5 实战:用代码模拟长任务

光说不练假把式。咱们写一段代码,模拟一个长任务,然后用工具抓出来。

// 模拟一个长任务:计算大量数据
function simulateLongTask() {
  const start = performance.now();
  
  // 故意做大量无意义计算
  let result = 0;
  for (let i = 0; i < 50000000; i++) {
    result += Math.sqrt(i) * Math.sin(i);
  }
  
  const end = performance.now();
  console.log(`任务耗时:${end - start}ms`);
  
  // 更新UI
  document.getElementById('result').textContent = `计算结果:${result}`;
}

// 点击按钮触发
document.getElementById('btn').addEventListener('click', simulateLongTask);

这段代码在 Chrome 里跑,大概率会超过 50ms。打开 Performance 面板录制,点击按钮,你会看到火焰图上出现一块红色横条,那就是长任务。

关键点: 长任务的本质是 主线程被长时间占用。识别长任务只是第一步,更重要的是找到「为什么这个任务这么长」,然后想办法拆分它。

4.6 小结

嗯,咱们把长任务的核心概念捋了一遍:

  • 定义:主线程上超过 50ms 的任务
  • 影响:阻塞用户交互、滚动、动画、输入
  • 识别方法:Performance 面板、Long Tasks API、Lighthouse TBT

我个人觉得,识别长任务就像给页面做「体检」。你不需要每次都把所有工具用上,但至少要知道怎么查。下次遇到页面卡顿,先打开 Performance 面板看看有没有红色横条——这已经能解决 80% 的问题了。