React 的性能优化
核心思想是将变与不变的部分分离
- props
- state
- context
父组件的 state 可能会作为子组件的 props,父组件的 state 也可能作为子孙组件的 context。
例子:
点击查看代码
import { useState } from "react";
function ExpensiveCpn() {
let now = performance.now();
while (performance.now() - now < 100) {}
console.log("耗时的组件 render");
return <p>耗时的组件</p>;
}
export default function App() {
const [num, updateNum] = useState(0);
return (
<div>
<input value={num} onChange={(e) => updateNum(+e.target.value)} />
<p>num is {num}</p>
<ExpensiveCpn />
</div>
);
}
上面这个例子里,每次改变状态 num,耗时的组件就会执行多一次。这个 num 就是变的部分。下面我们尝试把变与不变的部分分离。
例子 1 解决方法
import { useState } from "react";
function ExpensiveCpn() {
let now = performance.now();
while (performance.now() - now < 100) {}
console.log("耗时的组件 render");
return <p>耗时的组件</p>;
}
function Input() {
const [num, updateNum] = useState(0);
return (
<>
<input value={num} onChange={(e) => updateNum(+e.target.value)} />
<p>num is {num}</p>
</>
);
}
export default function App() {
return (
<div>
<Input />
<ExpensiveCpn />
</div>
);
}
可以看到,分离了以后,耗时的组件只在首次渲染的时候执行了一次。
例子 2(HOC 高阶组件)
点击查看代码
import { useState } from "react";
function ExpensiveCpn() {
let now = performance.now();
while (performance.now() - now < 100) {}
console.log("耗时的组件 render");
return <p>耗时的组件</p>;
}
export default function App() {
const [num, updateNum] = useState(0);
return (
<div title={num + ""}>
<input value={num} onChange={(e) => updateNum(+e.target.value)} />
<p>num is {num}</p>
<ExpensiveCpn />
</div>
);
}
上面的例子里多了一个 div 也依赖 num,原则也是将变和不变抽离。
例子 2(HOC 高阶组件) 解决方法
import React, { ReactNode, useState } from "react";
function ExpensiveCpn() {
let now = performance.now();
while (performance.now() - now < 100) {}
console.log("耗时的组件 render");
return <p>耗时的组件</p>;
}
function InputWrapper({ children }: { children: ReactNode }) {
const [num, updateNum] = useState(0);
return (
<div title={num + ""}>
<input value={num} onChange={(e) => updateNum(+e.target.value)} />
<p>num is {num}</p>
{children}
</div>
);
}
export default function App() {
return (
<InputWrapper>
<ExpensiveCpn />
</InputWrapper>
);
}
上面将变的部分抽离到了 InputWrapper 里,当父组件满足性能优化条件,子孙组件才有可能命中性能优化。
例子 3 (性能优化 API)
点击查看代码
import React, { useState, useContext } from "react";
const numCtx = React.createContext < number > 0;
const updateNumCtx = React.createContext < React.Dispatch < number >> (() => {});
function Button() {
const updateNum = useContext(updateNumCtx);
console.log("btn render");
return <button onClick={() => updateNum(Math.random())}>产生随机数</button>;
}
function Show() {
const num = useContext(numCtx);
return <p>num is: {num}</p>;
}
const Middle = () => {
return (
<>
<Button />
<Show />
</>
);
};
export default function App() {
const [num, updateNum] = useState(0);
return (
<numCtx.Provider value={num}>
<updateNumCtx.Provider value={updateNum}>
<Middle />
</updateNumCtx.Provider>
</numCtx.Provider>
);
}
上面的例子中,变化的部分在 APP、Show 里。这里要注意 updateNum 是一个函数,是不变的。因为每次 APP 都会改变,默认情况下是 diff 是使用全等比较,然后传递给子组件的 props 的地址值已经改变了(即使是),所以子组件也会判定为发送改变。
例子 3(性能优化 API) 解决方法
import React, { useState, useContext } from "react";
const numCtx = React.createContext < number > 0;
const updateNumCtx = React.createContext < React.Dispatch < number >> (() => {});
function Button() {
const updateNum = useContext(updateNumCtx);
console.log("btn render");
return <button onClick={() => updateNum(Math.random())}>产生随机数</button>;
}
function Show() {
const num = useContext(numCtx);
return <p>num is: {num}</p>;
}
const Middle = React.memo(() => {
return (
<>
<Button />
<Show />
</>
);
});
export default function App() {
const [num, updateNum] = useState(0);
return (
<numCtx.Provider value={num}>
<updateNumCtx.Provider value={updateNum}>
<Middle />
</updateNumCtx.Provider>
</numCtx.Provider>
);
}
这里我们使用 React.memo 来优化,React.memo 会告诉 React 这个组件比较 prop 变化的方式由全等比较变为浅比较,当然也可以使用 useMemo 来优化,useMemo 的作用是缓存一个变量,它会帮我们记住原本的 props,它仅会在某个依赖项改变时才重新计算 memoized 值。例子里因为 Middle 是不变的,并且 props 也没有发生改变,所以没有重新渲染,所以子组件的 props 也未改变,所以子组件也不会重新渲染。
React 性能优化常见的几种方式
1. 使用 HOC(高阶组件)
参考上面
2. 使用 React.memo
参考上面
3. 使用 useMemo
有时渲染是不可避免的,但如果您的组件是一个功能组件,重新渲染会导致每次都调用大型计算函数,这是非常消耗性能的,我们可以使用新的 useMemo 钩子来“记忆”这个计算函数的计算结果。这样只有传入的参数发生变化后,该计算函数才会重新调用计算新的结果。
通过这种方式,您可以使用从先前渲染计算的结果来挽救昂贵的计算耗时。总体目标是减少 JavaScript 在呈现组件期间必须执行的工作量,以便主线程被阻塞的时间更短。
// 避免这样做
function Component(props) {
const someProp = heavyCalculation(props.item);
return <AnotherComponent someProp={someProp} />;
}
// 只有 `props.item` 改变时someProp的值才会被重新计算
function Component(props) {
const someProp = useMemo(() => heavyCalculation(props.item), [props.item]);
return <AnotherComponent someProp={someProp} />;
}
使用 React.PureComponent , shouldComponentUpdate
React.PureComponent 和 React.memo 类似,它会告诉 React 这个组件比较 prop 变化的方式由全等比较变为浅比较,shouldComponentUpdate 则是在内部来确保只有当组件 props 状态改变时才会重新渲染。如下例子:
export default function ParentComponent(props) {
return (
<div>
<SomeComponent someProp={props.somePropValue}
<div>
<AnotherComponent someOtherProp={props.someOtherPropValue} />
</div>
</div>
)
}
export default function SomeComponent(props) {
return (
<div>{props.someProp}</div>
)
}
// 只要props.somePropValue 发生变化,不论props.someOtherPropValue是否发生变化该组件都会发生变化
export default function AnotherComponent(props) {
return (
<div>{props.someOtherProp}</div>
)
}
// 第一种优化
class AnotherComponent extends React.PureComponent {
render() {
return <div>{this.props.someOtherProp}</div>;
}
}
//第二种优化
class AnotherComponent extends Component {
shouldComponentUpdate(nextProps) {
return this.props !== nextProps;
}
render() {
return <div>{this.props.someOtherProp}</div>;
}
}
4. 使用 useCallback
useCallback 会返回一个 memoized 的回调函数,当依赖项改变时才会重新创建该函数,否则会返回之前创建的函数。避免重复创建函数、减少 props 的变化,提高性能。
5. 避免使用内联对象
使用内联对象时,react 会在每次渲染时重新创建对此对象的引用,这会导致接收此对象的组件将其视为不同的对象,因此,该组件对于 prop 的浅层比较始终返回 false,导致组件一直重新渲染。许多人使用的内联样式的间接引用,就会使组件重新渲染,可能会导致性能问题。为了解决这个问题,我们可以保证该对象只初始化一次,指向相同引用。另外一种情况是传递一个对象,同样会在渲染时创建不同的引用,也有可能导致性能问题,我们可以利用 ES6 扩展运算符将传递的对象解构。这样组件接收到的便是基本类型的 props,组件通过浅层比较发现接受的 prop 没有变化,则不会重新渲染。示例如下:
// Don't do this!
function Component(props) {
const aProp = { someProp: "someValue" };
return <AnotherComponent style={{ margin: 0 }} aProp={aProp} />;
}
// Do this instead :)
const styles = { margin: 0 };
function Component(props) {
const aProp = { someProp: "someValue" };
return <AnotherComponent style={styles} {...aProp} />;
}
6. 避免使用匿名函数
虽然匿名函数是传递函数的好方法(特别是需要用另一个 prop 作为参数调用的函数),但它们在每次渲染上都有不同的引用。这类似于上面描述的内联对象。为了保持对作为 prop 传递给 React 组件的函数的相同引用,您可以将其声明为类方法(如果您使用的是基于类的组件)或使用 useCallback 钩子来帮助您保持相同的引用(如果您使用功能组件)。当然,有时内联匿名函数是最简单的方法,实际上并不会导致应用程序出现性能问题。这可能是因为在一个非常“轻量级”的组件上使用它,或者因为父组件实际上必须在每次 props 更改时重新渲染其所有内容。因此不用关心该函数是否是不同的引用,因为无论如何,组件都会重新渲染。
// 避免这样做
function Component(props) {
return <AnotherComponent onChange={() => props.callback(props.id)} />;
}
// 优化方法一
function Component(props) {
const handleChange = useCallback(() => props.callback(props.id), [props.id]);
return <AnotherComponent onChange={handleChange} />;
}
// 优化方法二
class Component extends React.Component {
handleChange = () => {
this.props.callback(this.props.id);
};
render() {
return <AnotherComponent onChange={this.handleChange} />;
}
}
7. 延迟加载不是立即需要的组件
延迟加载实际上不可见(或不是立即需要)的组件,React 加载的组件越少,加载组件的速度就越快。因此,如果您的初始渲染感觉相当粗糙,则可以在初始安装完成后通过在需要时加载组件来减少加载的组件数量。同时,这将允许用户更快地加载您的平台/应用程序。最后,通过拆分初始渲染,您将 JS 工作负载拆分为较小的任务,这将为您的页面提供响应的时间。这可以使用新的 React.Lazy 和 React.Suspense 轻松完成。
// 延迟加载不是立即需要的组件
const MUITooltip = React.lazy(() => import("@material-ui/core/Tooltip"));
function Tooltip({ children, title }) {
return (
<React.Suspense fallback={children}>
<MUITooltip title={title}>{children}</MUITooltip>
</React.Suspense>
);
}
function Component(props) {
return (
<Tooltip title={props.title}>
<AnotherComponent />
</Tooltip>
);
}
8. 调整 CSS 而不是强制组件加载和卸载
渲染成本很高,尤其是在需要更改 DOM 时。每当你有某种手风琴或标签功能,例如想要一次只能看到一个项目时,你可能想要卸载不可见的组件,并在它变得可见时将其重新加载。如果加载/卸载的组件“很重”,则此操作可能非常消耗性能并可能导致延迟。在这些情况下,最好通过 CSS 隐藏它,同时将内容保存到 DOM。尽管这种方法并不是万能的,因为安装这些组件可能会导致问题(即组件与窗口上的无限分页竞争),但我们应该选择在不是这种情况下使用调整 CSS 的方法。另外一点,将不透明度调整为 0 对浏览器的成本消耗几乎为 0(因为它不会导致重排),并且应尽可能优先于更该 visibility 和 display。有时在保持组件加载的同时通过 CSS 隐藏可能是有益的,而不是通过卸载来隐藏。对于具有显著的加载/卸载时序的重型组件而言,这是有效的性能优化手段。
// 避免对大型的组件频繁对加载和卸载
function Component(props) {
const [view, setView] = useState('view1');
return view === 'view1' ? <SomeComponent /> : <AnotherComponent />
}
// 使用该方式提升性能和速度
const visibleStyles = { opacity: 1 };
const hiddenStyles = { opacity: 0 };
function Component(props) {
const [view, setView] = useState('view1');
return (
<React.Fragment>
<SomeComponent style={view === 'view1' ? visibleStyles : hiddenStyles}>
<AnotherComponent style={view !== 'view1' ? visibleStyles : hiddenStyles}>
</React.Fragment>
)
}
9. 使用 React.Fragment 避免添加额外的 DOM
有些情况下,我们需要在组件中返回多个元素,例如下面的元素,但是在 react 规定组件中必须有一个父元素。
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
因此你可能会这样做,但是这样做的话即使一切正常,也会创建额外的不必要的 div。这会导致整个应用程序内创建许多无用的元素:
function Component() {
return (
<div>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</div>
);
}
实际上页面上的元素越多,加载所需的时间就越多。为了减少不必要的加载时间,我们可以使 React.Fragment 来避免创建不必要的元素。
function Component() {
return (
<React.Fragment>
<h1>Hello world!</h1>
<h1>Hello there!</h1>
<h1>Hello there again!</h1>
</React.Fragment>
);
}
总结
React
性能确实不算太好,这是不争的事实。原因在于 React
自顶向下的更新机制。然后 React
性能优化完全交给开发者。
每次状态更新,React
都会从根组件开始深度优先遍历整棵组件树。既然遍历方式是固定的,我们就可以通过寻找遍历时可以跳过的子树
来进行优化。
这里并没有列出其他第三方库优化的方法,但其实性能优化的方法有很多,但正如上面所说的,合适的方法也要在合适的场景下使用,过度的使用性能优化反而会得不偿失。
参考资料
https://www.bilibili.com/video/BV1Yr4y1J7oc/ (opens in a new tab)
https://www.bilibili.com/video/BV1j44y1g74m/ (opens in a new tab)
https://juejin.cn/post/6844903924302888973 (opens in a new tab)