长任务拆分与调度策略:从 Long Tasks 到 INP 的前端交互提速实战

HTMLPAGE 团队
18 分钟阅读

系统讲解 Long Tasks(>50ms)为什么会拖慢交互、拉高 INP,并给出可落地的拆分与调度策略:把重逻辑移出交互路径、按帧分片、优先级队列、Web Worker、requestIdleCallback、scheduler.postTask 等;附带排障路径与可复制的检查清单。

#INP #Long Tasks #性能优化 #交互性能 #调度 #Web Worker

长任务拆分与调度策略:从 Long Tasks 到 INP 的前端交互提速实战

用户对“卡”的感受通常来自两件事:

  • 点了没反应(交互延迟)
  • 动画掉帧(滚动/拖拽/输入不跟手)

从工程角度看,这两者经常有同一个根因:主线程被长任务占满

长任务(Long Task)通常指:主线程上连续执行超过 50ms 的任务。 一旦主线程被占用,浏览器就没法及时处理输入事件、也没法尽快绘制下一帧,最终表现为 INP 变差。

本文给你一套“从定位到修复”的实战路线:先找到是谁在占主线程,再用拆分与调度把交互路径变短。


1. Long Tasks 与 INP 的关系:为什么“看起来不大”的逻辑也会卡

INP 衡量的是:用户交互到页面下一次绘制的延迟。

当主线程忙时,交互会排队:

  • 输入事件来了,但 JS 正在跑
  • 事件处理函数要等前面的任务结束才能执行
  • 执行完还要等渲染与绘制

所以 INP 的优化核心不是“把事件处理函数写得更短”这么简单,而是:

确保交互发生时,主线程有足够空闲。


2. 定位:先抓到长任务发生在哪里

2.1 DevTools Performance:最可靠

  1. 打开 Performance 面板
  2. 录制一次“卡顿场景”(点击、输入、滚动)
  3. 查看 Main 线程上的长任务(通常会有标记)

你需要回答:

  • 长任务发生在交互前还是交互后?
  • 是某个组件渲染导致,还是某段业务计算导致?
  • 是否第三方脚本/埋点在抢主线程?

2.2 典型长任务来源

  • 大 JSON 解析与遍历
  • 大列表渲染(一次性渲染大量节点)
  • 复杂的 diff / 频繁状态更新
  • 第三方脚本(埋点、广告、客服)

3. 修复策略总览:三种最常用的方向

  1. 移出交互路径:把重逻辑从“点击处理”挪走
  2. 拆分成小片:让每一片都能在一帧内完成
  3. 移出主线程:Web Worker / OffscreenCanvas 等

你不需要一次用齐所有策略,先从最有效的一条开始。


4. 把重逻辑移出交互路径(最优先)

交互路径里应该只做:

  • 必要的状态更新
  • 必要的渲染触发

重计算可以:

  • 提前算(预计算)
  • 延后算(异步/空闲时算)
  • 后台算(worker)

示例(思路示意):

// 点击时只做最小状态切换
function onClick() {
  state.open = true

  // 重计算延后
  queueMicrotask(() => {
    expensiveCompute()
  })
}

这只是示意。更常见的做法是:把重逻辑从点击处理里拆出来,并在“页面空闲”或“数据到达时”提前准备。


5. 拆分成小片:按帧分片(time slicing)

当你必须在主线程完成某个大任务(例如处理大量数据),就要做分片。

5.1 使用 requestAnimationFrame 分片

function processInChunks(items: any[], chunkSize = 200) {
  let index = 0

  function run() {
    const end = Math.min(index + chunkSize, items.length)
    for (; index < end; index++) {
      // 处理 items[index]
    }

    if (index < items.length) {
      requestAnimationFrame(run)
    }
  }

  requestAnimationFrame(run)
}

优点:

  • 每帧做一点
  • 页面不会长时间失去响应

5.2 使用 requestIdleCallback(注意兼容与超时)

function scheduleIdleWork(work: () => void) {
  if ("requestIdleCallback" in window) {
    ;(window as any).requestIdleCallback(work, { timeout: 200 })
  } else {
    setTimeout(work, 0)
  }
}

requestIdleCallback 适合非关键任务(预计算、缓存构建)。


6. 移出主线程:Web Worker 是最稳的工程手段

当任务属于:

  • 数据解析
  • 复杂计算
  • 文本处理

优先考虑 Worker。

示意:

// main
const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module" })
worker.postMessage({ items })
worker.onmessage = (e) => {
  state.result = e.data
}

Worker 的关键收益是:

  • 主线程不被计算占用
  • 交互与渲染更顺

注意:

  • 数据传输要考虑序列化成本(必要时用 transferable)

7. 调度与优先级:让“重要的事”先完成

现代浏览器提供了更丰富的调度能力(支持情况逐步完善),例如:

  • scheduler.postTask(按优先级调度)

即便你不使用这些新 API,也可以自己做一个轻量队列:

  • 高优先级:交互相关
  • 中优先级:首屏渲染后需要的任务
  • 低优先级:预加载、预计算、日志上报

核心原则:

不要让低价值任务占用交互窗口期。


8. 可复制的排障清单(INP 变差时先做这些)

  • Performance 录制:找到长任务与来源
  • 检查交互路径:点击/输入处理是否做了重计算
  • 检查渲染:是否一次性渲染了大量 DOM
  • 检查第三方脚本:是否在关键交互时段运行
  • 对重计算做分片或移入 worker
  • 对非关键任务延后到 idle

结语:INP 优化的本质是“让主线程更空闲”

当你把长任务拆掉、把重逻辑移出交互路径、把计算移出主线程,交互体验会明显改善。

建议你把这篇与以下两篇组合成一条闭环:

  • TTFB(服务端响应)→ 影响加载
  • 资源优先级(Priority Hints)→ 影响 LCP
  • 长任务拆分(调度策略)→ 影响 INP