前言
我们现在刚开始学习React,接下来可能就要开始做项目,那日常开发中,我们经常需要把一个页面要拆分成多个组件, 通过拼装嵌套的方式来实现整体的页面效果,所以与其说去优化 React,不如聚焦在现有的组件中,思考🤔如何去设计一个组件才能提高他的性能,从而提高整个项目的性能以及交互的流畅性。
思考?
import React, { Component, useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
return (
<div>
<h2>App: count: {count}</h2>
<HelloWorld></HelloWorld>
<button onClick={(e) => setCount(count + 1)}> +1 </button>
</div>
);
};
class HelloWorld extends Component {
render() {
console.log("子组件渲染");
return <div>HelloWorld</div>;
}
}
export default App
我们看下这段代码,就是很简单的父组件嵌套子组件,大家有发现什么特别的地方吗,我们其实很容易的观察出来,每当我点击+1, 我们的子组件HelldWorld都会被渲染一次,但是我明明改变的父组件的count,我子组件完全没有任何地方有用到count,那能不能想办法优化一下,就是你在点击+1的时候,我子组件不进行渲染。
性能优化实践
由于 React 中的组件分为 Function(函数) 组件和 Class (类)组件,优化的手段根据其特性不同也分为两类。
Function(函数) 组件
memo
memo – React 中文文档 (docschina.org)
React.memo 是 React 提供的一个高阶组件,用于优化组件的性能。它可以在某些情况下避免不必要的组件重新渲染,从而提高应用程序的性能。其使用方式分为两种:
- 基础使用 函数组件直接包裹 memo 默认使用浅层比较。
- 高阶使用 如果需要更精确地控制何时重新渲染组件,可以通过传递第二个参数给 memo 来指定自定义的比较函数。这个比较函数接收两个参数,分别是前一次的 props 和当前的 props ,返回一个布尔值表示是否需要重新渲染组件。
import React, {memo} from 'react';
const areEqual = (prevProps, nextProps) => {
// 自定义比较逻辑
// 返回 true 表示两个 props 相等,不需要重新渲染
// 返回 false 表示两个 props 不相等,需要重新渲染 return prevProps.value === nextProps.value; };
const MyComponent = memo((props) => {
console.log('Rendering MyComponent');
return <div>{props.value}</div>;
}, areEqual);
useEffect
useEffect – React 中文文档 (docschina.org)
官方说法:useEffect 是一个 React Hook,它允许你 将组件与外部系统同步。
这么说很抽象,实际上我们可以把 useEffect 叫做副作用处理函数。它拥有生命周期特性,組件在更新或卸载之前会回调这个函数。我们可以在 useEffect 函数中,发送网络请求、监听、store.subscribe(卸载)
import React, {memo} from 'react';
const HeaderRight = memo(() => {
// 定义状态
const [showPanel, setShowPanel] = useState(false);
// 副作用处理函数
// 当前传入的回调函数会在组件渲染完成后,自动执行
useEffect(() => {
const windowHandleClick = ()=> {
setShowPanel(false);
}
window.addEventListener('click', windowHandleClick,
//设置捕获
true);
// 取消监听
return ()=> {
window.removeEventListener('click', windowHandleClick, true)
}
}, []);
// 事件处理函数
const panelHandleClick = () => {
setShowPanel(true);
};
return (
<div>
{showPanel && (
<div className="panel">注册</div>
)}
</RightWrapper>
);
});
export default HeaderRight;
useCallback
useCallback 是 React 中的一个 Hook,用于优化性能和避免不必要的渲染。它主要用于创建一个稳定的回调函数,并在依赖项未发生变化时缓存该函数。
import React, { memo, useCallback, useRef, useState } from "react";
// memo 允许你的组件在 props 没有改变的情况下跳过重新渲染。
const HYHome = memo((props)=> {
const {increment} = props
return (
<div>
<button onClick={e => increment()}>修改message HYHome</button>
{/* 100个子组件 */}
</div>
)
})
const App = memo(() => {
const [counter, setCounter] = useState(1);
const [message, setMessage] = useState("Hello");
// const increment = e=> {setCounter(counter + 1)}
// 当一个函数需要给多个组件使用,我们可以使用useCallback进行性能优化,只有当特定值发生改变时,方法才会重新执行
// 闭包陷阱
const increment = useCallback(
// 回调函数
function increment() {
setCounter(counter + 1);
console.log("渲染");
},
// 依赖值
[counter]
);
// 进一步优化: counter 改变时也使用同一个函数
// 方法1: 将counter直接移除:缺点: 闭包陷阱, increment只会加1 ,之后不会改变
// 方法2: useRef,在组件多次渲染后,返回的是同一个值
// const counterRef = useRef()
// counterRef.current = counter
// const increment = useCallback(
// // 回调函数
// function increment() {
// setCounter(counterRef.current + 1);
// console.log("渲染");
// },
// // 依赖值
// []
// );
// 每次渲染都会被定义一次
return (
<div>
<h2>当前计数; {counter}</h2>
消息: {message}
<button onClick={increment}>+ 1</button>
<button onClick={(e) => setMessage(Math.random())}>修改message</button>
<HYHome increment={increment}></HYHome>
</div>
);
});
export default App;
useMemo
它用于在组件渲染过程中进行记忆化计算,以避免不必要的重复计算,提高应用的性能。 使用场景:
- 计算昂贵的计算结果:涉及到需要执行昂贵的计算或处理大量数据的情况下,可以使用
useMemo将计算结果缓存起来 - 避免不必要的渲染:某个组件的渲染结果仅依赖于特定的输入参数,并且这些参数没有发生变化时,可以使用
useMemo缓存该组件的输出,避免不必要的重新渲染
import React, { memo, useMemo, useState } from "react";
const calcNumber = (num) => {
console.log("calcNumber渲染");
let total = 0;
for (let i = 0; i < num; i++) {
total += i;
}
return total;
};
const HelloWord = memo(() => {
console.log('hell word')
return <div>Hello Word</div>;
})
// let total = calcNumber(50)
const App = memo(() => {
const [counter, setCounter] = useState(0);
// 1.不依赖任何的值,进行计算
let total = calcNumber(50)
// let total = useMemo(() => {
// return calcNumber(50);
// }, []);
// 2.依赖counter
// let total = useMemo(() => {
// return calcNumber(counter * 2);
// }, [counter]);
// function fn () {}
//3. useCallback和useMemo 比较
// 返回有记忆的函数
// const result = useCallback(fn, []);
// 返回有记忆的值 对函数的返回值做优化
// const result = useMemo(() => fn, []);
// 4.使用useMemo对子组件渲染进行优化
const info = useMemo(()=> ({ age: 18, name: 'why'}), [])
return (
<div>
<h2>计数: {counter} </h2>
<button onClick={(e) => setCounter(counter + 1)}>+ 1</button>
{/* <h2>计算结果: {calcNumber(50)}</h2> */}
<h2>{total}</h2>
<HelloWord total={total} info={info}/>
</div>
);
});
export default App;
Class (类)组件
PureComponent
!!仅支持React 15.3及以上版本 PureComponent 是继承自 React.Component 的一个子类,它额外实现了shouldComponentUpdate 方法,并通过对组件的 props 和 state 进行浅层比较来确定是否需要重新渲染组件。如果 props 和 state 没有发生改变,就不会执行 render 函数,省去了生成 Virtual DOM 和 Diff 的过程。
合理使用shouldComponentUpdate
如果想对渲染进行更加细微的控制,或者是对引用类型进行渲染控制我们可以使用shouldComponentUpdate,通过返回 true 进行更新, false 阻止不必要的更新。
shouldComponentUpdate (nextProps, nextState) {
if(this.state.message !== nextState.message || this.state.counter !== nextState.counter) {
return true
} else {
return false
}
}
特别注意⚠️:合理使用,手动实现 shouldComponentUpdate 可能会增加代码的复杂性,并且过度使用它可能会导致更多的维护问题。只有在确实需要优化性能时,才建议使用它。
使用lazy进行组件懒加载
过组件懒加载可以将代码分割成更小的块,并且只有在需要时才会被加载 当用户访问某个特定页面时,只有与该页面相关的代码会被下载和执行,而其他代码则不会被加载。这样可以使应用程序更快地启动,并减少页面响应延迟。通常我们在编写路由的时候会使用到懒加载
import React from "react";
// 懒加载 打包出来是单独的
const About = React.lazy(()=>import('../pages/About'))
const Login = React.lazy(()=>import('../pages/Login'))
// 这些都是同步加载,没有做分包处理
const routes = [
{
path: "/",
element: <Navigate to="/home" />,
}
]
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { HashRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<HashRouter>
{/* 懒加载的组件 */}
<Suspense fallback={<h3>Loading</h3>}>
<App />
</Suspense>
</HashRouter>
</React.StrictMode>
);