写在前面
平时开发的时候,专注于功能实现,较少关注是否合理,
因此打算学习一下react项目相关的最佳实践,有助于少在代码里面下毒,本文为自己的总结。
本文例子引用了部分 https://github.com/ohansemmanuel/Cardie-performace.git 的代码
写在中间
场景1-父组件更新导致的不必要的子组件一起刷新
下图demo的为一个简单的功能,点击按钮,更新文案中的专业描述

我们使用react dev tool 的highlight Update 工具看看更新的组件
此时我们没有区分组件,所有的元素都写在一个页面中,点击按钮,整个app一起刷新(看到高亮的组件是最外层),然而我们实际上只更新了 文案的部分,这就是wasted render

如何解决这个问题?我们知道,当component的prop或者state 更新时,会导致re-render,基于这个思路,我们可以将文案信息和变更逻辑交给组件自己维护
// description是我们的文案内容 存在store中
// bad
class App extends Component {
handleProfessionUpdate = () => {
// 修改文案逻辑
};
...
<p>
{" "}
<span className="faint">I am</span> a {description}
</p>
<button onClick={this.handleProfessionUpdate}>
Change user's profession
</button>
...
}
// good
// app.js
class App extends Component {
handleProfessionUpdate = () => {
// 修改文案逻辑
};
render() {
return (
<Description />
<button onClick={this.handleProfessionUpdate}>
Change user's profession
</button>
}
}
}
// Description.js
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
看下效果

可以看到 re-render范围缩小了。
同样的原理,我们还应该避免将无关的数据不要放在state中,可以直接放在this中 否则会导致re-render
新的问题来了
Description 下的某些组件 并没有读取description,或者读取的是其他prop时,也会跟着父组件re-render.
但子元素的prop/state 并未改变
我们可以用purecomponent解决这个问题。它类似于componentshouldupdate,
两者的区别是,前者是将当前状态和prevState & prevProp 做浅对比,如果没有变化,则不re-render,后者则可以自定义是否更新,如果多层结构的数据,我们可以自定义是否更新
//bad
import React, { Component } from "react";
class A extends Component {
render() {
console.log("render called in <A/>");
return <span>a </span>;
}
}
export default A;
//good
import React, { PureComponent } from "react";
class A extends PureComponent {
render() {
console.log("render called in <A/>");
return <span>a </span>;
}
}
export default A;
看下效果

OK ~ 现在无关的组件没有产生waste re-render
同样的,在react.FC组件中 可以使用 React.memo() 和 useReact hook 实现同样的效果
场景2: 误传入新的对象实例导致re-render
const amClick = function(){
// ...
}
const Description = ({ description }) => {
render(){
<Am onClick={()=>this.amClick()}/>
}
);
};

上述例子中,am组件为什么re-render了呢,明明我们并没有变更回调函数的内容。
因为onclick 的回调函数每次都会返回新的函数对象
虽然内容相同 但地址不同
因此导致am组件的onClick props 属性变化
For scalar values such as strings and numbers, they are compared by value. For objects, these are compared by reference.
因此我们应该避免使用arrow func 绑定 this, 可以在construtor中绑定以替代
为了避免类似的情况 我们需要注意的还有
-
避免在render中new变量,然后将变量传给组件
-
避免在render中new HOC 。应该写在外面
-
避免使用inline-function 也就是将函数定义写在jsx中
场景3:不合理使用key
从虚拟Dom的角度来理解这个问题,当某个组件的children有好几个,他会被转成如下对象
// ...
props: {
children: [
{ type: 'div' },
{ type: 'span' },
{ type: 'br' }
]
},
// ...
假设我们逻辑中调整了子元素的顺序,就会变成
// ...
props: {
children: [
{ type: 'span' },
{ type: 'div' },
{ type: 'br' }
]
},
// ...
如果在更新的过程中,react会对比更新前后的对象,相同索引上的内容,若果不同,则会进行删除,创建,挂载的操作。
而如果实际上我们只是移动了元素,并不需要删除,创建。如果这样,代价更大。
想想一下,一个有1000个子元素的组件,把第一个子元素删除了,那么后面的元素的index-1,全部删除重建挂载,花销巨大。
因此react中有一个机制,当有key,可以通过key值对比元素,因此,合理使用key可以提高更新效率
场景4:使用code-spliting 可以避免我们将所有的逻辑都打包到一个地方,并且一次从传输
如果有条件渲染的组件,特别是大的组件,可以使用react.lazy懒加载,减少主js的体积。
const OneComponent = React.lazy(() => import('./OneComponent'));
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
let isIOS = getCurrentEnv();
return (
<div>
{
isIOS ? <OneComponent /> : <OtherComponent />>
}
</div>
);
}
场景5:使用 React Fragments 避免多余的标签
因为每个react组件只能有一个根节点,因此我们经常不得不在外层包裹一个空的div,因此我们可以使用 React Fragments 避免一个多渲染一个标签的浪费。
总结
1 react页面的交互性能主要解决的是两类问题,挂载和更新
挂载有以下最佳实践
- 使用 React Fragments
- 使用code split,做懒加载
更新有以下最佳实践
-
使用purcomponent/shouldupdatecomponent/useMemo/react.memo()
使用 shouldupdatecomponent 自定义是否更新时,如果遇到复杂的数据结构,推荐使用Immutable.js,效率更高。
-
避免将无关的数据不要放在state中,可以直接放在this中
-
避免在render中new变量,然后将变量传给组件
-
避免在render中new HOC 。应该写在外面
-
避免使用inline-function 也就是将函数定义写在jsx中
-
合理使用key
-
避免使用inline-style
2 还有很多其他的实践方案在这里不一一赘述,开发的时候可以结合火焰图,react profile,chrome audit,chrome performance等工具。找出性能消耗的重点,排查问题,优化代码中不合理的地方
3 文章内容为个人理解,如有不正确的地方,务必指正..🐶
参考文章