total blocking time 怎么降:先揪出拖住主线程的长任务
total blocking time 量的是首屏之后主线程被长任务占住多久。这篇讲清它的口径、200 毫秒那条线,以及把长任务拆短的五个动作。

total blocking time(简称 TBT)量的是首屏出现之后,主线程被长任务占住了多久——长任务指任何一个一口气跑超过 50 毫秒、中途不给浏览器喘息机会的脚本片段。页面点一下半天没反应,多半就是它高。Lighthouse 把 200 毫秒以内算好,而大多数「看着沉」的站,这个数会超标好几倍。
读这篇前
你需要能跑 Lighthouse(Chrome DevTools 里直接跑,或用命令行版),并且能改到页面加载的脚本。不需要构建流程。这篇假设你已经知道浏览器把 JavaScript 放在一条主线程上、一个任务跑完才轮到下一个;interaction to next paint 讲的是用户真正感受到的响应,这篇讲工具报出来的那个数。想先看数字长什么样,我们那篇 24 个首页的 total blocking time 实测用的是同一套跑法。
total blocking time 为什么会高
浏览器只有一条主线程,它必须把当前这段 JS 跑完,才轮得到下一件事——包括重绘、滚动、响应点击。一个任务一旦超过 50 毫秒就算长任务;TBT 把这些长任务里超出 50 毫秒的那部分时长加起来。所以 TBT 量的不是你发了多少脚本,而是你最长的那么几段脚本一口气跑了多久、中途有没有让开。
它的计时起点是首次绘制(FCP)那一刻,不是导航开始,也不是第一个字节到达。这一点很容易看错:一个首屏出现得很快、随后卡住的页面,TBT 会把卡住的那段全算进去;而一个从头到尾都慢但没卡住的页面,TBT 反而可能不高。它和「用户等了多久」不是一回事,它是「用户在能看见东西之后,还要被冻多久」。
这个区别决定了该往哪儿查。一个站可以把 JS 发到一个兆,只要工作被切成一段段短的,TBT 照样好看;反过来,一个站总共才发几十 KB,却有一个函数一口气跑一秒,TBT 就会很难看。下面这张表把「什么算长任务、什么不算」分开。
| 在做的事 | 算进 TBT 吗 | 为什么 |
|---|---|---|
| 不到 50 毫秒的任务 | 不算 | 没跨过 50 毫秒那道线 |
| 一个 300 毫秒的任务 | 算 250 毫秒 | 只把超过 50 毫秒的部分算进去 |
| 连着的十个 40 毫秒任务 | 不算 | 每个都让开了,没有一个够长 |
| 第三方标签跑了 400 毫秒 | 算 350 毫秒 | 第三方代码和自己的一样占这条线程 |
| 解析 HTML 和 CSS | 不算 | TBT 只算主线程上的脚本,不含样式表解析 |
TBT 不看你的脚本发了多少,只看你最长的那个任务拖了多久才松手。
由此有两条推论。第一,最值钱的修法通常是揪出一两个长任务,而不是到处抠体积。第二,TBT 是在一台机器、一种节流档位下跑出来的 lab 指标,它只是响应速度的代理,不是你真实读者体验到的响应——真实的那一面归 interaction to next paint 管。
按这个顺序做
五步,每步都有一个能验证的「做完了」标志。第一步免费,它决定后面几步值不值得做。
- 先量,别急着改。把页面丢给 PageSpeed Insights 或 Lighthouse 命令行,记下 TBT,并记下解释它的两个审计项:「Minimize main thread work」和「Reduce unused JavaScript」。做完了的标志是手里有一个数、还有被点名的几段长脚本,而不是一句「感觉挺慢」。
- 把第三方的账挑出来。在报告的主线程明细里看有多少脚本时间算在你控制不了的域名头上——标签管理器、客服弹窗、A/B 工具、广告像素。做完了的标志是你能说出阻塞时长里有多大一块是别人写的代码。
- 砍掉或推迟第三方脚本。统计、客服这类改到首次交互之后再加载,说不清价值的直接删。head 里一个同步标签,自己就能加上几百毫秒阻塞。做完了的标志是第 2 步那块第三方占比降下来了。
- 把自己写的长任务拆开。不需要在首次绘制前跑的工作挪进
idle回调,一个重循环切成几批、批与批之间让出线程,首屏以下的组件推迟 hydrate。做完了的标志是报告里最长的那个任务掉到 50 毫秒以下。 - 复测,把前后两个数都存好。用同样的节流档位再跑一遍,和第 1 步比。做完了的标志是你能说清改了什么、差了多少,并且两次读数都留了档。
交付物:一张长任务分诊表
下面这张表是我们把一份 Lighthouse 报告变成「一个动作」用的。把你报告里最长的那个任务对到某一行,照那一行修。对得上两行就优先做第三方那一行,它通常更便宜。
| 最长的任务 | 多半是什么 | 先怎么修 |
|---|---|---|
| 标签管理器或统计 | head 里的第三方代码 | 推迟到首次交互后,或直接去掉 |
| 框架的 hydrate | 把静态内容用前端渲染出来 | 改成 HTML 里就渲染,再分批 hydrate |
| 一个大的数据变换 | 一个不让开的循环 | 把工作分批,批与批之间让出线程 |
| 客服或支持组件 | 加载即初始化 | 改成点击时才加载 |
| 未知,藏在某个 bundle 里 | 运行时才用到的无用依赖 | 用 coverage 面板找出来再删 |
不想开 DevTools、只想拿到那个数,可以跑下面这条命令:它无头跑一遍 Lighthouse,只把 TBT 和两个解释项打成 JSON。需要机器上有 Node,不需要任何账号。
npx --yes lighthouse "$URL" --only-categories=performance \
--preset=desktop --output=json --output-path=/tmp/lh.json --quiet \
&& node -e 'const a=require("/tmp/lh.json").audits;
console.log("TBT ms:", a["total-blocking-time"].numericValue);
console.log("main-thread ms:", a["mainthread-work-breakdown"].numericValue);
console.log("unused JS ms:", (a["unused-javascript"].details||{}).overallSavingsMs);'
做错了会怎样
三种常见翻车,全都不报错,所以能拖很多年。
- 冲着分数改,不冲任务改。Lighthouse 分数是个加权合算,TBT 只是其中一项。分数涨了几分、最长的任务还是 400 毫秒,真实交互一点没变。要盯的是单次任务有多长,不只是分数。
- 把首屏依赖的脚本也推迟了。如果首屏或头部渲染靠脚本,推迟它等于把画面往后推。这种情况应该把渲染搬回 HTML,而不是往后挪。
- 只跑一次就下结论。共享机器上的 lab 跑动有噪声。两次跑差不到 100 毫秒,很可能就是噪声。改一处、跑两次,只有重复出现的差值才算数。
常见问题
total blocking time 多少算好
在节流的移动端档位下,Lighthouse 把 200 毫秒以内算好、600 毫秒以上算差。把 200 当及格线而不是目标:一个刚好卡在 200 以内、每次点击还要堵 180 毫秒的页面,用起来照样发涩,所以继续削最长的那个任务。
TBT 影响排名吗
它本身不是排名因素。它不是三项 Core Web Vitals 之一,Google 也没有给 TBT 单独定过排名权重。它的意义在于:它是一项正式指标在 lab 里的代理,而降低它的那些动作——少几个第三方脚本、更小的任务——通常同时改善了真实页面体验。
为什么页面加载挺快,TBT 却很高
因为「加载」和「响应」是从两个不同时刻算起的。页面可以很快画出首屏,然后跑一个长脚本把线程冻住,这正是 TBT 要抓的。先看主线程明细,别急着动图片和字体:这类问题几乎都出在脚本上。
TBT 能替代 interaction to next paint 吗
不能。TBT 在 lab 里测,interaction to next paint 来自真实访问;lab 过关不保证 field 过关。用 TBT 快速定位长任务,等改动上线后再看真实数据确认。
边界说清楚:这篇讲的是把阻塞线程的单次任务缩短,不是把整页变小或变快下,那是另一个问题。我们没有测过某一次改动在你站上能把 TBT 推多少,因为它取决于你的脚本和你的机器;在一台快机器、又不做节流时,同一个页面的 TBT 可能接近零,什么都说明不了。当哪天主线程明明闲着、页面还是慢,就说明瓶颈不在脚本执行上,该换一个指标去量了。把这件事和 QueryWin 怎么记录页面表现放在一起看没问题,但请先重跑第 1 步。
本文属于 QueryWin 实操手册 · 第 2 阶


