分享 五个 超级有效优化 React 中 INP 的技巧 😊😊😊

946 阅读10分钟

原文:5 tips to effectively optimize INP in React

在本篇文章中,我们将讨论一些优化技术,用于改善基于 React 构建的网站的 Core Web Vitals 指标。

我们将主要关注 Interaction to Next Paint (INP) 这个指标,也就是交互响应速度。优化基于 React 构建的网站速度,涉及到优化 JavaScript 的长任务。这些长任务与 React 的内部工作机制密切相关。

Interaction to Next Paint (INP):这是一个衡量用户与网站交互后,页面渲染响应的指标。换句话说,它表示用户触发交互(例如点击、输入等)后,页面响应并显示的速度。

React 上的 Web 是否自动变快了?错了!

在内部,React 使用了几种巧妙的技术来提高网站或 Web 应用程序的速度。它能够高效地只更新和渲染那些数据发生变化的组件。同时,Hook 系统能够部分地保护网站免受不必要的布局重新计算和所谓的"布局抖动"的影响。

这是否意味着基于 React 构建的网站会自动很快?这是许多开发者的常见看法。答案很简单:即使是基于 React 构建的网站,也需要进行优化。

HTTP 档案 的数据中也可以看出这一点:

20250206160323

数据显示,基于 React 构建的网站比基于 PHP 构建的网站更少符合 Core Web Vitals 指标。

虽然 React 中的声明式组件使得代码更具可预测性并且更易于理解,但不谨慎地操作状态或组件数量几乎肯定会导致交互性变慢。

React 就像其他任何工具一样,性能的好坏取决于开发者对它的掌握程度。

接下来,我们将介绍一些基于我们工作经验的 React 优化建议。如何保持 React 代码的可控性?

减少 DOM 大小

调整 DOM 大小并进行优化是一个基本要求。如果 DOM 太大(元素太多或嵌套太深),可能会导致性能下降、渲染变慢以及内存负载增加。

在 React 中,这一建议更为重要。元素越少,意味着组件越少,进而意味着需要下载和处理的 JavaScript 也越少。

可以通过 Google Chrome 控制台快速验证 DOM 大小。只需输入以下脚本:

document.querySelectorAll("*").length;

20250206160833

Google 建议 DOM 元素的最大数量为 1,400 个。这是一个相当严格的限制,尤其对于像电商网站或应用这样的较大网站来说。在我们的经验中,2,500 个 DOM 元素浏览器仍然可以处理得比较流畅。一旦 DOM 元素的数量超过这个限制,问题就会迅速变得复杂起来。

而在显示当中,是远远低于 1400 的。

关于 DOM 大小,最有效的方法是删除和使用懒加载。

不在 DOM 中的元素不需要被渲染,浏览器可以得到休息。因此,首先关注那些较大且对 SEO 不重要的组件。将它们从 DOM 中移除,并使用懒加载来加载它们:

import { lazy } from "react";

const MarkdownPreview = lazy(() => import("./MarkdownPreview.js"));

为了最大化效果,当用户需要时或者组件出现在视口中时再加载这些组件。典型的组件包括:地图、图表、可视化工具、WYSIWYG 编辑器,还有表单和过滤器,这些都应该在第一个可视区域之外进行懒加载。

简化组件结构

许多组件通常在 UI 中创建。借助当今的现代 HTML 和 CSS 功能,我们需要越来越少的仅起布局作用的包装元素。

一个典型的浪费例子是星级评分和不必要的 DOM 扩展,通过单独的“星星”元素:

<!-- 不推荐 -->
<StarRating>
  <SVGStar />
  <SVGStar />
  <SVGStar />
  <SVGStar />
  <SVGStar />
</StarRating>

类似的效果可以通过一个元素来解决,只需设置宽度和重复的背景图像。

还有其他技术可以有效地摆脱大量渲染组件,值得一提的是大型列表的虚拟滚动。

ssr 总是有帮助

使用 SSR(服务器端渲染)时,构建第一个 HTML 响应所需的时间得到了优化,同时数据大小也更小。这是一个从各方面来看都非常有利的选择。

你应该始终考虑组件的优先级以及 HTML 构建的效率。

另外,还可以通过其他方式有效地解决大型 DOM 的问题,具体文章请点击 DOM 大小对互动的影响以及应对措施

将组件分为简单版本和扩展版本

再次强调,这个建议其实就是删除 DOM 元素。但它有些不同,它从根本上改变了我们今天看待 HTML 和 DOM 结构的方式。

即使一个组件或其内容对于 SEO 或可访问性很重要,也并不意味着它在首次渲染时必须以完全的视觉效果呈现。特别是当该组件在首次视口中不可见时。

用户实际会看到多少个组件?其中一些可能被交互元素隐藏,例如大菜单或模态窗口,其他组件则可能在滚动后才会显示。最终的 HTML 中是否真的需要所有的组件都以最终的形式存在?

通过将组件拆分为简单组件和丰富组件的方式进行优化,尤其对于那些在页面上多次重复的元素来说,效果非常明显。典型的情况包括登录页、产品列表或其他类似于你在图片中看到的页面元素。

image.png

一个组件的例子,它在页面加载时仅提供与 SEO 相关的数据。这个状态在视觉上对用户是隐藏的。当组件出现在视口中时,"丰富版" 会被激活。

为了说明这一点,以下代码示例展示了如何使用 Intersection Observer 来加载该组件的更丰富版本:

import React from "react";
import { useInView } from "react-intersection-observer";

const Offer = ({images, title}) => {
  const { ref, inView, entry } = useInView();

  return (
    <article className="offer" ref={ref}>
      <div className="gallery">
        {!inView ? <Image data={images[0]}> : <ImagesCarousel data={images} />}
        <h3>{title}<h3>
      </div>
    </article>
  );
};

当你比较重要的内容和最终生成的 HTML 时,你会发现 DOM 的结构可以非常简单。视觉上的丰富性可以在用户访问时通过前端来添加。

然而,始终要注意布局的稳定性,以避免在渲染优化过程中破坏 CLS 指标。

Server Components 可以提供帮助

本节提到的一些问题可以通过相对较新的 React 服务器组件(React Server Components) 来解决,它允许你编写仅在服务器上可用、而非客户端 JavaScript 中可用的组件。

在这种情况下,浏览器接收到的是已经渲染好的内容,不需要重新运行 JavaScript 来显示或动画化内容。即便如此,仍然需要尽量保持 DOM 结构尽可能小。我们的建议是优化浏览器中实际的 UI 渲染过程。

使用 Suspense 组件

在前面我们已经优化了 DOM。这通常会显著改善 INP 指标。接下来我们考虑是否可以进一步分解工作负载。

<Suspense> 标签在 React 中主要用于在加载不同组件时显示占位符内容。

然而,少有人知道,<Suspense> 的一个隐藏功能是它启用了选择性渲染。这使得页面及其组件可以分为重要部分和次要部分。

20250206163952

组件树通过 标签进行拆分。红色组件通过这个标签被标记为次要部分。

在使用 SSR(服务器端渲染)时, 标签的效果是至关重要的。

水合(Hydration)是指服务器端生成的代码在浏览器客户端被“复活”的过程。服务器提供的 HTML 会迅速显示给用户,而 React 随后为其添加交互性。如果不使用 ,这个过程通常是一个长时间的 JavaScript 任务。

将页面拆分为独立的逻辑单元,并将它们各自包裹在 标签中。

20250206164132

booking.com 网站分为独立的逻辑单元。

但要小心!使用 <Suspense> 标签也可能适得其反。如果用户快速对一个位于 <Suspense> 内的元素执行操作,React 必须切换焦点并处理整个块。否则,它就无法准确知道用户的操作。这会导致同步处理,从而延长整个事件的时间。

因此,切勿将一个大块内容用这个标签包裹,而是应该针对较小的部分——比如各个区域进行包裹。同时,不要对首次可视区域内的元素使用 <Suspense>

注意补水 (Hydration) 错误

我们已经解释了什么是水合(hydration)。但我们还没有提到一件重要的事情。在水合过程结束时,会进行一次验证,比较生成的元素树(DOM)与从服务器端获取的状态。这时,HTML 必须完全符合 React 在客户端的预期。

如果这两个版本之间存在差异,React 会在浏览器控制台抛出一个错误:

20250206164453

此时,React 可能会使页面上所有或大部分组件失效,并触发它们通过客户端 JavaScript 更新,这会延长水合阶段,从而影响性能,并可能令人烦恼地延迟 LCP 指标。

更糟糕的是,这个错误很容易发生。例如,在服务器端和客户端渲染时直接使用 Math.random()Date.now(),会导致内容不一致。

另一个错误的原因是条件性使用仅在浏览器中可用的 API。

// 错误写法
function LanguageComponent() {
  const language = window?.navigator?.language ?? "en";

  return <h1>Your language is: {language}</h1>;
}

在这种情况下,需要使用 useEffect 函数。这会使代码稍显复杂,但可以避免错误。

// 正确写法
function LanguageComponent() {
  const [language, setLanguage] = useState("en");
  useEffect(() => {
    // 使用客户端 API 更新语言
    setLanguage(window.navigator.language);
  }, []);

  return <h1>Your language is: {language}</h1>;
}

注意 useEffect

useEffect 是 React 中的一个特殊函数,它的作用是对组件或其状态的变化做出反应。

import React, { useState, useEffect } from "react";

function Counter() {
  const [count, setCount] = useState(0);

  // useEffect 监听 'count' 的变化并做出反应
  useEffect(() => {
    console.log(`The count has been updated to: ${count}`);
  }, [count]); // 只监听 'count' 的变化

  return (
    <div>
      <h1>Click count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

export default Counter;

从定义上来说,useEffect 是在变化发生之后被调用的函数。React 开发者通常会认为这个函数是在 HTML 渲染之后调用的。

然而,不幸的是,这并不完全正确。useEffect 并不总是异步的。当用户触发输入(例如点击)时,所有 React 代码,包括“effect hooks”,都是同步执行的。

如果你希望这个钩子函数实际上推迟任务,直到下一个渲染周期,你必须使用 setTimeout 或其他方法。

useEffect(() => {
  // 将任务推迟到一个独立的任务:
  setTimeout(() => {
    sendAnalytics();
  }, 0);
}, []);

对于任何分析代码,都要特别小心。

总结

我希望我已经为你提供了一些优化基于 React 构建的网站性能的具体方法,重点关注 INP 指标。

基本上,这很简单——要注意避免大 DOM,尽可能延迟任务,并特别关注水合过程。

React 只是一个工具,关键在于对它的深入理解。在这方面,我们强烈推荐 《React Internals Deep Dive》 系列文章。也可以了解其他优化 INP 的方法。