资讯文章 资讯 · NEWS
列表页点一下卡半秒:scheduler.yield 和长任务拆分的落地顺序
2026-10-24 约 1,597 字 预计阅读 4 分钟 yiyunkj
列
长任务的定义很简单:占用主线程超过 50 毫秒的任务。它的危害不在总耗时,而在于这段时间里浏览器什么别的都做不了——不能绘制下一帧,也不能响应用户输入。
所以用户点击筛选按钮后卡半秒,很多时候不是处理点击的代码慢,而是点击之前那个任务还没跑完。浏览器得先把手上的活干完,才能回头处理点击。

图片来源:网络,版权归原作者所有,侵权请联系删除
拆任务的三种写法
最原始的是 setTimeout:把一段工作切成两半,中间 setTimeout(fn, 0)。问题在于,让出之后,你的后半段要排在「已经排好队的其他任务」后面执行。如果队列里有个慢任务,你的后半段可能要等很久。
scheduler.yield() 解决的就是这一点。它是 Prioritized Task Scheduling API 的一部分,Chrome 129 起可用。用法是在 async 函数里让出:
async function myJob() {
myJobPart1();
await scheduler.yield(); // 让出主线程
myJobPart2(); // 续体优先级高于新任务
}
关键差别在最后一行:让出之后的续体会被排到比「新排队的同类任务」更高的优先级。也就是说,你主动让出不会吃亏——浏览器处理完输入和绘制,会优先接着跑你的后半段,而不是先去跑别人刚排进来的任务。
scheduler.postTask 是同一个 API 家族的另一个工具,可以显式指定优先级。它也能拆任务,但续体同样排在已排队任务之后,适合的是「这批活不着急」的场景,而不是「我要让出但还要接着干」。
目录站上最该拆的四段代码
| 位置 | 典型表现 | 拆法 |
|---|---|---|
| 筛选后重渲染 | 一次生成几百张卡片 | 每 50 到 100 条让出一次 |
| 搜索联想渲染 | 候选词多时输入卡顿 | 配合防抖,渲染前先让出 |
| 分类树一次性展开 | 点开全部分类卡住 | 分层渲染,每层之间让出 |
| 第三方脚本初始化 | 页面加载完成前主线程被占 | 延后到空闲时,必要时拆包 |
落地顺序
第一步,先量。Performance 面板里找右上角带红色标记的长任务,或者用 Long Tasks API 在生产环境采样。别凭感觉改,很多自以为慢的代码其实只跑了 3 毫秒。
第二步,定位到具体函数。DevTools 从 Chrome 某几个版本起会在长任务里标出归属脚本,第一方脚本能直接定位到函数。第三方脚本只能看到来源域名,那种就得靠延后加载或替换方案。
第三步,在合适的边界插入让出。不是越多越好——每次让出都有调度开销,一个 60 毫秒的任务切一刀就够,切成十段反而更慢。经验是在循环里累计执行超过 50 毫秒就让出一次。
第四步,兼容处理。scheduler.yield 目前只在 Chromium 系可用,Safari 较新的版本才跟上,Firefox 还没支持。所以要做特性检测,不支持时回退到 setTimeout:
const yieldToMain = () =>
'scheduler' in globalThis && 'yield' in scheduler
? scheduler.yield()
: new Promise(r => setTimeout(r, 0));
两个容易走偏的方向
一是把所有同步循环都改成异步。让出点只在「用户可能想在这段时间交互」的地方才有价值,纯后台的批量计算让不让出用户也感知不到,还白白增加代码复杂度。
二是把 navigator.scheduling.isInputPending 当成万能信号。它是 Chromium 才有的接口,且只适合在长循环里做「是否该让出」的判断,不能替代让出本身。有 scheduler.yield 的场景优先用它,语义更清楚。
怎么确认改好了
实验室数据只能验证单次:Performance 面板里看那个长任务是否消失了、交互到下一帧的时长是否降下来。
真实效果要看 CrUX 的 INP 字段数据,那是 28 天滚动的 p75,只统计真实 Chrome 用户。开发机上跑得再快,也换不来这个数字的变化——这也是为什么改完至少要看一个月的数据再下结论。