减少不必要的渲染
合理使用shouldComponentUpdate
class App extends React.Component {
state = {
count: 1,
};
shouldComponentUpdate(nextProps, nextState) {
if(nextState.count === this.state.count) {
return false
}
return true
}
click = () => {
this.setState({ count: 1 });
};
render() {
console.log('--------render')
return <button onClick={this.click}>{this.state.count}</button>;
}
}
合理使用PureComponent
PureComponent 在shouldComponentUpdate中实现了浅比较(比较最外层属性)
class App extends React.PureComponent {
state = {
count: 1,
};
click = () => {
// this.setState({ count: this.state.count+1 });
this.setState({count: 1})
};
render() {
console.log('--------render')
return <button onClick={this.click}>{this.state.count}</button>;
}
}
PureComponent原理
class App extends React.Component {
state = {
count: 1,
};
shouldComponentUpdate(nextProps, nextState) {
const hasNotChange =
objEqual(this.props, nextProps) &&
objEqual(this.state, nextState)
if(hasNotChange === true){
return false
}
return true
}
click = () => {
// this.setState({ count: this.state.count+1 });
this.setState({count: 1})
};
render() {
console.log('--------render')
return <button onClick={this.click}>{this.state.count}</button>;
}
}
// 浅比较
function objEqual(oldObj, newObj) {
for(let key in newObj) {
if(oldObj[key] !== newObj[key]) {
return false
}
}
return true
}
合理使用memo
函数组件用memo,实现和 pureComponent一样的功能
问题:
- 父组件更新, 子组件也触发了更新
- 但是子组件的props并没有发生变化, 并不需要重新渲染
import React, { useState, useCallback } from 'react@18';
import { createRoot } from 'react-dom@18/client';
const Test = function () {
const [count1, setCount1] = useState<number>(0);
const handleClick = () => {
setCount1(count1 + 1);
}
return <div onClick={handleClick}>
<h1>click me {count1}</h1>
<Child />
</div>;
};
let Child = (props) => {
alert('-----render')
return <h1>
{props.count}
</h1>
}
解决: 使用Reat.memo
import React, { useState, useCallback } from 'react@18';
import { createRoot } from 'react-dom@18/client';
const Test = function () {
const [count1, setCount1] = useState<number>(0);
const handleClick = () => {
setCount1(count1 + 1);
}
return <div onClick={handleClick}>
<h1>click me {count1}</h1>
<Child />
</div>;
};
let Child = (props) => {
alert('-----render')
return <h1>
{props.count}
</h1>
}
Child = React.memo(Child) // <---------------
原理
function memo(FnComponent) {
return class Memo extends React.PurComponent {
render(){
return <>{FnComponent(this.props)}</>
}
}
}
useMemo缓存数据
- 如果依赖的数据经常变化,就不用useMemo
- 如果依赖的数据偶尔变化,就可以用useMemo
问题: 每次修改n1, getN3 就会执行一次
const Test = function () {
const [n1, setN1] = useState(0)
const [n2] = useState(0)
const click = () => setN1(n1+1)
const getN3 = () => {
alert('getN2执行了')
return n2+100
}
return (
<>
<h1 onClick={click} >{n1}</h1>
<h1>{getN3()}</h1>
</>
)
};
解决: 使用useMemo
const Test = function () {
const [n1, setN1] = useState(0)
const [n2] = useState(0)
const click = () => setN1(n1+1)
const n3 = useMemo(() => {
return n2+100
}, [n2])
return (
<>
<h1 onClick={click} >{n1}</h1>
<h1>{n3}</h1>
</>
)
};
问题: React.memo 在下面情况无法阻止子组件发生渲染
const Test = function () {
const [n1, setN1] = useState(0)
const [n2, setN2] = useState(18)
const click = () => setN1(n1+1)
const jack = {age: n2}
return (
<>
<h1 onClick={click} >{n1}</h1>
<Child a={jack} />
</>
)
};
let Child = (props) => {
alert('---render')
return <h1>child: {props.a.age}</h1>
}
Child = React.memo(Child)
解决: 使用useMemo
const Test = function () {
const [n1, setN1] = useState(0)
const [n2, setN2] = useState(18)
const click = () => setN1(n1+1)
const jack = useMemo(() => {
return {age: n2}
}, [n2])
return (
<>
<h1 onClick={click} >{n1}</h1>
<Child a={jack} />
</>
)
};
let Child = (props) => {
alert('---render')
return <h1>child: {props.a.age}</h1>
}
Child = React.memo(Child)
useCallback 缓存函数
- 如果依赖值经常变化, 使用useCallback就没有意义
问题: 即使使用了React.memo, 父组件更新, 子组件也会更新
import { memo, useCallback, useMemo, useState } from "react";
let Child = ({ info }) => {
console.log("---child render");
return <h3>child: {info.name}</h3>;
}
// 子组件使用了 memo 进行浅比较
Child = memo(Child) // <<--------
export default function App() {
const [num, setNum] = useState(0);
const cb = () => {}
return (
<div className="App">
<h1 onClick={() => setNum(num + 1)}>{num}</h1>
<Child cb={cb} />
</div>
);
}
使用useCallback解决了上面问题
import { memo, useCallback, useMemo, useState } from "react";
let Child = ({ info }) => {
console.log("---child render");
return <h3>child: {info.name}</h3>;
}
// 子组件使用了 memo 进行浅比较
Child = memo(Child) // <<--------
export default function App() {
const [num, setNum] = useState(0);
const cb = useCallback(() => {}, []);
return (
<div className="App">
<h1 onClick={() => setNum(num + 1)}>{num}</h1>
<Child cb={cb} />
</div>
);
}
减少层级
使用Fragment
return <>
<div>xxx</div>
</>
避免多层级的嵌套组件
- 当组件层级嵌套过深时,会增加diff算法的复杂度和渲染时间。尽量保持组件的层级扁平和简洁。
- 就是尽量减少fiber节点的数量
class 组件
在构造函数中bind(this)
good
class MyComponent extends React.Component {
constructor(){
this.click = this.click.bind(this)
}
click(){
alert(1111)
}
render(){
return <Button onClick={this.click}>按钮</Button>
}
}
bad
- 如果这样写
- 数据每更新一次,
render就执行一次 render每执行一次,bind就会执行一次- 上面的写法不会频繁的执行
bind
render(){
return <Button onClick={this.click.bind(this)}>按钮</Button>
}
jsx中不要定义函数
bad
- 数据每更新一次, render就执行一次
- render每执行一次, 函数就会被定义一次
<Button onClick={() => {...}}>按钮</Button>
good
- 数据更新, render方法会不停的执行
- 函数写在外面, 避免被不停创建
class MyComponent extends React.Component {
click = () => {...}
render(){
return <Button onClick={this.click}>按钮</Button>
}
}
模拟v-show
// v-show
<MyComponent style={{display: flag ? 'block' : 'none'}} />
// v-if
{show && <MyComponent />}
useState优化
- 可以在useState中传入函数提升性能
// ❌ 错误:每次渲染都会执行 calculateInitialValue()
const [value] = useState(calculateInitialValue());
// ✅ 正确:只在组件挂载时执行一次
const [value] = useState(() => {
return calculateInitialValue(); // 复杂计算
});
Suspense & 懒加载
对于不是首次加载就需要的组件,可以采用懒加载的方式,按需加载它们,减少初始渲染体积和提高首屏加载速度。在React中,可以使用Suspense和lazy组件来实现。
- 使用
React.Lazy和import()来引入组件 - 使用
<React.Suspense></React.Suspense>来做异步组件的父组件,并使用 fallback 来实现组件未加载完成时展示信息 fallback可以传入html,也可以自行封装一个统一的提示组件
const Child = lazy(
/* webpackChunkName: 'Chidl'*/
() => import("./child")
);
export default function App() {
return (
<h1 className="App">
<Suspense fallback={<h1>loading...</h1>}>
<Child msg="aaa" />
</Suspense>
</h1>
);
}
自定义事件dom事件及时销毁
export default function App() {
const [w, setW] = useState(0);
function getW(e: UIEvent) {
setW(e.target.innerWidth);
}
useEffect(() => {
window.addEventListener("resize", getW);
return () => {
window.removeEventListener("resize", getW);
};
}, []);
return <h1 className="App">{w}</h1>;
}
列表渲染性能优化key
- 组件是否更新条件:
- props发生变化
- state发生变化
- 不加key:
[1,2,3] -> [4,1,2,3]- 1 更新成 4
- 2 更新成 1
- 3 更新成 2
- 重新创建3添加到尾部
- 加key:
[1,2,3] -> [4,1,2,3]- 1, 2, 3, 都不变
- 创建4添加到头部
const Child = memo<{ msg: number }>(({ msg }) => {
console.log(msg, "---child render");
return <h3>child {msg}</h3>;
});
export default function App() {
const [arr, setArr] = useState([1, 2, 3]);
// 添加到前面: 如果不加key 所有的Chid都会重新渲染
const addHead = () => setArr([Math.random(), ...arr]);
// 添加到后面: 加不加key都不影响
const addEnd = () => setArr([...arr, Math.random()]);
return (
<div className="App">
<p onClick={addHead}>添加到前面: 如果不加key 所有的Chid都会重新渲染</p>
<p onClick={addEnd}>添加到后面: 加不加key都不影响</p>
{arr.map((num) => (
<Child msg={num} key={num} />
))}
</div>
);
}
合理使用Immutable.js
在React中使用Immutable数据可以减少渲染的次数。Immutable数据在更新时会产生新的对象,而不是修改原对象,这有助于在shouldComponentUpdate中进行浅比较,从而避免不必要的渲染。
类似的库还有immer
const jack = Immutable.Map({
name: "jack",
age: 22
});
const tom = jack.set("name", "tom");
jack.get("name"); // jack
tom.get("name"); // tom
拆分
- 第三方库的代码基本上是不会变的
- 可以将他们分包出来缓存到本地
- 第二次加载时直接从本地读取
- 分析打包结果
- 路由懒加载
- 将react-dom单独打一个包
- 将antd单独打一个包
- 将其他第三方库单独打一个包
- 可以将包从1.7M优化到30kb
其他
除了上述React特有的优化策略外,还可以考虑一些通用的前端性能优化手段,如资源加载优化、减少重绘与回流、服务端渲染、启用CDN等。这些策略同样适用于React应用,可以进一步提升应用的性能。
需要注意的是,性能优化是一个持续的过程,需要根据应用的实际情况和性能瓶颈进行针对性的优化。同时,也要注意不要过度优化,以免引入不必要的复杂性和维护成本。