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 长任务识别流程图
下面这张图,是我自己总结的识别流程。你可以照着这个思路排查。
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% 的问题了。