React性能优化小贴士

日常在完成业务功能开发后,不知道你是否还会精益求精,作一些性能优化方面的工做呢?React框架中有一些性能优化相关的注意事项,若是日常不怎么关注的话,可能就很容易忽略掉。接下来的这篇文章,将围绕工做中会用到的几种性能优化的相关经验进行介绍。javascript

Key

在渲染列表结构数据的时候,使用key能够说已经成为React开发中的最佳实践了。那么你知道为何咱们要使用key吗?缘由是使用key可以让组件保持结构的稳定性。咱们都知道React以其DOM Diff算法而著名,在实际比对节点更新的过程当中带有惟一性的key可以让React更快得定位到变动的节点,从而能够作到最小化更新。html

在实际使用过程当中,不少人经常图方便会直接使用数组的下标(index)做为key,这是很危险的。由于常常会对数组数据进行增删,容易致使下标值不稳定。因此在开发过程当中,应该尽可能避免这种状况发生。前端

下面以商品列表组件为例,演示一下key的使用:java

class ShopMenu extends React.Component {
    render() {
        return (
            <ul> { this.props.shopItems.map((shopItem) => <ShopItem key={shopItem.id} itemName={shopItem.name}></ShopItem>) } </ul>
        )
    }
}
复制代码

数据比对

做为一款优秀的前端框架,React自己已经为咱们作了不少工做。不过在开发过程当中,若是咱们能让组件避免在非必要的状况下从新渲染,就能使开发出的组件性能更良好。react

浅比较 shadowEqual

组件在更新过程当中,数据比对这一过程是必不可少的,它是触发组件从新渲染的关键。所以,咱们有必要深刻理解React组件在更新过程当中的数据变化机制。React对于状态更新的比较方式默认都是采用浅比较,咱们能够看一下它的源码实现git

/** * Performs equality by iterating through keys on an object and returning false * when any key has values which are not strictly equal between the arguments. * Returns true when the values of all keys are strictly equal. */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  if (is(objA, objB)) {
    return true;
  }

  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}
复制代码

另外,对于对象作相等比对的is方法,不一样于直接使用=====,它针对特殊的+0-0,NaNNaN的比对作了修复,而且不会作隐式转换。它的实现是像这样的:github

/** * inlined Object.is polyfill to avoid requiring consumers ship their own * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is */
function is(x: any, y: any) {
  return (
    (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y)
  );
}
复制代码

能够从上面的代码中看到,对于引用对象来讲,浅比较算法首先会使用Object.keys获取对象全部的属性,并比对对应的属性值。不过这里只会比对第一层的数据,并无作递归对比。这大概就是叫作"浅比较"的缘由吧。web

shouldComponentUpdate

对于Class组件来讲,咱们可使用shouldComponentUpdate方法来判断是否进行组件渲染,从而更好地提升页面性能。这个方式会在每次props和state变化的时候执行,框架对于这个方法的默认实现是直接返回true,即每次只要属性和状态变动,组件都会从新渲染。而若是咱们对于数据的变动逻辑比较清楚,彻底能够手动实现比对过程来避免重复渲染:算法

class ShopItem extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        return this.props.itemName !== nextProps.itemName;
    }
    
    render() {
      return (<div>{this.props.itemName}</div>);
    }
}
复制代码

pureComponent

要达到性能优化的目的,有时候也没必要手动实现shouldComponentUpdate。你只要让你的组件继承自React.PureComponent便可,它已经内置了浅比较算法,因此上面的例子能够改写成:chrome

class ShopItem extends React.PureComponent {
    render() {
        return (<div>{this.props.itemName}</div>);
    }
}
复制代码

关于箭头函数

还有一点要记住的是,在使用箭头函数的时候要当心:

class Button extends React.Component {
	render() {
		return <button onClick={() => {console.log('hello, scq000');}}>click</button>
	}
}
复制代码

直接在组件上绑定箭头函数虽然写法简便,但因为每次渲染的时候都会从新生成该函数,会致使性能损耗。即便组件的其余props或state没有变动,因为使用了内联的箭头函数也会触发从新渲染。

因此,为了不这种状况的发生,咱们能够先声明好事件监听函数后,而后再拿到其引用传给组件:

class Button extends React.Component {
	handleClick = () => {
		console.log('hello, scq000');
	}
	
	render() {
		return <button onClick={this.handleClick}>click</button>
	}
}
复制代码

useCallback

若是咱们使用的是函数式组件,React16中的useCallback的hook为咱们提供了一种新思路:

export const Button = (text, alertMsg) => {
	const handleClick = useCallback(() => {
    	// do something with alertMsg
    }, [alertMsg]);
	return (
		<button onClick={handleClick}>{text}</button>
	);
}
复制代码

将箭头函数传入useCallback方法中,这是一个高阶函数,它会返回一个记忆化(memoized)的方法。这个方法只有当它所依赖的props或state变化的时候才会更新。在上面的例子中,当它的依赖状态alertMsg变化的时候,handleClick函数才会更新。

在React16中,你可能还会用到useEffect这个Hook来处理一些反作用,就像这样:

const Student = ({name, age}) => {
	useEffect(() => {
		doSomethingWithInfos(infos)
	}, [name, age]);
	
	return (
		<div>This is a child component.</div>
	);
}

const Person = () => {
	return (<Student name="scq000" age="11" />) } 复制代码

useEffect传入的第二个参数也是它的依赖项,若是这个依赖项中使用的是一个箭头函数,那么每次useEffect中的回调函数都会执行。这样一来结果可能就不是咱们想要的了,此时也能够借助useCallback来避免这种状况的发生。

useCallback虽然可以缓存函数,但对于大多数场景来讲使用它反而会增长垃圾回收和运行封装函数的时间。只有对于大计算量的函数来讲,利用useCallback才能起到良好的优化效果。

useMemo

除了直接缓存函数,有时候还须要缓存数据和计算结果。实现记忆化的关键是记住上一次的状态值和输出值。咱们利用闭包就能实现一个简化的Memorize方法:

function memorize(func) {
  let lastInput = null;
  let lastOuput = null;
  return function() {
  	// 这里使用浅比较来判断参数是否一致
    if (!shallowEqual(lastInput, arguments)) {
      lastOuput = func.apply(null, arguments);
    }
    lastInput = arguments;
    return lastOuput;
  }
}
复制代码

在React中,useMemo hook已经为咱们实现了这个功能,直接使用就能够了:

const calcResult = React.useMemo(() => expensiveCalulate(a, b), [a, b]);
复制代码

当输入参数a,b没有发生变化的时候,会自动使用上一次的值。这也意味着咱们使用useMemo只能用来缓存纯函数的计算结果。对于大计算量的操做来讲,能够有效避免重复计算过程。

React.Memo

针对Functional组件来讲,因为缺乏shouldComponentUpdate方法,能够考虑用React.Memo来优化组件性能:React.Memo是一个高阶组件,它内置了useMemo方法来缓存整个组件。

考虑下面这段代码:

function Demo() {
	return (
		<Parent props={props}>
			<Child title={title} subtitle={subtitle} />
		</Parent>
	);
}
复制代码

父组件因为props中的属性变动从新渲染,即便子组件props没有变化,子组件Child也会跟着从新渲染。这时候,能够考虑使用React.Memo来缓存子组件:

export function Card({title, subtitle}) {
	// do some render logic
}
export const MemoziedCard = React.Memo(Card);
复制代码

为了更深刻地理解这部分逻辑,让咱们看一下相关的源码:

if (updateExpirationTime < renderExpirationTime) {
    // This will be the props with resolved defaultProps,
    // unlike current.memoizedProps which will be the unresolved ones.
    const prevProps = currentChild.memoizedProps;
    // Default to shallow comparison
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }
复制代码

咱们能够看到React.Memo默认状况下也是使用的浅比较算法,因此对于复杂的数据,咱们须要本身实现数据比对逻辑。能够在React.Memo传入第二个参数,就像下面这样:

const compartor = (prevProps, nextProps) => {
	return prevProps.id === nextProps.id;
}

React.Memo(Card, compartor)
复制代码

不可变数据Immutable

Immutable是Facebook封装好的抽象数据结构,因为其结构的不变性和共享性,能让引用对象在比对的时候更加快速。使用Immutable建立的数据不可变动,所以数据在整个应用中都易于追踪。这也符合函数式编程的思想。它的核心是采用持久化的数据结构,当改变数据的时候,只会更新变动的那一部分,而数据结构中的不变部分都会公用同一引用,达到结构共享的目的。因此,在高度嵌套的数据进行深拷贝的时候,性能也会更优。

438px-Purely_functional_tree_after.png

import Immutable from 'immutable';

var obj = Immutable.fromJS({1: "one"});
var map = Immutable.Map({a: 1, b: 2, c: 3});
map.set('b', 4);
var list = Immutable.List.of(1,2,3);
list.push(5);
复制代码

虽然Immutable JS在性能上有它的优点,但请注意使用的影响面。不要让原生对象和Immutable对象进行混用,这样反而会致使性能降低,由于将Immutable数据再转换成原生JS对象在性能上是不好的。关于使用Immutable JS的最佳实践,能够参考这篇文章

reselect

在使用Redux过程当中,组件的状态数据一般是从state派生出来的,要作不少计算的逻辑。 假设如今我应用中的状态树是这样的:

const state = {
  a: {
    b: {
      c: 'c',
      d: 'd'
    }
  }
};
复制代码

每次a.b.c更新的时候,即便d没有更新,全部引用到a.b.d的地方也会从新计算。

那么,咱们在这一步要优化的点,一样也是使用缓存或记忆化。reselect就是为了这个目的而生的,它能够帮助咱们避免重复的计算:

import {createSelector} from "reselect";

const shopItemSelector = (state) => state.shopItems;
const parentSelector = (state) => state.parent;

export const shopMenuSelector = createSelector(
	[shopItemSelector, parentSelector],
	(shopItems, parent) => {
      // do something with shopItems and parent
	}
);
复制代码

只有状态shopItemsparent变化后,才会从新计算。

默认状况下,新旧属性的比对也是采用浅比较来进行的。结合上一小节介绍的Immutable,咱们能够进一步优化比对过程。

首先是将咱们的整个state树改用Immutable数据结构:

const state = Immutable.fromJS(originState);
复制代码

接着,改写派生状态的时候,使用Immutable中的is进行比对:

import {createSelectorCreator, defaultMemoize} from 'reselect';
import { is } from 'immutable';

const createImmutableSelector = createSelector(defaultMemoize, is);

export const shopMenuSelector = createImmutableSelector(
	[shopItemSelector, parentSelector],
	(shopItems, parent) => {
      // do something with shopItems and parent
	}
);
复制代码

按需加载

上面介绍的优化方式主要都是围绕组件渲染机制来展开的,而接下来要介绍的方法是依靠延迟计算思想来优化应用响应性能。虽然并不能达到减小总渲染时间的目的,但能够更快地让用户跟页面进行交互,从而提升应用的用户体验。

在React 16以前,咱们通常要实现懒加载可使用react-loadable等库,但如今能够直接使用React.lazy方法就能够了。本质上它也是经过代码拆分的方式,让部分非核心的组件延迟加载。要使用React.lazy还须要配合Suspense组件一块儿。Suspense组件能够为懒加载组件提供基本的过渡效果,一般状况下是提供一个loading动画:

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

function MyComponent() {
  return (
    <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent /> </Suspense> </div>
  );
}
复制代码

不过这一策略,目前只支持浏览器端。至于使用了SSR的React应用,能够考虑https://github.com/smooth-code/loadable-components来达到相同的目的。

测试性能

著名管理学大师彼得.德鲁克(Peter Drucker)曾说过"If you can't measure it, you can't improve it."。虽然这句话是说管理学中的事情,但放在软件开发中也是一样适用的。在考虑优化React页面性能以前,咱们必需要作好对应的测试工做,找到性能瓶颈。使用React DevTools Profiler能够检测组件渲染性能,这个工具能够在谷歌商店下载到。 [图片上传失败...(image-e0f95d-1564019981226)] 更具体的使用方式能够参考reactjs.org/blog/2018/0…

总结

性能优化永远是软件开发中的痛点和难点,要学习和实践的知识有不少,只能说任重而道远。不过在工做中也并不提倡过早优化。性能虽然是重要的评判标准,但在开发过程当中还必须在代码的可维护性、对将来的适应性等方面作出取舍。应用中并不是全部的部分都必须快如闪电,有些部分的可维护性每每更加剧要。

若是必定要作性能优化,核心仍是在减小频繁计算和渲染上,在实现策略上主要有三种方式:利用key维持组件结构稳定性、优化数据比对过程和按需加载。其中优化数据比对过程能够根据具体使用的场景,分别使用缓存数据或组件、改用Immutable不可变数据等方式进行。最后,也必定记得要采用测试工具进行先后性能对比,来保障优化工做的有效性。

参考文章

www.ayqy.net/blog/react-…

zhuanlan.zhihu.com/p/56975681

codeburst.io/memorized-f…

blog.bitsrc.io/lazy-loadin…

kentcdodds.com/blog/usemem…

reactjs.org/docs/optimi…

reactjs.org/blog/2018/0…

redux.js.org/recipes/usi…

——转载请注明出处———

微信扫描二维码,关注个人公众号
最后,欢迎你们关注个人公众号,一块儿学习交流。
相关文章
相关标签/搜索