React项目性能优化

react

1. 使用生产版本和Fragment

1. 生产版本

确保发布的代码是生产模式下(压缩)打包的代码。

一般运行npm run build命令。

直接从webpack看配置文件,需要设置mode = 'production'。 调用teaser-webpack-plugin

React Devtools可以根据地址栏右侧图标颜色判断是否是生产模式。

2. Fragment

减少不必要节点的生成。也可以使用空标签(<></>)

2. 类组件使用PureComponent

减少不必要的重复渲染!

PureComponent的原理是重新设置了shouldComponentUpdate(nextProps, nextState)方法。

在该方法中将nextProps和this.props对象浅比较,将nextState和this.state对象进行浅比较

PureComponent模拟代码实现:

import React from 'react';

function shallowCompare(obj1, obj2) {

if(obj1 === obj2) {

return true;

}

if(typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {

return false;

}

const obj1Keys = Object.keys(obj1);

const obj2Keys = Object.keys(obj2);

if (obj1Keys.length !== obj2Keys.length) {

return false;

}

for(let key of obj1Keys) {

// 如果obj1[key]和obj2[key]为对象;浅比较有些情况会失效

if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {

return false;

}

}

return true

}

export default class PureComponent extends React.Component {

shouldComponentUpdate(nextProps, nextState) {

return !(shallowCompare(nextProps, this.props) && shallowCompare(nextState, this.state))

}

}

测试用例:

/*******父组件********/

class App extends React.Component{

constructor(props) {

super(props);

this.state = {

number: 1

}

this.ref = React.createRef();

}

add = () => {

// this.state.number通过prop传递给Counter

// Counter通过PureComponnet进行浅比较

// Number(this.ref.current.value)为0时,Counter不刷新

this.setState(state => ({

number: state.number + Number(this.ref.current.value)

}))

}

render() {

console.log('App Render');

return (

<div>

<Counter number={this.state.number} />

<input ref={this.ref} />

<button onClick={this.add}>+</button>

</div>

)

}

}

/*********子组件***********/

import React from 'react';

import PureComponent from './pureComponent';

export default class Counter extends PureComponent {

constructor(props) {

super(props);

this.state = {

count: 1

}

}

// 证明PureComponent会对this.state和nextState进行浅比较

add = () => {

// 不会导致render

this.setState((state) => ({

count: state.count + 0 // 0不会刷新,其他值会刷新

}))

}

render() {

console.log('Counter Render');

return (

<div>

<div>

Counter:{this.state.count}

</div>

<button onClick={this.add}>+</button>

<div>

{this.props.number}

</div>

</div>

)

}

};

3. 不可变数据-immutable

上面的PureComponent只是对简单数据(值为原始类型)进行浅比较;

而实际开发应用中,数据都是多层嵌套的复杂数据;只使用PureComponent可能会导致render失效。

示例(错误示例-数组):

  state= {words: []}; 

add = () => {

// 将一个对象赋值给另一个变量;两者对应同一个地址

let newWords = this.state.words;

// push,pop,shift,splice方法都不会改变原址;

newWords.push(this.ref.current.value);

// 如果在子组件使用了PureComponent浅比较words的值,nextProps.words === this.props.words

// 所以不会引起子组件Counter的render方法调用

this.setState({

words: newWords

});

}

上面的示例可以通过不可变数据修复BUG。

不可变数据: 保持原数据不变,生成一个新的对象进行赋值

示例1:

  add = () => {

// concat方法调用后,返回一个新数组

this.setState(state => ({

words: state.words.concat(this.ref.current.value)

}))

}

示例2:

  add = () => {

// []符号本身就是new Array()的简写形式

this.setState(state => ({

words: [...state.words, this.ref.current.value]

}))

}

上面的示例都是复合数据是数组的情形;对于数据是对象的示例如下:

错误示例(对象):

class App extends React.PureComponent{

constructor(props) {

super(props);

this.state = {

words: {}

}

this.ref = React.createRef();

}

add = () => {

// Object.assign()被修改对象在第一个参数时,直接在原state.words对象地址上修改;

// 对于PureComponent来说,nextState和this.state相同,永远不会render

this.setState(state => ({

words: Object.assign(state.words, {a:this.ref.current.value})

}))

}

render() {

console.log('App Render');

return (

<div>

<input ref={this.ref} />

<button onClick={this.add}>+</button>

</div>

)

}

}

使用不可变数据,生成新对象的方法修改;

示例1: 

  add = () => {

// Object.assign()第一个参数是空对象,表明生成一个新的对象

this.setState(state => ({

words: Object.assign({}, state.words, {a:this.ref.current.value})

}))

}

示例2:

  add = () => {

// {}本身是new Object的简写形式

this.setState(state => ({

words: {...state.words, a: this.ref.current.value}

}))

}

上面的方法虽然解决了使用PureComponent组件时,对象导致的不刷新问题;

但是,会出现,只要是调用setState就重新的render的情况,尽管对应的值其实是一样的。

因为对于js引擎来说,任何两个对象都不相同。而且,嵌套层次特别深时,书写也复杂。

immutable库可以解决这个!

immutable

immutable不仅可以生成新的对象;还可以对对象进行深层次的值比较。

import { Map, is } from 'immutable';

class App extends React.Component{

constructor(props) {

super(props);

this.state = {

// is方法判断的时候,深层比较只能比较从immutable对象变化而来的对象

// 因为这里考虑words会进行深层变化,要追踪变化,需要将其变成immutable对象

words: Map({}),

number: 1

}

this.ref = React.createRef();

}

shouldComponentUpdate(nextProps, nextState) {

// is方法用于比较两个immutable对象是否值相等

return !(is(Map(nextState), Map(this.state)) && is(Map(nextProps), Map(this.props)))

}

add = () => {

// set方法给immutable对象赋值;

// 另可以通过get获取immutable对象的值

// this.state.words.get('a')

this.setState(state => ({

words: state.words.set('a', this.ref.current.value)

}))

}

reset = () => {

this.setState(state => ({

number: state.number + 1

}))

}

render() {

console.log('App Render');

return (

<div>

<input ref={this.ref} />

<button onClick={this.add}>+</button><br/>

<button onClick={this.reset}>reset</button>

</div>

)

}

}

4. 函数组件使用React.memo避免重复渲染

React.memo()本质上是将函数组件转为继承React.PureComponent的类组件的高阶组件。

稍有不同的是,只比较props的值。

代码实现如下:

function memo(WrappedComponent) {

return class extends React.PureComponent {

render() {

return (

<WrappedComponent {...this.props} />

)

}

}

}

对于深层比较,还可以接受第二个参数

function MyComponent(props) {

/* 使用 props 渲染 */

}

function areEqual(prevProps, nextProps) {

/*

如果把 nextProps 传入 render 方法的返回结果与

将 prevProps 传入 render 方法的返回结果一致则返回 true,

否则返回 false

*/

}

export default React.memo(MyComponent, areEqual);

5. React.lazy()和Suspense进行懒加载

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./routes/Home'));

const About = lazy(() => import('./routes/About'));

const App = () => (

<Router>

<Suspense fallback={<div>Loading...</div>}>

<Switch>

<Route exact path="/" component={Home}/>

<Route path="/about" component={About}/>

</Switch>

</Suspense>

</Router>

);

其中:React.lazy()中import只支持默认导出的组件。

其外要包含在Suspense组件中。

6. 异常捕获边界(Error Boundaries)

捕获发生异常的React组件。将异常组件和正常组件分割开。

提高用户的体验性能。

import MyErrorBoundary from './MyErrorBoundary';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (

<div>

<MyErrorBoundary>

<Suspense fallback={<div>Loading...</div>}>

<section>

<OtherComponent />

<AnotherComponent />

</section>

</Suspense>

</MyErrorBoundary>

</div>

);

MyErrorBoundary代码:

class MyErrorBoundary extends React.Component {

constructor(props) {

super(props);

this.state = { hasError: false };

}

static getDerivedStateFromError(error) {

// 更新 state 使下一次渲染能够显示降级后的 UI

return { hasError: true };

}

componentDidCatch(error, errorInfo) {

// 你同样可以将错误日志上报给服务器

logErrorToMyService(error, errorInfo);

}

render() {

if (this.state.hasError) {

// 你可以自定义降级后的 UI 并渲染

return <h1>Something went wrong.</h1>;

}

return this.props.children;

}

}

7. 骨架屏

骨架屏插件以react-content-loader(svg图片生层)为基础。

用于在页面初始加载时,避免出现白屏现象。

代码参考

8.长列表优化

虚拟化长列表:只加载可视范围内的数据。

当网站需要加载大批量数据(成千上万)时,会加载特别慢。

这个时候我们可以使用“虚拟滚动”技术(react-window或者react-virtualized),只渲染当前屏幕范围内的数据。

鼠标滚动去触发事件,再渲染一屏。

react-window用法示例:

import { FixedSizeList as List } from 'react-window';

const Row = ({index, style}) => (

<div style={{...style, backgroundColor: getRandomColor()}}>

Row{index}

</div>

)

function Container(props) {

return (

<List

height={200}

itemCount={100}

itemSize={30}

width={'100%'}

>

{Row}

</List>

)

}

function getRandomColor() {

const color = Math.floor((Math.random()*0xFFFFFF)).toString(16)

if (color.length === 6) {

return '#' + color

}

return getRandomColor();

}

ReactDOM.render(<Container />, window.root)

原理是监听鼠标的滚动事件。

实现react-window的FixedSizeList的源码如下:

import React from 'react';

export class FixedSizeList extends React.Component {

constructor(props) {

super(props);

this.ref = React.createRef();

this.state = {

start: 0

}

}

componentDidMount() {

this.ref.current.addEventListener('scroll', () => {

// 注意: 监听函数内部有this.ref,需要使用箭头函数

let scrollTop = this.ref.current.scrollTop;

this.setState({

start: Math.floor(scrollTop/this.props.itemSize)

})

})

}

render() {

const { itemSize, itemCount, height, width, } = this.props;

const containerStyle = {

height, width, position: 'relative', overflow: 'auto'

}

let children = [];

const itemStyle = {

height: itemSize,

width: '100%',

position: 'absolute',

top: 0,

left: 0

}

const { start } = this.state;

for(let i=start; i< start + Math.ceil(height/itemSize); i++) {

children.push(this.props.children({index: i, style: {...itemStyle, top: i*30}}))

}

return (

<div style={containerStyle} ref={this.ref}>

<div style={{width: '100%', height: itemSize*itemCount}}>

{children}

</div>

</div>

)

}

}

9. 根据性能优化工具修改代码

1. 使用Chrome的Performance工具

2. React Devtools的Profiler工具分析;

   通过React.Profiler组件包裹需要分析渲染时间的组件(不适合生产环境)。

10. SEO优化-预渲染

使用prerender-spa-plugin的puppeteer(无头浏览器)功能。

11. 图片懒加载

使用react-lazyload插件

12. key优化

以上是 React项目性能优化 的全部内容, 来源链接: utcz.com/z/383495.html

回到顶部