21 个 React 性能优化技巧
React 为高性能应用设计提供了许多优化方案,本文列举了其中的一些最佳实践。
在以下场景中,父组件和子组件通常会重新渲染:
在同一组件或父组件中调用 setState 时。
从父级收到的“props”的值发生变化。
调用组件中的 forceUpdate。
下面是提升 React 应用性能的 21 个技巧。
1. 使用纯组件
如果 React 组件为相同的状态和 props 渲染相同的输出,则可以将其视为纯组件。
对于像 this 的类组件来说,React 提供了 PureComponent 基类。扩展 React.PureComponent 类的类组件被视为纯组件。
它与普通组件是一样的,只是 PureComponents 负责 shouldComponentUpdate——它对状态和 props 数据进行浅层比较(shallow comparison)。
如果先前的状态和 props 数据与下一个 props 或状态相同,则组件不会重新渲染。
什么是浅层渲染?
在对比先前的 props 和状态与下一个 props 和状态时,浅层比较将检查它们的基元是否有相同的值(例如:1 等于 1 或真等于真),还会检查更复杂的 JavaScript 值(如对象和数组)之间的引用是否相同。
比较基元和对象引用的开销比更新组件视图要低。
因此,查找状态和 props 值的变化会比不必要的更新更快。
复制代码
import React from 'react'; export default class ApplicationComponent extends React.Component { constructor() { super(); this.state = { name: "Mayank" } } updateState = () => { setInterval(() => { this.setState({ name: "Mayank" }) }, 1000) } componentDidMount() { this.updateState(); } render() { console.log("Render Called Again") return ( ) }} class RegularChildComponent extends React.Component { render() { console.log("Regular Component Rendered.."); return {this.props.name}
; }} class PureChildComponent extends React.PureComponent { // Pure Components are the components that do not re-render if the State data or props data is still the same render() { console.log("Pure Component Rendered..") return {this.props.name}
; }}
在上面的示例中,状态被传播到子组件 RegularChildComponent 和 PureChildComponent。PureChildComponent 是一个纯组件。
setstate 在一秒的间隔之后被调用,这将重新触发组件的视图渲染。由于初始 props 和新 props 的值相同,因此组件(PureChildComponent)不会被重新渲染。
状态的浅层比较表明 props 或状态的数据没有变化,因此不需要渲染组件,从而提升了性能。
2. 使用 React.memo 进行组件记忆
React.memo 是一个高阶组件。
它很像 PureComponent,但 PureComponent 属于 Component 的类实现,而“memo”则用于创建函数组件。
这里与纯组件类似,如果输入 props 相同则跳过组件渲染,从而提升组件性能。
它会记忆上次某个输入 prop 的执行输出并提升应用性能。即使在这些组件中比较也是浅层的。
你还可以为这个组件传递自定义比较逻辑。
用户可以用自定义逻辑深度对比(deep comparison)对象。如果比较函数返回 false 则重新渲染组件,否则就不会重新渲染。
复制代码
function CustomisedComponen(props) { return ( User name: {props.name} User age: {props.age} User designation: {props.designation}
)} // The component below is the optimised version for the Default Componenent // The Component will not re-render if same props value for "name" property var memoComponent = React.memo(CustomisedComponent);
上面的组件将对前后两个 props 的值进行浅层比较。
如果我们将对象引用作为 props 传递给 memo 组件,则需要一些自定义登录以进行比较。在这种情况下,我们可以将比较函数作为第二个参数传递给 React.memo 函数。
假设 props 值(user)是一个对象引用,包含特定用户的 name、age 和 designation。
这种情况下需要进行深入比较。我们可以创建一个自定义函数,查找前后两个 props 值的 name、age 和 designation 的值,如果它们不相同则返回 false。
这样,即使我们将参考数据作为 memo 组件的输入,组件也不会重新渲染。
复制代码
// The following function takes "user" Object as input parameter in props function CustomisedComponen(props) { return ( User name: {props.user.name} User age: {props.user.age} User designation: {props.user.designation}
)} function userComparator(previosProps, nextProps) { if(previosProps.user.name == nextProps.user.name || previosProps.user.age == nextProps.user.age || previosProps.user.designation == nextProps.user.designation) { return false } else { return true; }} var memoComponent = React.memo(CustomisedComponent, userComparator);
上面的代码提供了用于比较的自定义逻辑
3. 使用 shouldComponentUpdate 生命周期事件
这是在重新渲染组件之前触发的其中一个生命周期事件。
可以利用此事件来决定何时需要重新渲染组件。如果组件 props 更改或调用 setState,则此函数返回一个 Boolean 值。
在这两种情况下组件都会重新渲染。我们可以在这个生命周期事件中放置一个自定义逻辑,以决定是否调用组件的 render 函数。
这个函数将 nextState 和 nextProps 作为输入,并可将其与当前 props 和状态做对比,以决定是否需要重新渲染。
比如说我想在网页上显示员工的详细资料。每位员工都包含多个属性,如姓名、年龄、牌号、薪水、当前经理、前任经理、奖金等。
我想只在网页上渲染所选员工的姓名和年龄。员工的牌号会在某时刻更新。
由于员工牌号不在视图内,理想情况下视图是无需更新的。我们可以在组件中添加自定义逻辑,判断是否需要组件更新视图。
代码如下:
复制代码
import React from "react"; export default class ShouldComponentUpdateUsage extends React.Component { constructor(props) { super(props); this.state = { name: "Mayank"; age: 30, designation: "Architect"; } } componentDidMount() { setTimeout(() => { this.setState({ designation: "Senior Architect" }); } } shouldComponentUpdate(nextProps, nextState) { if(nextState.age != this.state.age || netState.name = this.state.name) { return true; } return false; } render() { return ( User Name: {this.state.name} User Age: {this.state.age}
) }}
这里即使组件中 designation 发生变化也不会影响应用的视图。
调用 setState 时组件会重新渲染,但因为 designation 更改不会改变 / 影响组件的视图,因此在更改 designation 时重新渲染组件会带来额外开销。
为了避免这种开销,我们可以使用自定义逻辑来检查 name 或 age 是否更新,因为视图仅受它们的影响。
shouldComponentUpdate 将输入参数作为状态和 props 的新值。
我们可以比较 name 和 age 的当前值和新值。有任何一个发生变化就可以触发重新渲染。
从 shouldComponentUpdate 传递 true 就意味着可以重新渲染组件,反之亦然。所以正确使用 shouldComponentUpdate 就可以优化应用组件的性能。
对比过初始状态和 props 后我们就可以决定是否需要重新渲染组件。这样可以减少重新渲染的需求来提升性能。
4. 懒加载组件
导入多个文件合并到一个文件中的过程叫打包,使应用不必导入大量外部文件。
所有主要组件和外部依赖项都合并为一个文件,通过网络传送出去以启动并运行 Web 应用。
这样可以节省大量网络调用,但这个文件会变得很大,消耗大量网络带宽。
应用需要等待这个文件的加载和执行,所以传输延迟会带来严重的影响。
为了解决这个问题,我们引入代码拆分的概念。
像 webpack 这样的打包器支持就支持代码拆分,它可以为应用创建多个包,并在运行时动态加载,减少初始包的大小。
为此我们使用 Suspense 和 lazy。
复制代码
import React, { lazy, Suspense } from "react"; export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null; if(this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if(this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); } return (
This is the Base User: {this.state.name} Loading... }>
) }}
上面的代码中有一个条件语句,它查找 props 值,并根据指定的条件加载主组件中的两个组件。
我们可以按需懒惰加载这些拆分出来的组件,增强应用的整体性能。
假设有两个组件 WelcomeComponent 或 GuestComponents,我们根据用户是否登录而渲染其中一个。
我们可以根据具体的条件延迟组件加载,无需一开始就加载两个组件。
复制代码
import React, { lazy, Suspense } from "react"; export default class UserSalutation extends React.Component { render() { if(this.props.username !== "") { const WelcomeComponent = lazy(() => import("./welcomeComponent")); return ( Loading...
}>
) } else { const GuestComponent = lazy(() => import("./guestComponent")); return (
Loading...
}>
) } }}
在上面的代码中我们没有预加载 WelcomeCompoment 和 GuestComponents 这两个组件,而是进行一个条件检查。
如果用户名存在(或相反),我们就根据指定的条件决定将某个组件作为单独的包加载。
这个方法的好处
主包体积变小,消耗的网络传输时间更少。
动态单独加载的包比较小,可以迅速加载完成。
我们可以分析应用来决定懒加载哪些组件,从而减少应用的初始加载时间。
5. 使用 React Fragments 避免额外标记
使用 Fragments 减少了包含的额外标记数量,这些标记只是为了满足在 React 组件中具有公共父级的要求。
用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签。所以我们经常在组件顶部添加额外标签,例如:
在上面指定的组件中,我们需要一个额外的标签为要渲染的组件提供公共父级。
除了充当组件的父标签之外,这个额外的 div 没有其他用途。
在顶层有多个标签会导致以下错误:
要解决此问题,我们可以将元素包含在片段(fragement)中。
片段不会向组件引入任何额外标记,但它仍然为两个相邻标记提供父级,因此满足在组件顶级具有单个父级的条件。
复制代码
export default class NestedRoutingComponent extends React.Component { render() { return ( <> This is the Header Component Welcome To Demo Page > ) }}
上面的代码没有额外的标记,因此节省了渲染器渲染额外元素的工作量。
更多信息参考: https://github.com/facebook/react/issues/2127?source=post_page-----d15fa52c2349
6. 不要使用内联函数定义
如果我们使用内联函数,则每次调用“render”函数时都会创建一个新的函数实例。
当 React 进行虚拟 DOM diffing 时,它每次都会找到一个新的函数实例;因此在渲染阶段它会会绑定新函数并将旧实例扔给垃圾回收。
因此直接绑定内联函数就需要额外做垃圾回收和绑定到 DOM 的新函数的工作。
复制代码
import React from "react"; export default class InlineFunctionComponent extends React.Component { render() { return (
Welcome Guest { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" /> ) }}
上面的函数创建了内联函数。每次调用 render 函数时都会创建一个函数的新实例,render 函数会将该函数的新实例绑定到该按钮。
此外最后一个函数实例会被垃圾回收,大大增加了 React 应用的工作量。
所以不要用内联函数,而是在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render 时就不会创建单独的函数实例了,参考组件如下。
复制代码
import React from "react"; export default class InlineFunctionComponent extends React.Component { setNewStateData = (event) => { this.setState({ inputValue: e.target.value }) } render() { return (
Welcome Guest ) }}
7. 避免 componentWillMount() 中的异步请求
componentWillMount 是在渲染组件之前调用的。
这个函数用的不多,可用来配置组件的初始配置,但使用 constructor 方法自己也能做到。
该方法无法访问 DOM 元素,因为组件还没挂载上来。
一些开发人员认为这个函数可以用来做异步数据 API 调用,但其实这没什么好处。
由于 API 调用是异步的,因此组件在调用 render 函数之前不会等待 API 返回数据。于是在初始渲染中渲染组件时没有任何数据。
复制代码
import React from "react";import axios from "axios"; export default class UsingAsyncInComponentWillMount extends React.Component { constructor() { this.state = { userData: null } } componentWillMount() { axios.get("someResourceUrl").then((data) => { this.setState({ userData: data }); }); } render() { return ( <> UserName: {this.state.name} UserAge: {this.state.age} > ) }}
在上面的代码中,我们正在进行异步调用以获取数据。由于数据调用是异步的,需要一段时间才能获取到。
在检索数据时 React 会触发组件的 render 函数。因此第一个调用的渲染仍然不包含它所需的数据。
这样一开始渲染组件没有数据,然后检索数据,调用 setState,还得重新渲染组件。在 componentWillMount 阶段进行 AJAX 调用没有好处可言。
我们应避免在此函数中发出 Async 请求。这些函数和调用可以延迟到 componentDidMount 生命周期事件里。
注意 :React 16.3 不推荐使用 componentWillMount。如果你使用的是最新版本的 React,请避免使用这个生命周期事件。
8. 在 Constructor 的早期绑定函数
当我们在 React 中创建函数时,我们需要使用 bind 关键字将函数绑定到当前上下文。
绑定可以在构造函数中完成,也可以在我们将函数绑定到 DOM 元素的位置上完成。
两者之间似乎没有太大差异,但性能表现是不一样的。
复制代码
import React from "react"; export default class DelayedBinding extends React.Component { constructor() { this.state = { name: "Mayank" } } handleButtonClick() { alert("Button Clicked: " + this.state.name) } render() { return ( <> > ) }}
在上面的代码中,我们在 render 函数的绑定期间将函数绑定到按钮上。
上面代码的问题在于,每次调用 render 函数时都会创建并使用绑定到当前上下文的新函数,但在每次渲染时使用已存在的函数效率更高。优化方案如下:
复制代码
import React from "react"; export default class DelayedBinding extends React.Component { constructor() { this.state = { name: "Mayank" } this.handleButtonClick = this.handleButtonClick.bind(this) } handleButtonClick() { alert("Button Clicked: " + this.state.name) } render() { return ( <> > ) }}
最好在构造函数调用期间使用绑定到当前上下文的函数覆盖 handleButtonClick 函数。
这将减少将函数绑定到当前上下文的开销,无需在每次渲染时重新创建函数,从而提高应用的性能。
9. 箭头函数与构造函数中的绑定
处理类时的标准做法就是使用箭头函数。使用箭头函数时会保留执行的上下文。
我们调用它时不需要将函数绑定到上下文。
复制代码
import React from "react"; export default class DelayedBinding extends React.Component { constructor() { this.state = { name: "Mayank" } } handleButtonClick = () => { alert("Button Clicked: " + this.state.name) } render() { return ( <> > ) }}
箭头函数好处多多,但也有缺点。
当我们添加箭头函数时,该函数被添加为对象实例,而不是类的原型属性。这意味着如果我们多次复用组件,那么在组件外创建的每个对象中都会有这些函数的多个实例。
每个组件都会有这些函数的一份实例,影响了可复用性。此外因为它是对象属性而不是原型属性,所以这些函数在继承链中不可用。
因此箭头函数确实有其缺点。实现这些函数的最佳方法是在构造函数中绑定函数,如上所述。
10. 避免使用内联样式属性
使用内联样式时浏览器需要花费更多时间来处理脚本和渲染,因为它必须映射传递给实际 CSS 属性的所有样式规则。
复制代码
import React from "react"; export default class InlineStyledComponents extends React.Component { render() { return ( <> Welcome to Sample Page > ) }}
在上面创建的组件中,我们将内联样式附加到组件。添加的内联样式是 JavaScript 对象而不是样式标记。
样式 backgroundColor 需要转换为等效的 CSS 样式属性,然后才应用样式。这样就需要额外的脚本处理和 JS 执行工作。
更好的办法是将 CSS 文件导入组件。
11. 优化 React 中的条件渲染
安装和卸载 React 组件是昂贵的操作。为了提升性能,我们需要减少安装和卸载的操作。
很多情况下在我们可能会渲染或不渲染特定元素,这时可以用条件渲染。
复制代码
import React from "react"; import AdminHeaderComponent from "./AdminHeaderComponent";import HeaderComponent from "./HeaderComponent";import ContentComponent from "./ContentComponent" export default class ConditionalRendering extends React.Component { constructor() { this.state = { name: "Mayank" } } render() { if(this.state.name == "Mayank") { return ( <> > ) } else { return ( <> > ) } }}
在上面的代码中有一个条件语句,让组件根据指定的条件渲染。如果状态包含名称值 Mayank,则不会渲染 AdminHeaderComponent。
条件运算符和 if else 条件似乎没问题,但后面的代码有性能问题,需要分析一下。
每次调用 render 函数,并且值在 Mayank 和另一个值之间切换时,都会执行不同的 if else 语句。
diffing 算法会运行一个检查,比较每个位置的元素类型。在 diffing 期间,它看到 AdminHeaderComponent 不可用,并且需要渲染的第一个组件是 HeaderComponent。
React 将观察元素的位置。它看到位置 1 和位置 2 的组件已更改并将卸载组件。
组件 HeaderComponent 和 ContentComponent 将在位置 1 和位置 2 卸载并重新安装。其实这是用不着的,因为这些组件没有更改,这是一项昂贵的操作。优化方案如下:
复制代码
import React from "react"; import AdminHeaderComponent from "./AdminHeaderComponent";import HeaderComponent from "./HeaderComponent";import ContentComponent from "./ContentComponent" export default class ConditionalRendering extends React.Component { constructor() { this.state = { name: "Mayank" } } render() { return ( <> { this.state.name == "Mayank" && } > ) }}
在上面的代码中,当 name 不是 Mayank 时,React 在位置 1 处放置 null。
开始 DOM diffing 时,位置 1 的元素从 AdminHeaderComponent 变为 null,但位置 2 和位置 3 的组件保持不变。
由于元素没变,因此组件不会卸载,减少了不必要的操作。
详情参阅: https://medium.com/@cowi4030/optimizing-conditional-rendering-in-react-3fee6b197a20
12. 不要在 render 方法中导出数据
Render 方法是 React 开发人员最熟悉的生命周期事件。
和其他生命周期事件不一样的是,我们的核心原则是将 render() 函数作为纯函数。
纯函数对 render 方法意味着什么?
纯函数意味着我们应该确保 setState 和查询原生 DOM 元素等任何可以修改应用状态的东西不会被调用。
该函数永远不该更新应用的状态。
更新组件状态的问题在于,当状态更新时会触发另一个 render 循环,后者在内部会再触发一个 render 循环,以此类推。
复制代码
import React from "react"; export default class RenderFunctionOptimization extends React.Component { constructor() { this.state = { name: "Mayank" } } render() { this.setState({ name: this.state.name + "_" }); return ( User Name: {this.state.name}
); }}
在上面的代码中,每次调用 render 函数时都会更新状态。状态更新后组件将立即重新渲染。因此更新状态会导致 render 函数的递归调用。
render 函数应保持纯净,以确保组件以一致的方式运行和渲染。
13. 为组件创建错误边界
组件渲染错误是很常见的情况。
在这种情况下,组件错误不应该破坏整个应用。创建错误边界可避免应用在特定组件发生错误时中断。
错误边界是一个 React 组件,可以捕获子组件中的 JavaScript 错误。我们可以包含错误、记录错误消息,并为 UI 组件故障提供回退机制。
错误边界是基于高阶组件的概念。
详细信息参阅: https://levelup.gitconnected.com/introduction-to-reacts-higher-order-components-hocs-c42182fb634
错误边界涉及一个高阶组件,包含以下方法:static getDerivedStateFromError() 和 componentDidCatch()。
static 函数用于指定回退机制,并从收到的错误中获取组件的新状态。
componentDidCatch 函数用来将错误信息记录到应用中。
下面是代码示例:
复制代码
import React from 'react'; export class ErrorBoundaries extends React.Component { constructor(props) { super(props); this.state = { hasErrors: false } } componentDidCatch(error, info) { console.dir("Component Did Catch Error"); } static getDerivedStateFromError(error) { console.dir("Get Derived State From Error"); return { hasErrors: true } } render() { if(this.state.hasErrors === true) { return This is a Error
} return
}} export class ShowData extends React.Component { constructor() { super(); this.state = { name: "Mayank" } } changeData = () => { this.setState({ name: "Anshul" }) } render() { if(this.state.name === "Anshul") { throw new Error("Sample Error") } return ( This is the Child Component {this.state.name}
) }}
当 name 更新为 Anshul 时,上面的代码会抛出错误。
组件 ShowData 是 ErrorBoundaries 组件内的嵌入。
因此,如果错误是从 ShowData 函数内抛出的,则它会被父组件捕获,我们使用 static getDerivedStateFromError 函数和 componentDidCatch 生命周期事件中的日志数据部署回退 UI。
14. 组件的不可变数据结构
React 的灵魂是函数式编程。如果我们希望组件能一致工作,则 React 组件中的状态和 props 数据应该是不可变的。
对象的突变可能导致输出不一致。
复制代码
import React from "react" expoort default class ImmutableComponentData extends React.Component { constructor() { this.state = { userInfo: { name: "Mayank", age: 30, designation: "Software Architect" } } } updateUser() { this.setState({ userInfo: { name: "OtherUser" } }) } shouldComponentUpdate(nextProps, nextState) { if(nextState.userInfo != this.state.userInfo) { return true; } } render() { return ( <> User Name: {this.state.userName} > ) }}
如上所示。在 shouldComponentUpdate 函数中我们指定,如果 userInfo 的初始值与 userInfo 的新值不同,则应该重新渲染该组件;反之不应重新渲染组件。
详细信息参阅: https://blog.logrocket.com/immutability-in-react-ebe55253a1cc
15. 使用唯一键迭代
当我们需要渲染项目列表时应该为项目添加一个键。
键可以用来识别已更改、添加或删除的项目。键为元素提供了稳定的标识。一个键应该对应列表中的唯一一个元素。
如果开发人员没有为元素提供键,则它将 index 作为默认键。在下面的代码中我们默认不添加任何键,因此 index 将用作列表的默认键。
使用 index 作为键就不会出现标识不唯一的问题了,因为 index 只会标识所渲染的组件。
我们可以在以下场景中使用 index 作为键:
列表项是静态的,项目不随时间变化。
Items 没有唯一 ID。
List 永远不会重新排序或过滤。
不会从顶部或中间添加或删除项目。
复制代码
export default class ComponentRecreation extends React.Component { constructor() { super(); this.state = { inputName: "", arrayData: ["Mayank", "Meha", "Anshul", "Arjun"] } } updateUserName(event) { this.setState({ inputName: event.target.value }) } addUserData() { this.setState({ arrayData: [this.state.inputName, ...this.state.arrayData] }) } render() { var dataList = this.state.arrayData.map((data, index) => { return {data}
; }) return ( List of Users: {dataList}
) }}
在列表中添加项目
使用 index 作为键会加大错误率并降低应用的性能。
每当新元素添加到列表时,默认情况下 React 会同时遍历新创建的列表和旧列表,并在需要时进行突变。
在列表顶部添加一个新元素(包含 index 作为键)时,全部已有组件的索引都会更新。
索引更新后,之前键值为 1 的元素现在的键值变成了 2。更新所有组件会拖累性能。
上面的代码允许用户在列表顶部添加新项目。但在顶部插入元素后果最严重。因为顶部元素一变,后面所有的元素都得跟着改键值,从而导致性能下降。
因此,我们应该确保键值和元素一一对应不会变化。
总结一下:
Key 不仅影响性能,更重要的作用是标识。随机分配和更改的值不算是标识。
我们得知道数据的建模方式才能提供合适的键值。如果你没有 ID,我建议使用某种哈希函数生成 ID。
我们在使用数组时已经有了内部键,但它们是数组中的索引。插入新元素时这些键是错误的。
详细信息请参阅:
https://reactjs.org/docs/reconciliation.html?#recursing-on-children
https://medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
16. 事件节流和防抖
节流(throttling)和防抖(debouncing)可用来限制在指定时间内调用的事件处理程序的数量。
事件处理程序是响应不同事件(如鼠标单击和页面滚动)而调用的函数。事件触发事件处理程序的速率是不一样的。
节流的概念
节流意味着延迟函数执行。
这些函数不会立即执行,在触发事件之前会加上几毫秒延迟。
比如在页面滚动时,我们不会过于频繁地触发滚动事件,而是将事件延迟一段时间以便将多个事件堆叠在一起。
它确保函数在特定时间段内至少调用一次。如果函数最近运行过了,它将阻止函数运行,确保函数以固定间隔定期运行。
当我们处理无限滚动并且当用户接近页面底部必须获取数据时,我们可以使用节流。
否则滚动到页面底部将触发多个事件,并且触发对网络的多次调用,从而导致性能问题。
防抖的概念
防抖是指在调用停止一段时间之前忽略事件处理程序调用。
假设我们有一个事件,有一秒钟的 debounce 时间。一旦用户停止触发事件,该事件的事件处理程序将在一秒钟后触发。
典型的例子是用户在自动填充搜索框中键入数据。
一旦用户停止键入,就会进行 AJAX 查询以从 API 获取数据。每次键入都进行 AJAX 调用就需要多次查询数据库。
因此,我们对该事件做 debounce,直到用户不再输入数据为止,从而减少网络调用并提升性能。
我们可以使用第三方库来实现和使用节流和防抖功能,例如throttle-debounce 。具体用法参阅:https://www.npmjs.com/package/throttle-debounce。
17. 使用 CDN
谷歌、亚马逊和微软等公司提供了许多内容分发网络。
这些 CDN 是可在你的应用中使用的外部资源。我们甚至可以创建私有 CDN 并托管我们的文件和资源。
使用 CDN 有以下好处:
不同的域名 。浏览器限制了单个域名的并发连接数量,具体取决于浏览器设置。假设允许的并发连接数为 10。如果要从单个域名中检索 11 个资源,那么同时完成的只有 10 个,还有 1 个需要再等一会儿。CDN 托管在不同的域名 / 服务器上。因此资源文件可以分布在不同的域名中,提升了并发能力。
文件可能已被缓存 。有很多网站使用这些 CDN,因此你尝试访问的资源很可能已在浏览器中缓存好了。这时应用将访问文件的已缓存版本,从而减少脚本和文件执行的网络调用和延迟,提升应用性能。
高容量基础设施 。这些 CDN 由大公司托管,因此可用的基础设施非常庞大。他们的数据中心遍布全球。向 CDN 发出请求时,它们将通过最近的数据中心提供服务,从而减少延迟。这些公司会对服务器做负载平衡,以确保请求到达最近的服务器并减少网络延迟,提升应用性能。
如果担心安全性,可以使用私有 CDN。
18. 用 CSS 动画代替 JavaScript 动画
在 HTML 5 和 CSS 3 出现之前,动画曾经是 JavaScript 的专属,但随着 HTML 5 和 CSS 3 的引入情况开始变化。现在动画甚至可以由 CSS 3 来处理了。
我们可以制定一些规则:
理由如下:
破损的 CSS 规则和样式不会导致网页损坏,而 JavaScript 则不然。
解析 CSS 是非常便宜的,因为它是声明性的。我们可以为样式并行创建内存中的表达,可以推迟样式属性的计算,直到元素绘制完成。
为动画加载 JavaScript 库的成本相对较高,消耗更多网络带宽和计算时间。
虽然 JavaScript 可以提供比 CSS 更多的优化,但优化过的 JavaScript 代码也可能卡住 UI 并导致 Web 浏览器崩溃。
详细信息参阅: https://developers.google.com/web/fundamentals/design-and-ux/animations/css-vs-javascript
19. 在 Web 服务器上启用 gzip 压缩
压缩是节省网络带宽和加速应用的最简单方法。
我们可以把网络资源压缩到更小的尺寸。Gzip 是一种能够快速压缩和解压缩文件的数据压缩算法。
它可以压缩几乎所有类型的文件,例如图像、文本、JavaScript 文件、样式文件等。Gzip 减少了网页需要传输到客户端的数据量。
当 Web 服务器收到请求时,它会提取文件数据并查找 Accept-Encoding 标头以确定如何压缩应用。
如果服务器支持 gzip 压缩,资源会被压缩后通过网络发送。每份资源的压缩副本(添加了 Content-Encoding 标头)指定使用 gzip 解压。
然后,浏览器将内容解压缩原始版本在渲染给用户。
只是 gzip 压缩需要付出成本,因为压缩和解压缩文件属于 CPU 密集型任务。但我们还是建议对网页使用 gzip 压缩。
详细信息参阅: https://royal.pingdom.com/can-gzip-compression-really-improve-web-performance
20. 使用 Web Workers 处理 CPU 密集任务
JavaScript 是一个单线程应用,但在渲染网页时需要执行多个任务:
处理 UI 交互、处理响应数据、操纵 DOM 元素、启用动画等。所有这些任务都由单个线程处理。
可以使用 worker 来分担主线程的负载。
Worker 线程在后台运行,可以在不中断主线程的情况下执行多个脚本和 JavaScript 任务。
每当需要执行长时间的 CPU 密集任务时,可以使用 worker 在单独的线程上执行这些逻辑块。
它们在隔离环境中执行,并且使用进程间线程通信与主线程交互。主线程就可以腾出手来处理渲染和 DOM 操作任务。
详细信息参阅: https://medium.com/prolanceer/optimizing-react-app-performance-using-web-workers-79266afd4a7
21. React 组件的服务端渲染
服务端渲染可以减少初始页面加载延迟。
我们可以让网页从服务端加载初始页面,而不是在客户端上渲染。这样对 SEO 非常有利。
服务端渲染是指第一个组件显示的内容是从服务器本身发送的,而不是在浏览器级别操作。之后的页面直接从客户端加载。
这样我们就能把初始内容放在服务端渲染,客户端只按需加载部分页面。
其好处包括:
性能:初始页面内容和数据是从服务器本身加载的,因此我们不需要添加加载器和下拉列表,而是等待初始页面加载完毕后再加载初始组件。
SEO 优化:爬虫在应用初始加载时查找页面内容。在客户端渲染时初始 Web 页面不包含所需的组件,这些组件需要等 React 脚本等文件加载完毕后才渲染出来。
服务端渲染还可以使用第三方库,如 Next.js。详细信息参阅: https://nextjs.org/
这里有服务端渲染的示例项目: https://github.com/Mayankgupta688/reactServerRendering。只需从项目存储库执行以下步骤即可运行应用:
npm install
npm start
这个应用中“pages”文件夹里的文件是可以用服务端渲染加载的初始 URL。