长任务拆分是让耗时逻辑不独占主线程超50ms的实操技术:先定位真实卡点,再按任务性质选策略(如Web Worker、requestIdleCallback、setTimeout切片),善用宏/微任务差异,动态调粒度,并用PerformanceObserver等验证效果。

直接上手练,别从概念开始背。长任务拆分不是理论题,是浏览器主线程“喘气”的实操技术——核心就一条:让耗时逻辑不独占主线程超过 50ms,给渲染和交互留出空隙。
先盯住真实卡点,再动手拆
别一上来就写 requestIdleCallback。先确认是不是真有长任务:
- 打开 Chrome DevTools → Performance 面板 → 录制一次典型操作(比如点开列表页、提交表单),看 Scripting 区域有没有连续 >50ms 的红色块
- 用
console.time('init')+console.timeEnd('init')包住可疑初始化函数,实测耗时 - 留意用户反馈:“点按钮没反应”“滚动突然卡住”“输入延迟明显”,这些往往是长任务在后台吃掉主线程
按任务性质选拆法,不是所有都用同一种
不同场景,调度策略完全不同:
-
启动初始化类:必须同步的(如挂载根节点、路由配置)立刻做;非首屏预加载、埋点 SDK 等,用
requestIdleCallback或setTimeout(..., 0)延后 -
海量数据渲染类(日志、日历、表格):按“批”切,每批 20–50 条(DOM 创建)或 1000–5000 条(纯计算),配合
DocumentFragment批量插入 - CPU 密集型计算类(JSON 解析、正则过滤、坐标运算):直接扔进 Web Worker,主线程只管分片传数据、收结果
-
循环遍历类(索引 5 万条本地数据):用
setTimeout做切片边界,每轮处理固定数量(如 200 条),处理完再调度下一轮
用对宏任务和微任务,别混着用
这是最容易翻车的细节:
-
宏任务(
setTimeout,setImmediate):真正让出主线程,浏览器能趁机渲染、响应点击——它是切片的“安全锚点” -
微任务(
queueMicrotask,Promise.then):紧接当前同步代码执行,不会释放控制权——适合批内快速衔接,比如“校验完这批→格式化这批→存进临时数组”,但不能用来替代setTimeout做主切片 - 避免
Promise.then链式切片:错误未捕获会中断整个流程;也别用setInterval控制节奏,它不感知空闲时间,容易抢资源
动态调粒度,别写死数字
同一段代码,在 iPhone SE 和 MacBook Pro 上表现天差地别:
- 冷启动时参考
navigator.deviceMemory和navigator.hardwareConcurrency设初始切片大小(如低端设备默认每批 ≤50 条) - 运行中用
performance.now()打点测真实耗时,滑动窗口取中位数,动态调整下一批数量 - 监听
document.visibilityState === 'hidden'或滚动事件,检测到压力升高就主动收紧粒度(比如砍半) - 设硬边界:最小 10 条,最大 200 条,防止震荡或过度切分
验证效果,别只看代码写了没
拆完不验证,等于没拆:
- 用
PerformanceObserver监听longtask类型,对比优化前后数量、平均时长、总占比 - 集成
web-vitals上报 INP(Interaction to Next Paint),重点关注 P75 值是否降到 150ms 内 - 手动测“最差路径”:用低端安卓机快速连点、滚动,看 FPS 是否稳定、提示是否及时出现
- 在火焰图里找原本连续的大红块,是否被切碎、是否避开用户操作后的黄金 100ms 窗口


















