核心网页指标-INP

什么是 INP

今天听组内同学分享,了解到一个前端概念 INP(Interaction to Next Paint),它用来衡量互动性,为了提供良好的用户体验,网页的 INP 应不超过 200 毫秒。

INP 指标通过观测网页在用户访问期间发生的所有点击、点按和键盘互动的延迟时间,评估网页对用户互动的总体响应情况。最终 INP 值是观测到的最长互动时间,离群值会被忽略。

常见的 INP 优化手段

INP 的通常优化手段是将大任务拆解成小任务,然后通过 setTimeout 调用,保障用户输入的响应。比如一个 200ms 的任务,将它拆分成 4 个 50ms 的任务来执行。

1
2
3
4
5
6
7
8
// 原来的重任务
doHeavyWork();

// 拆小之后
setTimeout(task1, 0);
setTimeout(task2, 0);
setTimeout(task3, 0);
setTimeout(task4, 0);

疑问

到这里移动端的同学是不是突然有些疑问

  1. JavaScript 是单线程的,拆分成小任务,并不能缩短整体的执行时间,这个优化有用吗
  2. 每个任务 50ms,它执行完成之后,下一个任务又会被执行,这中间的间隙能响应用户的操作吗
  3. 两个任务中间的窗口期这么短,会有用户命中吗

解答

问题 1

JavaScript 引擎是单线程,拆分成小任务,并不能缩短时长,甚至因为调度的问题,执行时长还会增加,但这类优化的目标不是“加速”,而是让用户的操作在执行任务的间隙能更快被响应。

问题 2

JavaScript 的 Event Loop 循环,执行完任务会执行其他的,这时候可以执行用户的操作,如果这个用户的操作耗时也比较长,整体 Loop 的执行也会加长,导致下一个 50ms 的任务更靠后执行

问题 3

会的,在这 50ms 执行期间,用户的操作仍能被接收,但是不会被处理,而是延后处理,所以这个优化会保证用户的操作尽快响应,减少卡顿感。

对于在第一个 50ms 用户做的操作,

  • 假如没有优化,要到 200ms 之后才会执行
  • 假如进行了优化,会在第一个 50ms 任务执行完成后执行,因为这个时候进入了下一个 Event Loop 周期,可以处理积压的用户操作了

新的问题 4 - 为什么使用定时器就可以保证每个任务之间有间隔

因为浏览器的定时器是一个单独的线程,setTimeout(task1, 0) 的任务并不会立即执行,而是把任务放入宏任务队列,等主线程空闲时才取出来执行。

浏览器规定,同一宏任务结束后,主线程会先处理 微任务队列(Promise、MutationObserver),再去处理下一个宏任务(下一个 setTimeout)。所以即使设置 0ms,也会有一个事件循环的调度间隙,因为主线程在任务之间会检查是否有用户事件或渲染需要处理。

新的问题 5 - 一次性加进去了 4 个定时任务,不会一次性全部取出执行吗

setTimeout 任务是宏任务,每次事件循环只取一个宏任务执行,即使宏任务队列里有 4 个 setTimeout,也不会一次性全部执行。事件循环每次只取队列头部的一个任务执行。

task1 执行完后,浏览器会:

  • 处理微任务队列(Promise.then 等)
  • 更新渲染(如果需要)
  • 处理用户事件(点击/输入)
  • 然后才执行 task2,依次类推

扩展 - 浏览器的其他线程

  • 主线程:执行 JS、解析 HTML、构建 DOM 树、计算样式、布局、绘制。几乎所有前端逻辑都在这里执行。
  • 合成线程:负责将多个图层合成,决定哪些区域需要重绘。加速滚动与动画渲染,使页面更流畅。
  • Worker 线程:运行在后台的 JS 线程,处理计算密集或网络请求任务,不阻塞主线程。
  • 定时器线程:负责 setTimeout / setInterval 的计时实现。
  • 事件线:管理用户输入事件(鼠标、键盘、触摸),并将其派发给主线程。
  • 动画线程: 用于协调 requestAnimationFrame 与浏览器渲染节奏。

扩展 - 和移动端的区别

  • 移动端是利用多线程,把耗时操作丢到其他线程处理,保证主线程的流畅度
  • 前端也有类似的操作,如使用 Web Worker

扩展 - 为什么是 50ms

这个数字并不是固定标准,而是经验值,因为浏览器通常以 60 帧每秒(16.7ms 每帧)刷新,如果一个任务执行时间太长,就会阻塞渲染导致掉帧,将任务控制在 50ms 以内,能有效保证交互和动画的流畅性。

扩展 - 其他优化手段

除了拆分任务外,INP 也还有其他的一些优化手段,比如说把复杂耗时的计算丢到 Web Worker 当中去处理,这是一个单独的线程,不会阻塞 JavaScript 引擎线程。

其他还有一些更细节的优化,大家也可以自己去搜索,此处就不再赘述了。

参考文章: