React的性能问题
React最大的一个性能问题:React的某个组件的更新(state状态和props属性更新都会让组件更新)会连带着它的子组件一起更新。
解决这个问题的方法:
- 源码上尽量弥补这个问题(React的时间切片)
- 让子组件只做合理的更新
React的时间切片
Vue有依赖收集,做到了最小的更新范围。而React并没有做依赖收集,所以React要更新的话会更新整个组件树,这样就会有很大的diff算法对比和计算工作。如果有大量的更新操作,虚拟dom对比和计算就会花很大的时间,这样可能会阻塞浏览器的渲染,导致页面长时间白屏。
React为了解决这个问题选择的策略就是时间切片。时间切片就是先计算一部分的更新,然后让渲染进程进行渲染,然后再进行下一步更新。如此往复,就不会出现长时间的白屏了。
React会保障在执行更新操作时计算的时间不能超过16ms,如果超过16ms就需要先暂停,让给浏览器进行渲染。
为了支持这种切片,需要将更新转化为一个一个的单元,然后也必须要有恢复上一次中断的计算进度的能力。React就设计了一种数据结构——fiber,每一个组件都会被转化为fiber结构的对象,然后组成一个一个的单元,fiber让React有恢复上一次中断的计算进度的能力。
在组件树中每个组件单元都有child指针指向当前组件的子组件和sibling指针指向当前组件的兄弟组件。
无用渲染的问题和解决方法
父组件state状态的数据更改导致子组件更新
父组件数据的更改导致父组件进行更新操作,而父组件的更新会连带着它的子组件一起进行更新。
// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx"; // 引入子组件
export default function App() {
console.log("App父组件渲染了")
let [num, setNum] = useState(0);
const numChange = () => {
setNum(++num);
}
return (
<div>
<h1>App父组件</h1>
<p>{num}</p>
<button onClick={numChange}>点击加1</button>
<Son />
</div>
)
}
// Son.jsx
import React from "react";
const Son = (props) => {
console.log("Son组件渲染了")
return (
<>
<h2>Son组件</h2>
</>
)
}
export default Son;
在父组件中更新了父组件state状态的num数据,导致父组件更新了。这里没有修改子组件的state状态,按理来说子组件不会更新,但父组件的更新导致了子组件的更新。
解决方法:在类组件中可以使用PureComponent来避免父组件数据更改导致子组件更新,在函数组件中使用React.Memo避免父组件数据更改导致子组件更新。
// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx"; // 引入子组件
export default function App() {
console.log("App父组件渲染了")
let [num, setNum] = useState(0);
const numChange = () => {
setNum(++num);
}
return (
<div>
<h1>App父组件</h1>
<p>{num}</p>
<button onClick={numChange}>点击加1</button>
<Son />
</div>
)
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
console.log("Son组件渲染了")
return (
<>
<h2>Son组件</h2>
</>
)
}
// memo方法接受一个组件,它会返回一个新的组件。memo实际上就是一个高阶组件
const MemoSon = memo(Son);
export default MemoSon;
组件的state状态修改为同样的值而产生的更新
解决方法:类组件的PureComponent以及函数组价本身会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。
// App.jsx
import React from "react"
import Des from "./Des.jsx";
export default function App() {
console.log("App组件渲染了");
return (
<div>
<Des />
</div>
)
}
// Des.jsx
import React from "react"
class Des extends React.Component {
state = {
msg: "您收到一条消息"
}
changeMsg = () => {
this.setState({ msg: "您收到一条消息" })
}
render() {
console.log("Des组件渲染了");
return (
<>
<h1>Des组件</h1>
<p>{this.state.msg}</p>
<button onClick={this.changeMsg}>修改</button>
</>
)
}
}
export default Des
Des类组件的state状态的msg数据修改的值都是同一个,但是组件还是更新了。
解决方法:类组件继承于PureComponent组件,而非继承于Component组件。PureComponent组件会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。
// App.jsx
import React from "react"
import Des from "./Des.jsx";
export default function App() {
console.log("App组件渲染了");
return (
<div>
<Des />
</div>
)
}
// Des.jsx
import React from "react"
class Des extends React.PureComponent {
state = {
msg: "您收到一条消息"
}
changeMsg = () => {
console.log("修改方法执行了");
this.setState({ msg: "您收到一条消息" })
}
render() {
console.log("Des组件渲染了");
return (
<>
<h1>Des组件</h1>
<p>{this.state.msg}</p>
<button onClick={this.changeMsg}>修改</button>
</>
)
}
}
export default Des
函数组价本身会进行判断state状态修改的是不是同样的值,是的话就不会进行更新操作,不是的话就会进行更新操作。
// App.jsx
import React from "react"
import Son from "./Son.jsx";
export default function App() {
console.log("App组件渲染了");
return (
<div>
<Son />
</div>
)
}
// Son.jsx
import React, { useState } from "react";
const Son = () => {
console.log("Son组件渲染了");
let [msg, setMsg] = useState("您收到一条消息");
const changeHandle = () => {
console.log("修改方法执行了");
setMsg("您收到一条消息");
}
return (
<>
<h1>Son组件</h1>
<p>{msg}</p>
<button onClick={changeHandle}>修改</button>
</>
)
}
export default Son
父组件props属性传入方法、对象、数组这些引用类型时而导致的子组件的更新
即使类组件中使用PureComponent和函数组件中使用React.Memo来避免了父组件state状态的数据更改导致子组件更新,以及做到了如果父组件传入的props属性不改变就不会触发子组件的更新。但是只要父组件传入子组件的props的引用发生变化,子组件就会重新渲染。
这是因为在React中,当父组件向子组件传递引用类型的props(如对象、数组、方法)时,React通过浅比较来检测props的变化,如果引用不同,React会认为传入子组件的props发生了变化,从而触发子组件的更新。
以下是一些常见的情况,子组件可能因为新的props引用而更新:
- 每次渲染都创建新对象或数组:
如果父组件在每次渲染时都创建一个新的对象或数组,并将其作为props传递给子组件,那么即使这些对象或数组的内容与上次相同,但由于它们是不同的引用,React会认为props已改变并重新渲染子组件。
- 使用状态管理库时的不当操作:
在使用如Redux等状态管理库时,如果状态树中的某个部分被替换为一个新对象(即使内容相同),这将导致所有订阅该状态的组件接收到新的引用,从而触发重新渲染。
- 函数组件的
useState或useReducer:
当使用useState或useReducer等Hook时,如果状态是一个对象或数组,并且状态更新函数返回了一个新的引用,即使这个新的引用与旧的引用内容完全相同,子组件仍然会被重新渲染。
举例其中一个说明:
// App.jsx
import React, { useState } from "react"
import Son from "./Son.jsx";
export default function App() {
console.log("App组件渲染了");
let [num,setNum] = useState(0);
const addChange = () => {
setNum(num++);
}
let obj = {
title:"Earth Sun"
}
let fn = () => {
console.log(123);
}
return (
<div>
<h1>App组件</h1>
<p>{num}</p>
<button onClick={addChange}>点我加1</button>
<Son fn={fn} obj={obj} />
</div>
)
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
console.log(props,99)
console.log("Son组件渲染了");
return (
<>
<h2>Son组件</h2>
<p>{props.obj.title}</p>
</>
)
}
const MemoSon = memo(Son)
export default MemoSon
App组件中修改了App组件的
state状态的num数据,让App组件的函数重新运行了,从而让obj对象和fn方法这两个引用数据被重新创建了,它们的引用地址发生了改变,所以传入Son组件的props发生了改变,也就让Son组件重新渲染了。
解决方法:用useCallback包裹传递给子组件的方法,传给子组件的非state状态的对象和数组的数据要用useMemo包裹起来。useCallback用于缓存一个方法让这个方法不会被重新创建,useMemo用于缓存一个数据让这个数据不会被重新创建。因为useCallback和useMemo这两个方法开销也很大,大量使用也会有性能问题,所以推荐只有给子组件的方法,值用它们包裹起来。
// App.jsx
import React, { useState, useCallback, useMemo } from "react"
import Son from "./Son.jsx";
export default function App() {
console.log("App组件渲染了");
let [num, setNum] = useState(0);
const addChange = () => {
setNum(num++);
}
// 传入空数组,只在页面初次加载时会执行第一个参数的函数体,创建这个对象数据
let obj = useMemo(() => {
return {
title: "Earth Sun"
}
},[])
// 传入空数组,只在页面初次加载时会执行第一个参数的函数体,创建这个方法数据
let fn = useCallback(() => {
console.log(123);
}, [])
return (
<div>
<h1>App组件</h1>
<p>{num}</p>
<button onClick={addChange}>点我加1</button>
<Son fn={fn} obj={obj} />
</div>
)
}
// Son.jsx
import React, { memo } from "react";
const Son = (props) => {
console.log(props,99)
console.log("Son组件渲染了");
return (
<>
<h2>Son组件</h2>
<p>{props.obj.title}</p>
</>
)
}
const MemoSon = memo(Son)
export default MemoSon