如何优化 INP

INP 是 Interaction to Next Paint 的缩写,指的是从用户交互(通常是点击和键盘输入)到网页内容重绘之间的时间间隔。是目前 Google 推行的 Web Vitals 网页性能基准的评价指标之一。它的主要作用是帮助开发者识别和减少主进程阻塞。

  1. 如果在一个网页的生命周期中发生了多次交互,INP 会取阻塞时间最长的那次。
  2. 即使一个元素没有监听任何点击事件,点击也会生成 INP 数据,但是因为不会阻塞主进程,INP 会很低,通常低于 10ms,不影响性能指标。
  3. 如果用户没有操作,或者只用了 scroll 或者 hover 这类 INP 目前无法记录的操作,则会上报为 -1,统计时应该排除。
  4. 因为 1-3,INP 只能在网页关闭前上报,但是这样就不能保证每次都能上报成功。
  5. 因为 1-4,INP 的采样率会明显低于 FCP 和 LCP。
  6. 目前 Google 的建议时:小于 200ms 为优秀,200~500ms为良好,大于 500ms 为差。
  7. INP 与用户设备的 CPU 性能强相关,CPU 性能越弱,INP 越高(差)。移动设备的 CPU 通常比 PC 差得多,H5 的 INP 会明显更高,需要更多的性能优化。
  8. 目前 INP 很可能无法采集到触发元素,最常见的情况时,是当你点击一个按钮后,按钮会消失(比如跳转到了下一个页面)。无法采集到触发元素的情况,就很难排查找到优化方法,只能靠开发者的经验。

以下是我自己总结的比较确定的优化方法。

排查方法

浏览器性能火焰图工具

Firefox 和 Chrome 都提供了性能录制功能,借助生成的火焰图,可以定位比较耗时的代码,从而进行优化:

React 开发工具

开启渲染更新高亮功能,这样在你对 React 应用交互时,可以直观地知道哪些组件被重新渲染了。尤其要注意那些大面积的 React 重新渲染,这经常会导致性能问题。优化方法后面会介绍。

注意:React 的重绘区域并不等于浏览器的重绘区域。有些 React 组件更新可能并不会导致 DOM 更新,也不会导致浏览器重绘。有些很小的 React 组件更新可能会导致更大面积的浏览器重绘(比如一个 Table Cell 的更新,会导致整个 Table 甚至整个页面重绘)。因此它不能完全取代浏览器内置的性能工具。

测试浏览器/WebView性能

有时候我们的网页运行在一些客户端的内嵌 WebView 内,由于客户端的性能比较差导致其中运行的网页 INP 较高。我们可以在同一台设备上分别对 Chrome 浏览器和客户端 WebView 跑 https://browserbench.org/ 测试。它提供了三种测试模型,我们主要用第一种 speedometer 3.0 就可以了:

speedometer 3.0 测试结果,数字越大越好

如果客户端的跑分比 Chrome 浏览器差很多,那就要找找客户端的原因了。

优化方法

避免浏览器原生弹窗

浏览器有几种方法触发原生弹窗:

  • alert()
  • confirm()
  • window.onbeforeunload 或者 window.addEventListener(‘beforeunload’)

但我建议你永远不要用它们!因为从弹窗出现的一刻开始,页面主进程就会被阻塞,从而开始计算 INP,直到你关闭弹窗为止。这种情况下,INP 会高达数秒。

alert 和 confirm 都可以用 React/Vue 组件来替代,通常体验也更好。

而 beforeunload 事件最常用的场景是用户有未保存的数据,避免意外关闭窗口。目前来看并没有其他方法能直接替代,我的建议是:

  1. 在 localStorage 中保存数据,下次打开页面时自动填充数据。
  2. 或者保存草稿到后端数据库,下次打开页面时重新下载草稿。

长列表虚拟化

如果一个 Select 包含了几百项,那么它弹出的一瞬间 CPU 负载会非常高,阻塞主进程,导致卡顿,INP 指标也因此变差。

目前来看最好的解决方法就是采用虚拟滚动技术。

  • Ant Design 5 会根据 options 的数量自动启用虚拟滚动。
  • Fusion Design 则需要 <Select useVirtual> 手动开启。
  • Material UI 自身不支持虚拟滚动,需要结合第三方库自行实现。

降低 React State 层级

常见的 React 应用结构:

  • App
    • Page
      • Header
      • List
        • ListItem

比如我们有一个控制弹窗 open 的 state,如果我们将这个 state 放在 Page 组件,那么当我们打开弹窗时,React 将会重绘整个 Page,其中 List 通常是最重的那个,会造成卡顿。而如果我们将这个 state 放在更低一层的 Header 里,则只会重绘 Header,大幅降低 CPU 负载。

当然如果你只有一个 Page 组件,所有代码都在里面,就很难优化了。对 React 应用来说,适当拆分组件是非常有好处的,不仅能提高代码可读性,也能提升性能。

React Pure Component

React Pure Component 相对普通 Component 渲染次数更少,因此也更能提高性能。

对于 Class Component,转换为 Pure Component 的方法为:

export default class MyPage extends React.PureComponent {
  ...
}

对于 Function Component,转换为 Pure Component 的方法为:

function MyPage() {
  ...
}
export default React.memo(MyPage)

React.lazy() 异步渲染

React.lazy() 最初设计的目的是用于分割代码,避免单个 JS 过于巨大,下载缓慢。由于 React.lazy() 加载的组件必然是异步执行的,我们可以利用这个特性将一个较大较长的页面分批渲染出来,从而减少阻塞时间,降低 INP。

import Header from './header'

const SectionOne = React.lazy(() => import('./section1');
const SectionTwo = React.lazy(() => import('./section2');
const SectionThree = React.lazy(() => import('./section3');

function App() {
  return (
    <div>
      <Header/>
      <React.Suspense fallback={<div>Loading...</div>}>
        <SectionOne/>
      </React.Suspense>
      <React.Suspense fallback={<div>Loading...</div>}>
        <SectionTwo/>
      </React.Suspense>
      <React.Suspense fallback={<div>Loading...</div>}>
        <SectionThree/>
      </React.Suspense>
    </div>
  );
}

但是要注意,异步加载必然会导致 LCP 和 CLS 指标的劣化,开发者要自行权衡利弊,决定哪些组件需要异步渲染。


📅

📁

🏷️

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据