常用React性能优化方案
一、React.lazy进行路由切换优化
使用React.lazy动态加载组件,减少性能浪费
- React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
- 详见官方文档:代码分割
使用之前:
import OtherComponent from './OtherComponent';
使用之后:
const OtherComponent = React.lazy(() => import('./OtherComponent'));
- 使用方法: src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import {HashRouter as Router,Route,Link} from 'react-router-dom';
import {dynamic} from './utils';
const LoadingHome = dynamic(()=>import('./components/Home'));
const LoadingUser = dynamic(()=>import('./components/User'));
ReactDOM.render(
<Router>
<ul>
<li><Link to="/">Home</Link></li>
<li> <Link to="/user">User</Link></li>
</ul>
<Route path="/" exact={true} component={LoadingHome}/>
<Route path="/user" component={LoadingUser}/>
</Router>
,document.getElementById('root'));
- 核心原理: src\utils.js
const Loading = () => <div>Loading</div>;
export function dynamic(loadComponent) {
const LazyComponent = lazy(loadComponent)
return () => (
<React.Suspense fallback={<Loading />}>
<LazyComponent />
</React.Suspense>
)
}
function lazy(load) {
return class extends React.Component {
state = { Component: null }
componentDidMount() {
load().then(result => {
this.setState({ Component: result.default});
});
}
render() {
let { Component } = this.state;
return Component && <Component />;
}
}
}
二、使用PureComponent和memo进行更新阶段优化
- 类组件使用
PureCompnent包裹实现props的浅比较,防止props浅层不变情况下子组件的重复渲染 - 函数式组件使用memo包裹可以达到同样效果
import React from 'react';
import {PureComponent,memo} from './utils';
export default class App extends React.Component{
constructor(props){
super(props);
this.state = {title:'计数器',number:0}
}
add = (amount)=>{
this.setState({number:this.state.number+amount});
}
render(){
console.log('App render');
return (
<div>
<Counter number={this.state.number}/>
<button onClick={()=>this.add(1)}>+1</button>
<button onClick={()=>this.add(0)}>+0</button>
<ClassTitle title={this.state.title}/>
<FunctionTitle title={this.state.title}/>
</div>
)
}
}
class Counter extends PureComponent{
render(){
console.log('Counter render');
return (
<p>{this.props.number}</p>
)
}
}
class ClassTitle extends PureComponent{
render(){
console.log('ClassTitle render');
return (
<p>{this.props.title}</p>
)
}
}
const FunctionTitle = memo(props=>{
console.log('FunctionTitle render');
return <p>{props.title}</p>;
});
- PureComponent和memo的实现原理
import React from 'react';
export class PureComponent extends React.Component{
shouldComponentUpdate(nextProps,nextState){
return !shallowEqual(this.props,nextProps)||!shallowEqual(this.state,nextState)
}
}
export function memo(OldComponent){
return class extends PureComponent{
render(){
return <OldComponent {...this.props}/>
}
}
}
export function shallowEqual(obj1,obj2){
if(obj1 === obj2)
return true;
if(typeof obj1 !== 'object' || obj1 ===null || typeof obj2 !== 'object' || obj2 ===null){
return false;
}
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if(keys1.length !== keys2.length){
return false;
}
for(let key of keys1){
if(!obj2.hasOwnProperty(key) || obj1[key]!== obj2[key]){
return false;
}
}
return true;
}
三、immutable解决memo浅比较陷阱
-
由于memo使用的是
Object.is()进行,只能比较一层(浅比较),如果props嵌套较深是无法识别到组件变化的,所以很多时候我们需要深比较,但是常规深比较涉及到大量递归算法,严重影响性能,但是借助immutable.js可以提高深比较效率。 -
immutable.js会将引用对象变成一个immutable对象,改变某一属性的时候,会更新当前属性以及它所有的父节点属性,其余属性保持不变,实现数据复用,提高深层次比较效率
import React from 'react';
import {PureComponent} from './utils';
+import { Map } from "immutable";
export default class App extends React.Component{
constructor(props){
super(props);
+ this.state = {count:Map({ number: 0 })}
}
add = (amount)=>{
+ let count = this.state.count.set('number',this.state.count.get('number') + amount);
+ this.setState({count});
}
render(){
console.log('App render');
return (
<div>
<Counter number={this.state.count.get('number')}/>
<button onClick={()=>this.add(1)}>+1</button>
<button onClick={()=>this.add(0)}>+0</button>
</div>
)
}
}
class Counter extends PureComponent{
render(){
console.log('Counter render');
return (
<p>{this.props.number}</p>
)
}
}
四、借助react-window的FixedSizeList进行大数据量渲染优化
-
大数据量数据进行渲染的时候,我们通常会使用虚拟列表方案进行优化
-
用数组保存所有列表元素的位置,只渲染可视区内的列表元素,当可视区滚动时,根据滚动的offset大小以及所有列表元素的位置,计算在可视区应该渲染哪些元素
-
react-window[www.npmjs.com/package/rea…]
-
fixed-size[react-window.now.sh/#/examples/…]
-
react-virtualized[react-window.now.sh/#/examples/…]
react-window使用示例
import React, { Component, lazy, Suspense } from "react";
import ReactDOM from "react-dom";
import { FixedSizeList as List } from 'react-window';
const Row = ({ index, style }) => (
<div style={style}>Row {index}</div>
);
const Container = () => (
<List
height={150}
itemCount={1000}
itemSize={35}
width={300}
>
{Row}
</List>
);
ReactDOM.render(<Container/>, document.querySelector("#root"));
自定义虚拟列表
- src\index.js
import React from 'react';
import { render } from 'react-dom';
//import VirtualList from 'react-tiny-virtual-list';
import VirtualList from './components/VirtualList';
const data = new Array(30).fill(0);
render(
<VirtualList
width='50%'
height={500}
itemCount={data.length}
itemSize={50}
renderItem={(data) => {
let { index, item, style } = data;
console.log(data);
return (
<div key={index} style={{ ...style, backgroundColor: index % 2 === 0 ? 'green' : 'orange' }}>
{index+1}
</div>
)
}
}
/>,
document.getElementById('root')
);
- VirtualList.js实现原理
- src\components\VirtualList.js
import React from 'react';
export default class Index extends React.Component {
scrollBox = React.createRef()
state = {start: 0}
handleScroll = () => {
const { itemSize } = this.props;
const { scrollTop } = this.scrollBox.current;
const start = Math.floor(scrollTop / itemSize);
this.setState({start})
}
render() {
const { height, width, itemCount, itemSize, renderItem } = this.props;
const { start } = this.state;
let end = start + Math.floor(height/itemSize)+1;
end = end>itemCount?itemCount:end;
const visibleList = new Array(end - start).fill(0).map((item,index)=>({index:start+index}));
const style = {position:'absolute',top:0,left:0,width:'100%', height: itemSize};
return (
<div
style={{overflow: 'auto',willChange:'transform', height,width}}
ref={this.scrollBox}
onScroll={this.handleScroll}
>
<div style={{position: 'absolute',width:'100%',height: `${itemCount * itemSize}px`}}>
{
visibleList.map(({index}) => renderItem({ index, style:{...style,top:itemSize*index} }))
}
</div>
</div>
)
}
}
五、React hooks中使用useImmer进行性能优化
使用useMemo缓存昂贵计算
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
-
返回一个 memoized 值。
-
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
使用useImmerState处理共享数据
- 使用示例1
import React from 'react';
import ReactDOM from 'react-dom';
import {useImmerState} from './immer'
let id = 1;
function Todos() {
const [todos, setTodos] = useImmerState({
list: []
})
const addTodo = () => setTodos((draft) => {
draft.list.push(id++)
})
return (
<>
<button onClick={addTodo}>增加</button>
<ul>
{
todos.list.map((item, index) => <li key={index}>{item}</li>)
}
</ul>
</>
)
}
ReactDOM.render(
<Todos />,
document.getElementById('root')
);
使用useImmer库
import React from "react";
import { useImmer } from "use-immer";
function App() {
const [person, updatePerson] = useImmer({
name: "Michel",
age: 33
});
function updateName(name) {
updatePerson(draft => {
draft.name = name;
});
}
function becomeOlder() {
updatePerson(draft => {
draft.age++;
});
}
return (
<div className="App">
<h1>
Hello {person.name} ({person.age})
</h1>
<input
onChange={e => {
updateName(e.target.value);
}}
value={person.name}
/>
<br />
<button onClick={becomeOlder}>Older</button>
</div>
);
}
使用useRef缓存每次渲染的不变值
useRef可以拿来当class组件的this上下文来用,而且用useRef定义的变量更新的时候并不会更新视图层,这个特性和class组件里的this变量相同。
const countdown = useRef(0)
const countdownHandler = () => {
if (countdown.current === 10) return
setTimeout(() => {
countdown.current += 1
countdownHandler()
}, 1000)
}
六、其他渲染方案
- 骨架屏
- 预渲染
- 图片懒加载
七、关于性能优化的实践思考
-
并不是所有函数组件都使用memo包裹就是性能优化! 如果一个子组件过分依赖于父组件的状态,那么对于这个子组件来说使用memo包裹的意义可有可无,但是memo本身计算对比也是需要时间的。那么,如果某个子组件跟随父组件重新渲染的次数比例很大,那额外的memo对比时间就成为了负担,哪怕这个时间非常短。
-
不要过度依赖于useMemo useMemo本身也是有开销的,因为记忆函数本身是将依赖项数组中的依赖取出来,和上一次记录的值进行对比,如果相等才会节省本次的计算开销,否则就需要重新执行回调,这个过程本身就是消耗一定的内存和计算资源。
那么,什么时候使用useMemo,思考以下2个问题?
- 传递给useMemo的函数开销是否大?
有些业务场景的计算开销会非常大,那么这个时候我们需要去缓存上一次的值,避免每一次父组件重新渲染就进行重新计算;如果开销并不大,那么可能useMemo本身的开销就超过了所节省的时间
- 计算出来的值类型是否是复杂类型?
如果返回的是复杂类型(object、array),由于每次重新渲染哪怕值不变都会生成新的引用,导致子组件重新渲染,那么可以使用useMemo;如果在父组件中使用useMemo计算出来的是基本类型的值,则子组件使用memo就可以浅比较避免重新渲染,无需使用useMemo