乐趣区

react的一些性能优化

react 性能优化

shouldComponentUpdate

对于对视图没有关联,但可能更新的数据,可以用 shouldComponentUpdate 过滤掉,减少无所谓的更新

返回一个 Boolean 值,true则更新,false则不更新,默认为true

 shouldComponentUpdate(nextProps,nextState){if(this.props.xxx !== nextProps.xxx){return true;}else {return false;}
 }

PureComponent

对组件的 stateprops仅浅对比更新,即传入的是对象,单纯的更改对象属性是无法触发更新,需要更改对象的引用(对于非基本类型的对比,对比的是引用的地址块)
可以配合 immutable.js 使用,以达到对象更改属性时更新的效果。

Redux 配合 immutable.js 使用

Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构)。

Immutable.js 了解一下?– 掘金

memo

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但 不适用于 class 组件
如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

原理同 PureComponent,但仅对props 仅做浅对比。
如果要手动添加对比过程,可以在第二个参数传入自定义的对比函数。

function MyComponent(props) {/* 使用 props 渲染 */}
function areEqual(prevProps, nextProps) {
  /* 如果函数返回 true,就会跳过更新。*/
   if(nextProps.xxx !== prevProps.xxx){return false;}else {return true;}
}
export default React.memo(MyComponent, areEqual);

useMemo

useMemo允许你通过「记住」上一次计算结果的方式在多次渲染的之间缓存计算结果。

memo 的区别是 useMemo 作用于计算以及组件。

  • 第一个参数是主体内容
  • 第二个参数是数组,传入要记住的参数

作用与function:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

作用于组件:

function Parent({a, b}) {
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a={a} />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b={b} />, [b]);
  return (
    <>
      {child1}
      {child2}
    </>
  )
}

注意这种方式在循环中是无效的,因为 Hook 调用 不能 被放在循环中。但你可以为列表项抽取一个单独的组件,并在其中调用 useMemo

useCallback

useCallbackuseMemo 有些相似。它接收一个内联函数和一个数组,它返回的是一个记忆化版本的函数。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

lazy & Suspense

React 集成了 lazySuspense
lazy会进行代码分割,它能让你像渲染常规组件一样处理动态引入(的组件)。。
fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

???? 对路由组件进行代码分割、懒加载。

import React, {lazy, Suspense} from "react";
import {renderRoutes} from "react-router-config";
import {HashRouter, Redirect} from "react-router-dom";

const Page1Comp = lazy(() => import("../Page1"));
const Page1 = props => {
  return (<Suspense fallback={<p>loading</p>}>
      <Page1Comp {...props} />
    </Suspense>
  );
};

const Page2Comp = lazy(() => import("../Page2"));
const Page2 = props => {
  return (<Suspense fallback={<p>loading</p>}>
      <Page2Comp {...props} />
    </Suspense>
  );
};

const routes = [{ path: "/", exact: true, render: () => <Redirect to={"/page1"} /> },
  {path: "/page1", component: Page1},
  {path: "/page2", component: Page2}
];

function App() {
  return (
    <HashRouter>
      <div className="App">
        <h1>Hello</h1>
        {renderRoutes(routes)}
      </div>
    </HashRouter>
  );
}

export default App;

Fragment

在 jsx 里面,会有很多无意义但必须有的的嵌套。可以使用 Fragment 代替减少无谓的嵌套。

render() {
  return (
    <React.Fragment>
      <ChildA />
      <ChildB />
      <ChildC />
    </React.Fragment>
  );
}
// or
render() {
  return (
    <>
      <ChildA />
      <ChildB />
      <ChildC />
    </>
  );
}

不适用内联函数

<input type="button" onClick={(e) => {this.setState({inputValue: e.target.value}) }} />

???? 上面的函数创建了内联函数。每次调用 render 函数时都会创建一个函数的新实例,render 函数会将该函数的新实例绑定到该按钮。
改成如下????:

export default class InlineFunctionComponent extends Component {setNewStateData = (event) => {
    this.setState({inputValue: e.target.value})
  }

  render() {return <input type="button" onClick={this.setNewStateData} />
  }
}

函数绑定在 constructor 完成

<input type="button" value="Click" onClick={this.handleButtonClick.bind(this)} />

每次调用 render 函数时都会创建并使用绑定到当前上下文的新函数,但在每次渲染时使用已存在的函数效率更高。优化方案如下:

constructor() {this.handleButtonClick = this.handleButtonClick.bind(this)
}

箭头函数与 constructor 函数绑定

当我们添加箭头函数时,该函数被添加为对象实例,而不是类的原型属性。这意味着如果我们多次复用组件,那么在组件外创建的每个对象中都会有这些函数的多个实例。
每个组件都会有这些函数的一份实例,影响了可复用性。此外因为它是对象属性而不是原型属性,所以这些函数在继承链中不可用。
因此箭头函数确实有其缺点。实现这些函数的最佳方法是在构造函数中绑定函数。

节流和防抖

节流函数

节流是将多次执行变成每隔一段时间执行。

/**
 * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait
 *
 * @param  {function}   func      回调函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始函数的的调用,传入{leading: false}。*                                如果想忽略结尾函数的调用,传入{trailing: false}
 *                                两者不能共存,否则函数不能执行
 * @return {function}             返回客户调用函数
 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 之前的时间戳
    var previous = 0;
    // 如果 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 如果设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 获得当前时间戳
      var now = _.now();
      // 首次进入前者肯定为 true
      // 如果需要第一次不执行函数
      // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于 0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
       // 如果设置了 trailing,只会进入这个条件
      // 如果没有设置 leading,那么第一次会进入这个条件
      // 还有一点,你可能会觉得开启了定时器那么应该不会进入这个 if 条件了
      // 其实还是会进入的,因为定时器的延时
      // 并不是准确的时间,很可能你设置了 2 秒
      // 但是他需要 2.2 秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 如果存在定时器就清理掉否则会调用二次回调
        if (timeout) {clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
        // 没有的话就开启一个定时器
        // 并且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };

防抖

防抖动是将多次执行变为最后一次执行。

/**
 * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
 *
 * @param  {function} func        回调函数
 * @param  {number}   wait        表示时间窗口的间隔
 * @param  {boolean}  immediate   设置为 ture 时,是否立即调用函数
 * @return {function}             返回客户调用函数
 */
export function debounce(func, wait = 200, immediate = false) {
    let timer, context, args

    // 延迟执行函数
    const later = () => setTimeout(() => {
        // 延迟函数执行完毕,清空缓存的定时器序号
        timer = null
        // 延迟执行的情况下,函数会在延迟函数中执行
        // 使用到之前缓存的参数和上下文
        if (!immediate) {func.apply(context, args)
            context = args = null
        }
    }, wait)

    // 这里返回的函数是每次实际调用的函数
    return function (...params) {
        // 如果没有创建延迟执行函数(later),就创建一个
        if (!timer) {timer = later()
            // 如果是立即执行,调用函数
            // 否则缓存参数和调用上下文
            if (immediate) {func.apply(this, params)
            } else {
                context = this
                args = params
            }
            // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
            // 这样做延迟函数会重新计时
        } else {clearTimeout(timer)
            timer = later()
            context = this
            args = params
        }
    }
}

避免内联样式

用 CSS 动画代替 JavaScript 动画

服务器 gzip 压缩

Web Workers 处理 CPU 密集任务

整理学习自你需要掌握的 21 个 React 性能优化技巧

退出移动版