React 性能优化
React 性能优化
React 性能优化常见手段
ui = render(data)
通过数据去驱动视图的更新,性能优化的手段可以主要围绕减少视图的更新
优化内容:尽量减少视图的更新
目的:减少不必要的组件更新
在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。从 React 的声明式设计理念来看,如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用也不应该发生改变。当子组件符合声明式设计理念时,就可以忽略子组件本次的 Render 过程。PureComponent 和 React.memo 就是应对这种场景的,PureComponent 是对类组件的 Props 和 State 进行浅比较,React.memo 是对函数组件的 Props 进行浅比较。
- pureComponent 纯组件
React.component –> shouldComponentUpdate
浅对比 props 和 state,当传入的props没有发生变化,组件不会重新渲染
// 类组件
class Demo1 extends React.PureComponent {
render(){
// 当传入的props没有发生变化,组件不会重新渲染
const { value } = this.props;
return <div>{ value }</div>
}
}
- React.memo (HOC)类似于pureComponent
// 函数组件
const Demo2 = React.memo( function( props ){
// 当传入的props没有发生变化,组件不会重新渲染
const { value } = props;
return <div>{ value }</div>
} )
- shouldComponentUpdate
组件render之前会执行该生命周期函数,返回值为true,组件会重新渲染;返回值为false,组件不会渲染。
update: 第一次更新后 -> shouldComponentUpdate -> render
class Demo3 extends React.component {
state = {
count: 0
}
shouldComponentUpdate( nextProps, nextState ){
// 逻辑判断 return true / false
}
}
场景:在项目初始阶段,开发者往往图方便会给子组件传递一个大对象作为 Props,后面子组件想用啥就用啥。当大对象中某个「子组件未使用的属性」发生了更新,子组件也会触发 Render 过程。在这种场景下,通过实现子组件的 shouldComponentUpdate 方法,仅在「子组件使用的属性」发生改变时才返回 true
,便能避免子组件重新 Render。
- React 组件中,会将公用数据放在需要更新状态的公共祖先组件上
- comp
- compA
- subCompA // count
- compB
- subCompB // count
- compC
- subCompC
在subCompA 和 subCompB 组件中,都需要共同的数据 count ,此时就可以将数据定义在公共组件 comp 内 通过props进行传递数据
也可以使用React.Context 向下传递公共数据
-
props
-
provider context
<Function.Provider> -> 定义context
<componentA></componentA> -> 通过hooks 获取 context
</Function.Provider>
HOC (high order component)高阶组件,传入一个组件,返回一个组件,在内部可以进行一些定制化的操作
- 状态下放,缩小状态影响范围
如果一个状态只在某部分子树中使用,那么可以将这部分子树提取为组件,并将该状态移动到该组件内部。使状态变化时所影响的面降到最低。
会导致频繁渲染ExpensiveTree组件,导致不必要的性能损耗:
import { useState } from "react"
export default function App() {
let [color, setColor] = useState("red")
return (
<div>
<input value={color} onChange={e => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
<ExpensiveTree /> // 比较耗时的操作
</div>
)
}
优化:
export default function App() {
return (
<>
<Form />
<ExpensiveTree />
</>
)
}
// Form 组件
function Form() {
let [color, setColor] = useState("red")
return (
<>
<input value={color} onChange={e => setColor(e.target.value)} />
<p style={{ color }}>Hello, world!</p>
</>
)
}
- key (Diff算法
- useMemo 相较于React.memo,颗粒度更细
如果传给子组件的派生状态或函数,每次都是新的引用,那么 PureComponent
和 React.memo
优化就会失效。所以需要使用 useMemo
和 useCallback
来生成稳定值,并结合 PureComponent
或 React.memo
避免子组件重新 Render。
useCallBack 尽量不要用:
1.大部分场景没有提升性能
2.代码可读性差,会产生很多依赖项,无法判断
利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
- 更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
- 更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值。
下方父组件状态更新后,不使用 useMemo 的子组件会执行 Render 过程,而使用 useMemo 的子组件不会执行:
import { useEffect, useMemo, useState } from "react"
import "./styles.css"
const renderCntMap = {}
function Comp({ name }) {
renderCntMap[name] = (renderCntMap[name] || 0) + 1
return (
<div>
组件「{name}」 Render 次数:{renderCntMap[name]}
</div>
)
}
export default function App() {
const setCnt = useState(0)[1]
useEffect(() => {
const timer = window.setInterval(() => {
setCnt(v => v + 1)
}, 1000)
return () => clearInterval(timer)
}, [setCnt])
const comp = useMemo(() => {
return <Comp name="使用 useMemo 作为 children" />
}, [])
return (
<div className="App">
<Comp name="直接作为 children" />
{comp}
</div>
)
}
前端通用优化
优先展示用户行为
优先级更新是批量更新的逆向操作,其思想是:优先响应用户行为,再完成耗时操作。
通过人工交互行为,影响对应操作的顺序
import { memo, useState } from "react";
import "./styles.css";
function getDefaultNumbers() {
const total = 100000;
return new Array(total)
.fill(0)
.map((it) => Math.floor(Math.random() * total))
.sort((a, b) => a - b);
}
const IntegerList = memo(({ numbers }) => {
console.log("render IntegerList");
return (
<div>
{numbers.map((it, idx) => (
<span
key={idx}
style={{
display: "inline-block",
textAlign: "center",
width: "60px"
}}
>
{it}
</span>
))}
</div>
);
});
export default function App() {
const [numbers, setNumbers] = useState(getDefaultNumbers());
const [showInput, setShowInput] = useState(false);
const [inputValue, setInputValue] = useState("");
const slowHandle = () => {
setShowInput(false);
setNumbers([...numbers, +inputValue].sort((a, b) => a - b));
};
const fastHandle = () => {
setShowInput(false);
// 异步下次操作,需要放在下次宏任务中
setTimeout(() => {
setNumbers([...numbers, +inputValue].sort((a, b) => a - b));
});
};
return (
<div className="App">
{showInput ? (
<div>
<input
placeholder="输入整数"
style={{ marginRight: 20 }}
value={inputValue}
onChange={(v) => setInputValue(v.target.value)}
></input>
<br />
<button onClick={slowHandle} style={{ marginTop: 8 }}>
点击我添加该整数,页面卡顿,不会立即反馈给用户结果
</button>
<br />
<button onClick={fastHandle} style={{ marginTop: 8 }}>
点击我添加该整数,输入框立即消失,立即反馈给用户页面响应
</button>
</div>
) : (
<button onClick={() => setShowInput(true)}>点我添加一个整数</button>
)}
<IntegerList numbers={numbers}></IntegerList>
</div>
);
}
在上方案例中,slowHandle 会造成点击按钮不会立即进行隐藏,要等待数据处理完毕,如果该操作比较耗时,则会产生长时间的等待;fastHandle 会优先相应用户的点击事件(隐藏按钮),再去执行数据处理
intersection observer API - - 组件优化
IntersectionObserver
接口(从属于 Intersection Observer API)提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。
Intersection Observer API(Intersection Observer API)提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。
- 在页面滚动时“懒加载”图像或其他内容。
- 实现“无限滚动”网站,在滚动过程中加载和显示越来越多的内容
- 报告广告的可见度,以便计算广告收入。
- 根据用户是否能看到结果来决定是否执行任务或动画进程。
具体请查阅官方文档
IntersectionObserver
支持两个参数:
callback 是当被监听元素的可见性变化时,触发的回调函数
options 是一个配置参数,可选,有默认的属性值
var observer = new IntersectionObserver(callback, options);
callback
目标元素的可见性变化时,就会调用观察器的回调函数callback。
callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
相关配置
//初始化一个实例
var observer = new IntersectionObserver(changes => {
for (const change of changes) {
console.log(change.time);
// Timestamp when the change occurred
// 当可视状态变化时,状态发送改变的时间戳
// 对比时间为,实例化的时间,
// 比如,值为1000时,表示在IntersectionObserver实例化的1秒钟之后,触发该元素的可视性变化
console.log(change.rootBounds);
// Unclipped area of root
// 根元素的矩形区域信息,即为getBoundingClientRect方法返回的值
console.log(change.boundingClientRect);
// target.boundingClientRect()
// 目标元素的矩形区域的信息
console.log(change.intersectionRect);
// boundingClientRect, clipped by its containing block ancestors,
// and intersected with rootBounds
// 目标元素与视口(或根元素)的交叉区域的信息
console.log(change.intersectionRatio);
// Ratio of intersectionRect area to boundingClientRect area
// 目标元素的可见比例,即intersectionRect占boundingClientRect的比例,
// 完全可见时为1,完全不可见时小于等于0
console.log(change.target);
// the Element target
// 被观察的目标元素,是一个 DOM 节点对象
// 当前可视区域正在变化的元素
}
}, {});
// Watch for intersection events on a specific target Element.
// 对元素target添加监听,当target元素变化时,就会触发上述的回调
observer.observe(target);
// Stop watching for intersection events on a specific target Element.
// 移除一个监听,移除之后,target元素的可视区域变化,将不再触发前面的回调函数
observer.unobserve(target);
// Stop observing threshold events on all target elements.
// 停止所有的监听
observer.disconnect();
options
传递到 IntersectionObserver()
构造函数的 options
对象,可以控制在什么情况下调用观察器的回调。它有以下字段:
-
用作视口的元素,用于检查目标的可见性。必须是目标的祖先。如果未指定或为
null
,则默认为浏览器视口。 -
根周围的边距。其值可以类似于 CSS
margin
属性,例如"10px 20px 30px 40px"
(上、右、下、左)。这些值可以是百分比。在计算交叉点之前,这组值用于增大或缩小根元素边框的每一侧。默认值为全零。 -
一个数字或一个数字数组,表示目标可见度达到多少百分比时,观察器的回调就应该执行。如果只想在能见度超过 50% 时检测,可以使用 0.5 的值。如果希望每次能见度超过 25% 时都执行回调,则需要指定数组 [0, 0.25, 0.5, 0.75, 1]。默认值为 0(这意味着只要有一个像素可见,回调就会运行)。值为 1.0 意味着在每个像素都可见之前,阈值不会被认为已通过。
案例:
- 懒加载
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var observer = new IntersectionObserver(
function(changes) {
changes.forEach(function(change) {
var container = change.target;
var content = container.querySelector('template').content;
container.appendChild(content);
observer.unobserve(container);
});
}
);
query('.lazy-loaded').forEach(function (item) {
observer.observe(item);
});
- 无限滚动
var intersectionObserver = new IntersectionObserver(
function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return;
loadItems(10);
console.log('Loaded new items');
});
// 开始观察
intersectionObserver.observe(
document.querySelector('.scrollerFooter')
);
babel-plugin-import
为组件库实现单组件按需加载并且自动引入其样式,如:
import { Button } from 'antd';
↓ ↓ ↓ ↓ ↓ ↓
var _button = require('antd/lib/button');
require('antd/lib/button/style');
只需关心需要引入哪些组件即可,内部样式我并不需要关心
如何用 ?
.babelrc
或 babel-loader. :
{
"plugins": [["import", options]]
}
// options 配置如下
[
{
"libraryName": "antd",
"libraryDirectory": "lib", // default: lib
"style": true
},
{
"libraryName": "antd-mobile"
},
]
// For Example:
// .babelrc
"plugins": [
["import", { "libraryName": "antd", "libraryDirectory": "lib"}, "antd"],
["import", { "libraryName": "antd-mobile", "libraryDirectory": "lib"}, "antd-mobile"]
]