React应用性能优化的最佳实践

728 阅读28分钟

1__-R-ZHoLt5K4EAwkLHbI4Q.png

前言

想象一下这种情景:您访问一个网站,渴望探索其内容,但页面加载时间无比漫长,让您感到沮丧,最终决定放弃。这种情况凸显了优化web性能的重要性。

用户期望网页应用加载迅速,并能对其交互操作做出迅速响应。一个运行顺畅的网站可以提升用户满意度并积极影响搜索引擎排名。相反,加载缓慢的网站和反应迟钝的界面可能导致用户感到沮丧,跳出率升高,并丧失潜在客户和收入。

在使用React时,通常可以默认获得快速而高性能的用户界面。然而,随着应用程序的复杂性增加,采用一些技术来保持最佳性能变得至关重要。

这篇文章将涵盖这些技术。从学习如何识别性能问题到通过诸如代码拆分和懒加载等方法来缓解这些问题,本指南将为您提供必要的工具和知识,使您的React应用程序能够发挥最佳性能。

必备知识

看懂并实践本文您应该熟悉以下技术:

  1. 对React的基本理解
  2. HTML, CSS, 和JavaScript知识

理解Web性能指标

性能优化不仅仅是主观感觉的问题;它基于量化用户体验的数据驱动指标。在深入优化React应用程序之前,让我们首先了解一些主要的Web性能指标。

可交互时间 (TTI)

可交互时间衡量的是网页变得能完全交互所需的时间,也就是用户可以与页面元素进行交互、点击按钮并在没有延迟的情况下导航的所需的时间。

为什么重要:   一个可交互的页面提供更流畅的用户体验,而较快的TTI意味着更高的用户参度。

首次内容绘制 (FCP)

FCP timeline from google.com

首次内容绘制记录的是网页中第一个视觉元素在用户视口中显示所需的时间。这个视觉元素可以是文本、图像或任何可见内容。

为什么重要:   FCP对于主观感知性能至关重要。用户在屏幕上看到某些内容时会感到进展,即使页面尚未完全加载,也能给用户一种进度的感觉。

速度指数 (Speed Index)

速度指数(Speed Index)衡量网页内容可视化填充的速度。它量化了页面加载的视觉进展,提供了关于用户如何感知加载速度的指标。

为什么重要:  较低的速度指数(Speed Index)表示加载体验更快,有助于提升用户的感知和参与度。

最大内容绘制 (LCP)

最大内容绘制(Largest Contentful Paint)测量视窗中最大内容元素的渲染时间,无论是图像、视频还是大型文本块。

为什么重要:  LCP 与用户感知的加载速度直接相关。快速的LCP确保用户迅速看到有意义的内容,从而提升他们的整体体验。

总阻塞时间 (TBT)

总阻塞时间(Total Blocking Time)测量浏览器主线程被阻塞且无法响应用户输入的时间。它与脚本执行相关,可能会影响交互性。

为什么重要:  高TBT(总阻塞时间)可能导致交互迟缓,使网站显得反应迟钝。

累积布局偏移 (CLS)

累积布局偏移(Cumulative Layout Shift)量化了网页的视觉稳定性。它测量在内容加载时发生的意外布局偏移。当页面上的元素出乎意料地移动时,就会发生意外的偏移,这可能导致用户感到沮丧。

为什么重要:  低CLS(累积布局偏移)得分确保元素不会出乎意料地移动,这种移动在移动设备上可能会导致用户感到沮丧。

初始性能评估

既然我们已经熟悉了一些关键的网页性能指标,现在让我们学习如何评估我们的网页应用的状态。

在进行优化之前,您首先需要建立一个基线性能测量。这将作为改进的起点,并在您进行优化时帮助跟踪您的进展。您可以使用评估工具,如Lighthouse、WebPageTest和React Developer Tools(React DevTools)来完成这一任务。

使用生产版本测试

在测量React应用的性能或识别性能问题时,确保您正在使用生产版本进行测试。开发版本通常比压缩后的生产版本体积更大、速度更慢,因此它不能准确地代表应用程序在实际运行时的表现。

但也有例外比如React DevTools,需要在开发版本测试。

使用Lighthouse

Lighthouse是由谷歌开发的一个开源工具,用于审查网页的性能、可访问性、最佳实践等方面。它提供了关于这些网站属性的详细报告,并就其发现的任何问题提出改进建议。

运行Lighthouse审查:

  1. 打开DevTools: 在浏览器中对该网站进行右键点击,然后选择“检查”(Inspect),以打开DevTools面板。
  2. 打开"Lighthouse" tab: 在DevTools中,找到并点击“Lighthouse”标签页。
  3. 配置Audits: 通过选择所需的类别(性能、可访问性、最佳实践、SEO)和设备仿真设置来自定义您的审查。
  4. 运行Audit: 点击“分析页面加载”按钮来启动审查。
  5. 查看结果: 审查完成后,查看Lighthouse报告。特别关注性能部分,它将提供需要改进的领域的建议。

使用WebPageTest

WebPageTest 是另一个用于评估网页性能的强大工具。它允许您模拟在不同设备和网络条件下的页面加载情况。

要使用WebPageTest进行测试,请按照以下步骤操作:

  1. 访问WebPageTest网站: 在您的浏览器中打开WebPageTest官方网站 WebPageTest.org.
  2. 输入网页URL: 在提供的输入框中输入您想要测试的网页的URL。
  3. 选择测试配置: 您可以配置多种测试选项,包括选择不同的浏览器、测试位置、网络速度和设备类型等,以模拟不同的用户体验场景。
  4. 启动测试: 配置完成后,点击“开始测试”或类似的按钮来启动性能测试。
  5. 查看结果: 测试完成后,您将收到一份详细的性能报告,其中包括页面加载时间、首次内容绘制(FCP)等指标。

使用React DevTools

不同于其他通用评估工具,React DevTools 是专门为 React 应用程序打造的。您可以将这个工具视为对您的 React 组件的显微镜,它允许您检查、分析和调试您应用程序的 React 结构。以下是您如何利用它来评估您应用的性能:

  1. 安装: 使用浏览器插件的方式安装React Devtools。 主流浏览器均可安装,比如 ChromeFirefox, and Edge.

  2. 性能分析:

1073e369-8a74-460c-92da-12ac5dfc4b37.gif

  1. 启动性能分析: 在 React DevTools 中点击“Profiler(分析器)”标签。通过点击“Record(记录)”按钮开始对您的应用进行性能分析。这个操作会在您与网站互动时捕获性能数据。

  2. 与应用程序交互: 在您的网站上执行典型的互动操作,比如导航页面、点击按钮、滚动内容等。

  3. 停止性能分析: 在与您的网站互动之后,再次点击“Record(记录)”按钮以结束性能分析会话。

  4. 分析组件:

    • 查看渲染时间: 在“Profiler(分析器)”标签中,您可以看到关于组件渲染时间的详细信息。识别那些渲染时间较长的组件,因为这些是潜在的优化区域。
    • 识别重复渲染: 在“Profiler(分析器)”标签中点击“Settings(设置)”按钮,然后导航至Profiler部分。勾选“在分析时记录每个组件渲染的原因”选项。启用这个功能可以让 React DevTools 提供每个组件渲染的原因,从而帮助您识别不必要的渲染。了解哪些组件进行了不必要的重渲染以及为什么会这样,可以帮助您确定在组件中优化性能的位置。

使用 React DevTools 可以让您直观地了解您的 React 组件的行为和性能。结合来自 Lighthouse 和 WebPageTest 的数据,您将获得对您的 React 应用当前状态的全面了解。

优化策略

既然我们已经学会了如何识别 React 应用中的性能问题,接下来让我们探索一些用于优化它们的策略和技术。

DOM大小优化

文档对象模型(DOM)表示网页的结构,而这个结构的大小直接影响了React应用程序的性能。大而复杂的DOM树可能会减慢渲染速度并增加内存使用。

让我们探讨一些用于优化DOM的技术。

避免复杂嵌套

每个DOM元素都增加了渲染成本。尽量减少不必要的元素,并考虑将相关元素分组到容器中,当不需要容器节点时,可以使用<Fragment>(通常使用<>...</>语法)来进行分组。

利用语义化的HTML来有效传达结构,同时保持DOM层次浅。例如,不要使用多个嵌套的div元素,而是使用语义标签如<header><main><section><footer>

// 复杂嵌套 ❌
<div className="container">
  <div className="content">
    <div>
      Content
    </div>
  </div>
</div>

// 简化结构 ✅
<main className="container">
  <section className="content">Content</div>
</div>

窗口化/列表虚拟化

窗口化或列表虚拟化是一种通过仅渲染用户视口中当前可见的项目来优化长列表的渲染的技术。这有助于在渲染页面上重复元素时减少创建的DOM节点数量。

您可以通过使用类似react-window 或者 react-virtualized 库在React应用中实现窗口化。例如,使用react-window,您可以获得多个组件,比如FixedSizeListVariableSizeList,它们通过在用户滚动时回收DOM元素来优化大型列表的渲染:

import { VariableSizeList as List } from 'react-window';

const data = [...]; // Your list of data

const RowRenderer = ({ index, style }) => {
  return <div style={style}>{data[index]}</div>;
};

const VirtualizedList = () => {
  return (
    <List
      height={500}
      itemCount={data.length}
      itemSize={(index) => 50} // Row height
      width={300}
    >
      {RowRenderer}
    </List>
  );
};

通过使用窗口化和列表虚拟化技术,React应用程序可以高效处理大型数据集,确保在处理大量动态内容时仍具有响应性的用户界面。

记忆化(Memoization)

React使用一种强大的优化技术称为记忆化(Memoization)来防止不必要的组件重新渲染。它通过将计算结果存储在缓存中,并在下次需要时从缓存中检索相同的信息,而不是重新计算它。这可以减少不必要的性能开销。

React提供了三种主要的记忆化技术:memouseCallbackuseMemo。让我们探讨这些方法,并了解如何有效地实现它们。

memo

memo 可以用来通过防止在其属性保持不变时不必要的重新渲染来对组件进行记忆化。

例如,假设您有一个UserList组件,负责根据users属性渲染一个大型用户列表:

const UserList = ({ users }) => {
  const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name));

  return (
    <ul>
      {sortedUsers.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
};

export default UserList;

UserList组件在渲染列表之前按照他们的姓名对用户进行排序。然而,排序操作可能在处理大量项目时具有计算开销。

因此,为了确保这种昂贵的重新渲染逻辑仅在users属性更改时运行,您可以将memo应用于该组件:

import { memo } from 'react';

const UserList = memo(({ users }) => {
  const sortedUsers = users.sort((a, b) => a.name.localeCompare(b.name));

  return (
    <ul>
      {sortedUsers.map(user => (
        <li key={user.id}>{user.name} - {user.email}</li>
      ))}
    </ul>
  );
});

export default UserList;

这种优化可以显著提高您的应用程序的性能,特别是在处理动态数据和频繁更新时。

useMemo

useMemo钩子可以对计算结果进行记忆化。它接受一个函数和一个依赖项数组,并返回记忆化的值。这对于优化昂贵的计算或数据转换非常有用。

考虑一种情景,一个数据可视化组件基于从API检索到的大型数据集来渲染图表。用于此可视化的数据处理涉及到复杂的计算,如聚合、过滤和排序。

import { useMemo } from 'react';

const Visualization = ({ data }) => {
  // 昂贵的数据处理逻辑 (比如 聚合 过滤 排序)
  const processedData = useMemo(() => {
    // 对数据进行复杂的计算
    // ...
    return processedResult;
  }, [data]);

  // Render chart using processedData
  return <Chart chartData={processedData} />;
};

export default Visualization;

在这个示例中,useMemo钩子对processedData变量进行了记忆化。useMemo回调函数中的昂贵数据处理逻辑确保只在data属性发生更改时运行计算。

useCallback

useCallback钩子允许您缓存一个函数,防止不必要地重新创建函数定义。当将回调函数传递给子组件时,此钩子非常有用,可以确保它们不会不必要地重新渲染。

例如,假设您有一个要优化的Dashboard组件:

const Dashboard = ({ month, income, theme }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 获取数据
    // ...
  }, []);

  const onFilterChange = (filter) => {
    // 处理昂贵的过滤变化逻辑
    // ...
    setData(result);
  }

  return (
    <div className={`dashboard ${theme}`}>
      <Chart data={data} onFilterChange={onFilterChange} />
      {/* Other components */}
    </div>
  );
};

在使用React DevTools之后,您发现仅当更改仪表板的theme时,仪表板中的Chart组件会不必要地重新渲染。因此,您决定对该组件应用memo

const Chart = memo(({ data, onFilterChange }) => {
  // ...
});

然而,即使应用了memo,您仍然注意到它仍然会不必要地重新渲染。经过更多的深入研究,您发现onFilterChange函数是问题所在。Dashboard组件在每次渲染时都重新创建该函数,因此它仍然会导致Chart组件重新渲染。

为了解决这个问题,您可以引入一个useCallback钩子来处理这个函数:

const Dashboard = ({ month, income, theme }) => {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 获取数据
    // ...
  }, []);

  const onFilterChange = useCallback((filter) => {
    // 处理昂贵的过滤变化逻辑
    // ...
    setData(result);
  }, [month, income]);

  return (
    <div className={`dashboard ${theme}`}>
      <Chart data={data} onFilterChange={onFilterChange} />
      {/* Other components */}
    </div>
  );
};

通过使用useCallbackonFilterChange函数包装起来,您可以记忆化函数实例本身。这意味着只有在它的依赖项发生变化时,函数才会重新创建。在这个例子中,依赖项是来自Dashboard组件的propsmonthincome变量。

是否应该始终使用记忆化(memoization)

当您的组件、函数或值因为某个props或者依赖项频繁重新渲染时,使用这些记忆化技巧变得非常有价值。特别是当其重新渲染逻辑消耗资源时,这一点尤其重要。

如果你的组件可以丝滑地重新渲染,而且没有任何明显的延迟,那么使用这些技术通常是不必要的。有关何时使用 memouseMemo, 和 useCallback,请参考 React 的官方文档。

资源文件优化

优化网站资源对于提高网站加载速度、改善首次内容绘制(FCP)、最大内容绘制(LCP)和累积布局偏移(CLS)至关重要。体积大、未优化的资源会显著增加页面加载时间并消耗不必要的带宽。让我们来看看如何在我们的web应用中优化资源。

正确存储图片和视频

以下是在您的项目中处理图片和视频时需要考虑的一些最佳实践:

  • 使用正确的格式:  根据内容选择合适的图片和视频格式。对于位图图片使用JPEG和PNG格式,对于简单图形和标志使用SVG格式。WebP是一种现代的、高度压缩的图片格式,具有出色的质量和较小的文件大小。考虑对支持WebP的浏览器使用WebP格式,因为它可以显著减少加载时间。

    对于视频,MP4格式在各种浏览器中都得到了广泛支持,而WebM则提供了出色的压缩效果和质量。

  • 压缩图片和视频:  在将图片和视频部署到生产环境之前,使用压缩工具或插件来压缩它们。像 ImageOptim 或者 TinyPNG 这样的工具适用于图片压缩,而 HandBrake 或者 FFmpeg 适用于视频压缩,这些工具可以在不影响质量的情况下减小文件大小。

  • 用视频替换动画GIF:  动画GIF文件体积较大,可能会拖慢页面加载速度。考虑使用视频格式(如MP4)替换动画GIF,以实现更流畅的动画和更快的页面加载。视频通常提供更好的压缩效果,并且可以根据用户交互自动播放或进行控制。

  • 提供响应式图片:<img>标签中使用srcset属性来指定多个图像源,并让浏览器选择最合适的一个。响应式图片确保用户接收到适当大小的图片,减少不必要的数据传输,并提高加载速度,尤其是在移动设备上。

  • 提供正确尺寸的图片和视频:  在HTML标签中指定图片和视频的具体的width(宽度)和height(高度),以防止在渲染过程中发生布局移位。当浏览器知道这些尺寸时,它可以预留必要的空间,避免内容重排,并提高您网站的累积布局偏移(CLS)表现。

预加载LCP(最大内容绘制)图片

LCP(最大内容绘制)图片是用户初始视口中渲染的网页上最大的图片元素。通过预加载这张图片,可以确保它在浏览器缓存中可用,并在进入用户视口时准备好显示。这可以显著提高LCP指标和感知的页面加载速度。

在React中预加载LCP图片,在项目public目录中的index.html文件,并添加以下代码:

<!DOCTYPE html>
<html lang="en">
    <head>
        ...
        <link
            rel="preload"
            fetchpriority="high"
            as="image"
            href="/path/to/hero-image.jpg"
        />
    </head>
    ...
</html>

使用带有rel="preload"属性的<link>元素来启动预加载过程。以下是<link>元素中使用的属性的详细解释:

  • rel="preload": 告诉浏览器应预加载资源。
  • fetchpriority="high": 为指定资源设置高优先级。这有助于浏览器优先于其他资源预加载这个资源。
  • as="image": 指定预加载资源的类型,在这个例子中是"image"图片。
  • href="/path/to/hero-image.jpg": 指定需要预加载的LCP图片文件的路径。

懒加载图片

懒加载是一种技术,可以推迟加载超出屏幕的图像,直到需要它们为止。这种优化可以减少初始页面加载时间,特别是对于包含多个图像的网页而言。

React 通过 img 标签的 loading 属性支持图片懒加载:

import React from 'react';

const LazyImage = () => {
  return (
    <img
      src="image.jpg"
      alt="Description"
      loading="lazy"
      width="200"
      height="150"
    />
  );
};

这只是一种简单的延迟加载图像的方法。在更复杂的情境中,您可能需要类似以下的功能:

  • 加载过程中的图像占位符
  • 加载过程中的模糊效果
  • 设置加载图像的特定阈值

对于这些情景,您可以利用第三方库: react-lazy-load-image-component.

不要对用户初始视口中可见的任何图像(例如您网站的标志和主图像)进行延迟加载。确保您网站的关键图像立即可见,从而提高页面加载速度。

预连接到第三方主机

第三方(3P)主机是提供网站所使用的服务、资产或资源的外部服务器或域。例如,社交媒体平台、内容分发网络(CDN)以及像Google Fonts这样的外部字体服务。

通过预连接到这些第三方主机,您可以让浏览器在实际资源请求时提前建立连接,从而减少延迟。

考虑一种情况,您的React应用程序依赖于Google Fonts来为文本设置样式。为了优化字体的加载,您可以预先连接到其域。以下是如何实现的方法:

<!-- /index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <!-- Other head elements -->

  <!-- Preconnect to Google Fonts and link to the stylesheet -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=YourFontFamily">

  <!-- Your other stylesheets and scripts go here -->
</head>
<body>
  <!-- React application content -->
</body>
</html>

在上述示例中:

  •  <link> 标签中的 rel="preconnect" 属性用于在应用程序加载过程中提前连接到 Google Fonts 的域。
  • href 属性指定了域的 URL。
  • crossorigin 属性是出于安全考虑添加的,以确保预连接遵循浏览器的跨域资源共享(CORS)策略。
  • 最后的 <link> 标签导入了所需的字体样式到您的应用中。

使用内容分发网络(CDN)

内容分发网络(CDN)在全球范围内分发网站资源文件,包括部署在多个服务器上的图像。当用户访问网站时,资产将从最近的服务器提供,减少延迟,提高加载速度。

通过将您的资源文件托管在像 CloudinaryCloudFrontCloudFlare, 等这样的CDN供应商上。 您可以确保全球访问者快速高效地获取您的资源文件。

使用 Service Workers 缓存文件和 API 响应

Service Workers 是在后台运行的脚本,拦截和控制网络请求。它们使得资源文件和 API 响应能够被缓存,从而使 React 应用能够在离线状态下运行,并提高了后续访问的加载速度。

在 React 应用中实现 Service Workers 取决于您使用的构建工具或框架(例如 Vite、Create React App、Next.js)。例如,如果您的 React 应用使用 Vite,您可以使用 workbox-window 和 vite-pwa  来简化在应用中生成和管理 Service Worker。

如果您的 React 应用不是PWA应用(渐进式网页应用,具有离线功能、推送通知和后台同步等特性),那您就不需要使用 Service Workers。

高效的状态管理

高效的状态管理是确保 React 应用性能优化的核心。每当状态发生变化时,React 组件都会重新渲染,因此正确管理状态不仅影响应用的响应性,还影响其内存使用。让我们探讨一些在 React 应用中高效管理复杂状态的方法。

遵循管理状态的最佳实践

React 的官方文档提供了一份全面的管理状态指南。其中涵盖了关键概念,如如何良好地组织状态、将状态提升以及随着应用程序的发展而扩展状态管理。通过遵循这些最佳实践,开发者可以在他们的应用程序中保持一个清晰而有组织的状态管理系统。

要详细了解在 React 中管理状态的指南,请参考官方 React 文档: 在 React 中管理状态.

Context API和useContext

React 的 Context API 提供了在组件之间共享状态而无需通过多层级传递 props(prop drilling)的便捷方式,从而提高了深层嵌套组件的性能。useContext 钩子简化了在函数组件中使用这个上下文的过程。

让我们看一个简单的例子:

import React, { useContext } from 'react';

const MyContext = React.createContext();

const ParentComponent = () => {
  const contextValue = 'Hello, Context!';
  return (
    <MyContext.Provider value={contextValue}>
      <ChildComponent />
    </MyContext.Provider>
  );
};

const ChildComponent = () => {
  const contextValue = useContext(MyContext);
  return <div>{contextValue}</div>;
};

通过使用 Context API 和 useContext,您可以在不牺牲性能的情况下高效地管理共享状态,尤其是在庞大的组件树中。

状态管理库

对于较大的应用程序,像 Redux 和 MobX 这样的状态管理库提供了管理复杂应用状态的工具,特别是那些可能经常更新的状态:

  • Redux: Redux 提供了一个集中式存储,并遵循单向数据流,使状态变更可预测且更容易追踪。动作触发更新,组件订阅存储以接收最新状态。

    让我们看一个使用 Redux Toolkit 的例子:

      import { createSlice, configureStore } from '@reduxjs/toolkit'
    
      const counterSlice = createSlice({
        name: 'counter',
        initialState: {
          value: 0
        },
        reducers: {
          incremented: state => {
            // Redux Toolkit allows us to write "mutating" logic in reducers. It
            // doesn't actually mutate the state because it uses the Immer library,
            // which detects changes to a "draft state" and produces a brand new
            // immutable state based off those changes
            state.value += 1
          },
          decremented: state => {
            state.value -= 1
          }
        }
      })
    
      export const { incremented, decremented } = counterSlice.actions
    
      const store = configureStore({
        reducer: counterSlice.reducer
      })
    
      // Can still subscribe to the store
      store.subscribe(() => console.log(store.getState()))
    
      // Still pass action objects to `dispatch`, but they're created for us
      store.dispatch(incremented())
      // {value: 1}
      store.dispatch(incremented())
      // {value: 2}
      store.dispatch(decremented())
      // {value: 1}
    
  • MobX: MobX 允许更细粒度的响应性。它跟踪观察状态之间的依赖关系,并在相关状态更改时自动更新组件。这可以导致更精细的重新渲染。让我们看一个例子:

      import { makeObservable, observable, action } from 'mobx';
    
      class CounterStore {
        count = 0;
    
        constructor() {
          makeObservable(this, {
            count: observable,
            increment: action,
            decrement: action,
          });
        }
    
        increment() {
          this.count += 1;
        }
    
        decrement() {
          this.count -= 1;
        }
      }
    
      const counterStore = new CounterStore();
    

在Redux、MobX或其他库之间的选择取决于您的应用程序需求以及对状态管理的个人偏好。

通过利用正确的状态管理技术,您可以构建响应迅速且高效的应用程序,确保应用在 UI 状态更新时能够平稳响应。

Web workers

A Simple Introduction to Web Workers in JavaScript | by Matthew MacDonald |  Young Coder | Medium

Web Workers 允许耗时的任务在后台执行而不影响主线程,确保用户体验的流畅性。它们特别适用于诸如数据处理、加密或其他需要大量 CPU 计算的操作。

考虑这样一个情景,您想要在不引起 UI 延迟的情况下从一个大型数据集中计算平均年龄。以下是使用 Web Worker 实现这一目标的方法:

// /public/worker.js
self.onmessage = function(event) {
  const data = event.data;

  // Calculate the average age
  const totalAge = data.reduce((sum, record) => sum + record.age, 0);
  const averageAge = totalAge / data.length;

  // Post the result back to the main thread
  self.postMessage(averageAge);
};

在这个代码片段中,我们定义了一个 Web Worker 脚本 (worker.js),通过 onmessage 事件接收数据。它从接收到的数据集中计算平均年龄,并使用 self.postMessage() 将结果发送回主线程。

现在,让我们在 UI 中使用这个 Web Worker:

// Main component
const WorkerComponent = ({ dataset }) => {
  const [averageAge, setAverageAge] = useState(null);

  useEffect(() => {
    const worker = new Worker('./worker.js');

    // 发送数据集给 Web Worker 进行处理:
    worker.postMessage(dataset);

    // 处理来自 Web Worker 的消息:
    worker.onmessage = function(event) {
      const calculatedAverageAge = event.data;
      setAverageAge(calculatedAverageAge);
    };

    // 在组件卸载时清理 Web Worker:
    return () => {
      worker.terminate();
    };
  }, []);

  return (
    <div>
      {averageAge ? (
        <p>Average Age: {averageAge.toFixed(2)}</p>
      ) : (
        <p>Calculating...</p>
      )}
    </div>
  );
};

WorkerComponent 中,我们使用 Worker 构造函数创建一个新的 Web Worker。组件将 dataset 属性发送给 Web Worker 进行处理。当 Web Worker 完成任务时,它返回计算得到的平均年龄,并在组件中显示。

打包大小优化

优化包大小可以确保您的应用在不良网络条件和各种设备上快速加载。以下是一些有效优化 React应用包的策略:

代码拆分(Code Splitting)

代码拆分(Code Splitting)是一种技术,它允许您将代码拆分成较小的块,在需要时才加载。这可以显著提高应用的初始加载时间和性能,尤其是对于较大的项目而言。

React 提供了一种使用动态导入和 <Suspense> 来实现代码拆分的简单方法:

import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";

const HomeComponent = lazy(() => import('./HomeComponent'));
const AboutComponent = lazy(() => import('./AboutComponent'));

const App = () => {
  return (
    <Router>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" exact element={HomeComponent} />
          <Route path="/about" element={AboutComponent} />
        </Routes>
      </Suspense>
    </Router>
  );
};

export default App;

在这个例子中,当用户导航到相应的路由时,HomeComponentAboutComponent 会被懒加载。在组件加载完成之前,将显示加载指示器。

通过利用 lazy<Suspense> 进行代码拆分,您可以显著优化 React 应用的性能,尤其是对于较大的项目,减小初始捆绑包大小对于更快的加载时间至关重要。

Dependency审查和Cleanup

定期审查项目的依赖关系至关重要。使用诸如 npm audityarn audit 的命令来识别存在漏洞的软件包和过时的依赖项。您还可以使用类似的工具如 depcheck 来找到未使用或多余的软件包,您可以删除它们以减小应用程序的捆绑包大小:

# 检查项目依赖项中的漏洞:
npm audit

# 检查未使用的依赖项:
npx depcheck

# 移除未使用的依赖包
npm uninstall package-name

Tree Shaking

摇树(Tree shaking)是一种技术,它从包中消除未使用的代码。借助ES6模块,现代的工具可以分析你的代码并删除未使用的导出,从而降低总体包大小。为了从摇树中受益,请使用ES6的导入/导出语法,并避免在只需要特定功能的情况下导入整个库。

// 避免 CommonJS 语法 ❌
const React = require('react'); // 不支持 tree shaking
const Component = React.memo(() => { ... })

// 使用 ES6 import 语法 ✅
import { memo } from 'react'; // 支持 tree shaking
const Component = memo(() => { ... })

通过利用这些技术,你可以优化你的React应用程序的依赖关系和包大小,从而实现更快的加载时间和更好的性能。

渲染模式

在React应用程序中,渲染模式在塑造应用程序如何传递到客户端方面起着至关重要的作用。常见的渲染模式,如客户端渲染(CSR)和服务器端渲染(SSR),具有明显的优势和用例。让我们探讨这些渲染模式,了解如何实施每种模式,以及何时使用它们。

客户端渲染(CSR)

在客户端渲染(CSR)中,初始HTML内容很少,只包含足够的JavaScript和CSS文件以引导应用程序。大部分的渲染和逻辑执行都发生在客户端浏览器中。这种模式提供了快速的初始加载时间,使其适用于具有丰富交互性的应用程序。

实现:  使用 Vite 或 Next.js 这样的工具创建一个标准的 React 应用程序,而不使用服务器端渲染。

CSR的优点:

  • 快速交互:  客户端渲染(CSR)在提供快速且响应灵敏的用户界面方面表现出色。一旦初始页面加载完成,后续的交互感觉瞬间完成,因为应用程序在不需要进行完整页面重新加载的情况下进行更新。
  • 丰富的用户体验:  像动态内容更新这样的交互式特性非常适合客户端渲染(CSR)。这使得它成为强调用户参与度的应用程序的绝佳选择,比如社交媒体平台或实时协作工具。

CSR的缺点:

  • SEO的挑战:  CSR 的一个重要缺点是它对搜索引擎优化(SEO)的潜在影响。搜索引擎可能难以索引在客户端渲染的内容,这可能会影响你的应用程序在搜索引擎上被搜索到。
  • 初始加载时间较慢:  虽然客户端渲染(CSR)在初始加载后提供了快速的用户体验,但首次加载可能较慢,尤其是在处理能力有限或网络连接较慢的设备上。特别是当网站有一个较大的 JavaScript包时,这可能导致长时间的首次内容绘制(FCP)和首次可交互时间(TTI)。

服务端渲染(SSR)

在服务器端渲染(SSR)中,React 组件是在服务器端响应用户请求时渲染的。服务器将预渲染的 HTML 和任何必要的 JavaScript 发送到客户端,提供一个完整的页面。一旦客户端接收到 HTML,它可以使页面变得活跃,通过添加事件监听器和设置 React 应用程序来增加交互性。

实现:  使用像 Next.js 或者 custom server setups in Node.js 这样的框架 来处理服务器端渲染(SSR)逻辑。

SSR的优点:

  • SEO友好:  服务器端渲染(SSR)的一个显著优势是它对搜索引擎优化的积极影响。搜索引擎可以轻松爬取和索引完全渲染的HTML,提高了内容的可发现性。
  • 更快的初始加载时间:  服务器端渲染(SSR)通常比客户端渲染(CSR)在初始加载时具有更快的加载时间,特别是在低带宽或性能较差的设备上。用户从服务器获得一个完全渲染的页面,缩短了首次内容绘制(FCP)和首次可交互时间(TTI)。

SSR的缺点:

  • 交互性受限:  尽管服务器端渲染(SSR)提供了更快的初始加载,但随后的交互可能不如客户端渲染(CSR)那样迅速。客户端仍然需要下载并执行 JavaScript 以使应用程序完全交互。
  • 服务器负载增加:  在服务器上进行渲染可能导致服务器负载的增加,特别是在具有大量并发用户的应用程序中。适当的服务器基础设施对于处理渲染需求至关重要。

如何选择

  • CSR:  选择客户端渲染(CSR)适用于交互式的 Web 应用程序,其中快速的客户端渲染至关重要,而初始时的搜索引擎优化并非主要关注点。
  • SSR: 选择服务器端渲染(SSR)当搜索引擎优化是首要考虑因素,或者你的应用程序需要快速的初始渲染,特别是对于内容丰富的网站。

这些渲染模式是 React 中最常见的两种,但还有其他模式,比如静态站点生成 (SSG), 增量静态生成 (ISR), 等等, 所有这些都是特定用例的精选模式。因此,选择正确的渲染模式取决于你的应用程序的具体要求。在决定要实现哪种模式时,请考虑搜索引擎优化、交互性、初始加载时间以及应用程序的复杂性等因素。

总结

这份指南探讨了优化React应用程序的各种策略,涵盖了从理解基本的 Web 性能指标和评估性能到实施懒加载、代码分割和窗口化等技术以优化性能的主题。

请记住,优化React应用程序不仅仅是一次性的任务,而是一个持续的过程。定期审查你的应用程序,监控性能指标,并随时了解最新的工具和最佳实践,以确保你的React应用程序保持流畅运行。

参考