菜单 学习猿地 - LMONKEY

 Vue转React两个月来总结的性能优化方法
ZackLee

Vue转React两个月来总结的性能优化方法

ZackLee profile image ZackLee ・1 min read

课程推荐:前端开发工程师--学习猿地精品课程

换了新公司,工作中使用的技术栈也从Vue换到了React,作为一个React新人,经常的总结和思考才能更快更好的了解这个框架。这里分享一下我这两个月来使用React总结的一些性能优化的方法。
因为目前公司的项目是全面拥抱hooks的,所以只会涉及function组件写法,不包含class组件写法的相关内容。注意:本文只涉及到一些业务开发层面的代码优化,很多通用的优化思想,比如虚拟列表,图片懒加载,节流防抖,webpack优化等等内容都不会涉及到。
React的更新机制
要来优化代码,首先我们来简单了解一下React的更新机制。看下图
file
我们重点来看第一步到第二步这个过程,当一个组件的props或state改变的时候,当前组件会重新render。当前组件下面的所有子、孙、曾孙。。。组件不管是否用到了父组件的props,全都会重新render。这是一个跟Vue更新原理很大的区别,Vue中所有组件的渲染都是由各自的data控制,父组件跟子组件的渲染都是独立的。
本文关于React的性能优化,主要是三块内容,

提高diff算法的dom重复利用率
减少资源的加载
减少组件的render次数和计算量(最重点的一块)

遍历列表使用key
这个跟React的diff算法有关,是一个很简单,可以作为必须遵守规范的一个优化。
在所有的需要遍历的列表当中,都加上一个key值,这个值不能是那种遍历时候的序号,必须是一个固定值。比如该条数据id。
这个key可以帮助diff算法更好的复用dom元素,而不是销毁再重新生成。
精简不必要的节点
因为React的diff算法跟Vue一样是对于虚拟dom从父到子,一层层同级的比较。所以减少节点的嵌套,可以有效的减少diff算法的计算量。



我的名字:{name}




我的简介: {content}




// 完全可以精简为


我的名字:{name}


我的简介: {content}



复制代码
精简state
不需要把所有状态都放在组件的state中,只有那些需要响应式的数据才应该存入state。
不要使用CSS内联样式
在React中处理样式有三种

css Module
css in js(以styled-components为代表的)
内联css (把样式写在组件的style里)

对于css Module和css in js来说,其实都有优缺点,用哪个其实都没问题。虽然很多人说css Module性能要比css in js好,但是那点性能真的不值一提。
这边要说的是内联css,如果你没有那种必须通过控制style来修改组件内容或者样式的需求的话,千万不要写。
这块在后面render的优化中会细讲。
使用useMemo减少重复计算
来看一个例子
import React from 'react';

export default function App() {
const [num, setNum] = useState(0);

const [factorializeNum, setFactorializeNum] = useState(5);

// 阶乘函数
const factorialize = (): Number => {
console.log('触发了');
let result = 1;
for (let i = 1; i <= factorializeNum; i++) {
result *= i;
}
return result;
};

return (
<>
{num}
setNum(num + factorialize())}>修改num
setFactorializeNum(factorializeNum + 1)}>修改阶乘参数
</>
);
}
复制代码
在这个组件里,每次点击修改num这个按钮,都会打印一次触发了,阶乘函数会重新计算一遍。但是其实参数是没有变化的,返回的结果也是没有变化的。
我们可以使用useMemo来缓存计算结果,避免重复计算。
import React, { useMemo } from 'react';

export default function App() {
const [num, setNum] = useState(0);

const [factorializeNum, setFactorializeNum] = useState(5);

// 当factorializeNum值不变的时候,这个函数不会再重复触发了
const factorialize = useMemo((): Number => {
console.log('触发了');
let result = 1;
for (let i = 1; i <= factorializeNum; i++) {
result *= i;
}
return result;
}, [factorializeNum]);

return (
<>
{num}
setNum(num + factorialize())}>修改num
setFactorializeNum(factorializeNum + 1)}>修改阶乘参数
</>
);
}
复制代码
多用三元表达式
我们写一些组件的时候经常会碰到这种需求,根据参数的不同,渲染不同的组件。例
const App = () => {
const [type, setType] = useState(1);

if (type === 1) {
return (
<>
component1
component2
component3
</>
);
}

return (
component2
component3
);
};
复制代码
上面的代码乍一看其实没啥问题,根据类型的不同,返回不同的组件。但是对于diff算法来说,它会对同级的新旧节点进行比较,当类型变化的时候,Component1没有生成了,对于diff算法来说,他会拿旧的第一项Component1跟新的第一项Component2比较,因为没有key,而且这是组件, diff算法会深入到组件的子元素中再去同级比较。假设这三个组件都是不一样的,diff算法就会把旧节点的三个组件全部销毁,再重新生成两个新组件。
但是按性能来说,其实只需要销毁第一个组件,复用剩下的那两个就可以。
加key当然可以,但是我们可以使用更简单的三元表达式。
<>
{type === 1 && component1}
component2
component3
</>
复制代码
当类型不符合的时候,三元表达式会放置一个null,diff算法会拿这个null跟旧的component1进行比较,剩下的两个组件顺序不变,diff算法会进行复用。而且这种方式,代码也更加精简。
异步组件(懒加载组件)
最典型场景是tab页面切换,当tab切换到相应的页面上时,再去加载相应页面的组件js。
这些的组件资源不会包含在主包里,在后续在用户需要的时候,再去加载相关的组件js资源。可以提高页面的加载速度,减少无效资源的加载。
主要用到两个方法React.Suspense和React.lazy
import React from 'react';

export default (props) => {
return (
<>



}>
{React.lazy(() => import('./Component1'))}



}>
{React.lazy(() => import('./Component2'))}




</>
);
};
复制代码
使用上面的方法之后,webpack会把这个import的组件单独打包成一个js。在tab切换到相应的页面时,加载这个js,渲染出相应的组件。
减少组件的render(重点)
使用React.memo
我们先来看个例子
import React from 'react';

const Child = () => {
console.log('触发Child组件渲染');
return (

这是child组件的渲染内容!


)
};

export default () => {
const [num, setNum] = useState(0);

return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
复制代码
当我们每次点击num加1这个按钮的时候,我们都会在控制台发现打印了一次触发Child组件渲染。说明Child这个组件在我们父组件的state变化之后,每次都会重新render。
我们可以使用React.memo来避免子组件的重复render。
import React from 'react';

const Child = React.memo(() => {
console.log('触发Child组件渲染');
return (

这是child组件的渲染内容!


)
});

export default () => {
const [num, setNum] = useState(0);

return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
复制代码
React.memo会判断子组件的props是否有改变,如果没有,将不会重复render。这时候我们点击num加1按钮,Child将不会重复渲染。
不要直接使用内联对象
我们再来看一个例子
import React from 'react';

const Child = React.memo((props) => {
const { style } = props;
console.log('触发Child组件渲染');
return (

这是child组件的渲染内容!


)
});

export default () => {
const [num, setNum] = useState(0);

return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
复制代码
这个相比较上一个例子,就是给Child组件多传入了一个style参数。传入的参数是一个静态的对象,你觉得现在子组件会重复渲染吗?
一开始我觉得不会,实际测试下来,发现子组件又开始了重复渲染。
state改变,父组件重新render的时候,像这种{color: 'green'}会重新生成,这个对象的内存地址会变成一个新的。而React.memo只会对props进行浅层的比较,因为传入对象的内存地址修改了,所以React.memo就以为传入的props有新的修改,就重新渲染了子组件。
我们可以有两种方式来修改。
// 如果传入的参数是完全独立的,没有任何的耦合
// 可以将该参数,提取到渲染函数之外
const childStyle = { color: 'green' };
export default () => {
const [num, setNum] = useState(0);

return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
// 如果传入的参数需要使用渲染函数里的参数或者方法
// 可以使用useMemo
export default () => {
const [num, setNum] = useState(0);
const [style, setStyle] = useState('green');
// 如果不需要参数
const childStyle = useMemo(() => ({ color: 'green' }), []);
// 如果需要使用state或者方法
const childStyle = useMemo(() => ({ color: style }), [style]);
return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
复制代码
传入组件的函数使用React.useCallback
函数导致子组件重新渲染的原理跟上面的内联对象一样,也是因为父组件的重新渲染,导致函数方法的内存地址发生变化,所以React.memo会认为props有变化,导致子组件重复渲染。
我们可以使用React.useCallback来缓存函数方法,避免子组件的重复渲染。
export default () => {
const [num, setNum] = useState(0);
const oneFnc = useCallback(() => {
console.log('这是传入child的方法');
}, []);
return (
<>
{num}
setNum(num + 1)}>num加1

</>
);
}
复制代码
同理,要避免在子组件的传入参数上直接写匿名函数。
// 不要直接写匿名函数

console.log('这是传入child的方法')} />
复制代码
使用children来避免React Context子组件的重复渲染
对于我们常用的Context,我们不但可以使用React.Memo来避免子组件的重复渲染,我们还可以通过children的方式。
import React, { useContext, useState } from 'react';

const DemoContext = React.createContext();

const Child = () => {
console.log('触发Child组件渲染');
return (

这是child组件的渲染内容!


)
};

export default () => {
const [num, setNum] = useState(0);
return (

setNum(num + 1)}>num加1

{...一些其他需要使用num参数的组件}

);
}
复制代码
在这里可以使用children方法来避免Child的重复渲染。
import React, { useContext, useState } from 'react';

const DemoContext = React.createContext();

const Child = () => {
console.log('触发Child组件渲染');
return (

这是child组件的渲染内容!


)
};

function DemoComponent(props) {
const { children } = props;
const [num, setNum] = useState(0);
return (

setNum(num + 1)}>num加1
{children}

);
}

export default () => {
return (


{...一些其他需要使用num参数的组件}

);
}
复制代码
这时候,修改state,只是对于DemoComponent这个组件内部进行render,对于外部传入的Child组件,将不会重复渲染。
总结
上面这些都是我平时开发当中真实碰到过的问题,相信也是所有React开发者都会碰到的问题,涉及到的技术不深,希望给一些新入坑React的同学有所帮助。

文章来自:https://juejin.im/post/6889825025638006797

评论 (0)