React 性能优化的手段有哪些
一、是什么
React凭借virtual DOM (虚拟DOM)和diff算法拥有高效的性能,但是某些情况下,性能明显可以进一步提高。
类组件通过调用setState方法, 就会导致render,父组件一旦发生render渲染,子组件一定也会执行render渲染
当我们想要更新一个子组件的时候,如下图绿色部分:
理想状态只调用该路径下的组件render:
但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比(黄色部分),如不变则不进行更新
从上图可见,黄色部分diff算法对比是明显的性能浪费的情况
二、怎么做
常见的性能优化手段如下:
- 避免使用内联函数
- 使用React.Fragment避免额外标记添加DOM
- 使用immutable
- 懒加载组件
- 事件绑定方式
- 服务端渲染
- 使用React.Memo来缓存组件
- 使用useMemo缓存大量的计算
- 使用React.PureComponent,shouldComponentUpdate
- 避免使用匿名函数
- 延迟加载不是立即需要的组件
- 调整CSS而不是强制组件加载和卸载
- 唯一的标识
shouldComponentUpdate
-
通过shouldComponentUpdate生命周期函数来比对 state和 props,确定是否要重新渲染。默认情况下返回true表示重新渲染,如果不希望组件重新渲染,返回 false 。
-
使用 shouldComponentUpdate 避免不需要的渲染,但是如果对 props 和 state做深比较,代价很大,所以需要根据业务进行些取舍;在有子组件的情况下,为了避免子组件的重复渲染,可以通过父组件来判断子组件是否需要PureRender。
-
将 props 设置为数组或对象:每次调用 React组件都会创建新组件,就算传入的数组或对象的值没有改变,他们的引用地址也会发生改变,比如,如果按照如下的写法,那么每次渲染时 style都是一个新对象
// 不推荐 <button style={{ color: 'red' }} /> // 推荐 const style = { color: 'red' } <button style={style} /> // 不推荐 <button style={this.props.style || {} } /> // 推荐 const defaultStyle = {} <button style={this.props.style || defaultStyle } />
使用immutable
使用 immutable 不可变数据,在我们项目中使用引用类型时,为了避免对原始数据的影响,一般建议使用 shallowCopy 和 deepCopy 对数据进行处理,但是这样会造成 CPU 和 内存的浪费,所以推荐使用 immutable。
优点:
-
降低了“可变”带来的复杂度
-
节省内存,immutable 使用结构共享尽量复用内存,没有被引用的对象会被垃圾回收
-
可以更好的做撤销/重做,复制/粘贴,时间旅行
-
不会有并发问题(因为数据本身就是不可变的)
-
拥抱函数式编程
唯一的标识
给子组件设置一个唯一的 key,因为在 diff 算法中,会用 key 作为唯一标识优化渲染
使用 React Fragments 避免额外标记
用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以我们经常在组件顶部添加额外标签div。
这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用fragement。也可以用<></>
export default class NestedRoutingComponent extends React.Component {
render() {
return (
<>
<h1>This is the Header Component</h1>
<h2>Welcome To Demo Page</h2>
</>
)
}
}
懒加载组件
从工程方面考虑,webpack存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小
而在react中使用到了Suspense和 lazy组件实现代码拆分功能
const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component'));
export const johanAsyncComponent = props => (
<React.Suspense fallback={<Spinner />}>
<johanComponent {...props} />
</React.Suspense>
);
服务端渲染
采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面
服务端渲染,需要起一个node服务,可以使用express、koa等,调用react的renderToString方法,将根组件渲染成字符串,再输出到响应中。
import { renderToString } from "react-dom/server";
import MyPage from "./MyPage";
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
res.write(renderToString(<MyPage/>));
res.write("</div></body></html>");
res.end();
});
客户端使用render方法生成HTML
import ReactDOM from 'react-dom';
import MyPage from "./MyPage";
ReactDOM.render(<MyPage />, document.getElementById('app'));
事件绑定方式
从性能方面考虑,在render方法中使用bind和render方法中使用箭头函数这两种形式在每次组件render的时候都会生成新的方法实例,性能欠缺
而constructor中bind事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善
避免使用内联函数
如果我们使用内联函数,则每次调用render函数时都会创建一个新的函数实例
import React from "react";
export default class InlineFunctionComponent extends React.Component {
render() {
return (
<div>
<h1>Welcome Guest</h1>
<input type="button" onClick={(e) => { this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />
</div>
)
}
}
我们应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 render 时就不会创建单独的函数实例,如下:
import React from "react";
export default class InlineFunctionComponent extends React.Component {
setNewStateData = (event) => {
this.setState({
inputValue: e.target.value
})
}
render() {
return (
<div>
<h1>Welcome Guest</h1>
<input type="button" onClick={this.setNewStateData} value="Click For Inline Function" />
</div>
)
}
}
React.memo
React.memo用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 PureComponent 十分类似。但不同的是, React.memo 只能用于函数组件
import { memo } from 'react';
function Button(props) {
// Component code
}
export default memo(Button);
如果需要深层次比较,这时候可以给memo第二个参数传递比较函数
function arePropsEqual(prevProps, nextProps) {
// your code
return prevProps === nextProps;
}
export default memo(Button, arePropsEqual);
持续优化中…