React优化点滴

谈到React优化,估计说的最多的就是减少子组件渲染,减少真实DOM操作等。

一  减少渲染

1. shouldComponentUpdate

通过对Props和State的浅比较,如果没有变化,return false,避免重复多余的render方法调用,省去虚拟DOM的生成和对比过程,提高性能。

早期类似的技术有pureRender,16版本中可以直接让class组件继承PureComponent,它的实现其实很简单,只是做浅比较,因为过深的比较也会消耗很多的时间,或许还比render方法带来的消耗大,所以这其实是权衡和取舍。

 注意这里的浅比较,基础类型比较值是否相等,不等则需要再次渲染;对于引用类型,先比较引用的地址,地址相同,不渲染,地址不同,判断两个对象的keys的长度,长度不等,则需要渲染,相等则对第一层属性比较,如果有不同,则渲染,否则不渲染。下面的code则是核心主要逻辑。

注意object.is(ES6推出的)和===的主要区别,是object.is(+0,-0) === false, Object.is(NaN,Nan) === false

function is(x, y) { //处理了基本类型的比较
  //1,针对+0===-0的情况
  //2. 针对NaN!==NanN的情况
  return x === y && (x !== 0 || 1 / x === 1 / y) || x !== x && y !== y 
  ;
}
var is$1 = typeof Object.is === 'function' ? Object.is : is;
var hasOwnProperty$2 = Object.prototype.hasOwnProperty;

//返回值:false更新  true不更新
function shallowEqual(objA, objB) {
  if (is$1(objA, objB)) {//基本数据类型 不更新
    return true;
  }
 //由于Object.is 可以对基本数据类型做一个精确的比较 如果不等只有一种情况 那就是object, objA/objB中,只要有一个不是object或为null则返回false
  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }
  //过滤掉基本数据类型 就是对象比较 首先比较长度 优化性能
 //比较oldProps和新的Props以及oldState和newState长度是否相同 如果长度不同则重新更新渲染 如果长度相同则不用重新渲染 如果不相等 不会往下执行 优化性能
  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) {
    return false;  
  }
   //如果key相等
   //如果objA的属性不在objB里,或者是objA的属性值和objB的属性值不等 则重新渲染 不考虑当keysA[i]为对象的多层问题 浅显比较 提高性能
   for (var i = 0; i < keysA.length; i++) { 
       if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  return true;
 }
}

2. React.memo()

由于PureComponent是只能用于class组件的继承,react为了支持函数组件,所以又推出了一个高级组件React.memo(), 当props变化时做浅比较。注意如果用useState或者useContext等hooks导致state状态变化时,函数组件依然会重新渲染,否则复用上一次的渲染结果(复用上一次生成的虚拟子节点Dom)。

const Child = memo(props => {
  useEffect(() => {
    console.log("schema", props.schema);
  }, [JSON.stringify(props.schema)]);

  return <div>Child</div>;
});

//another case
const ChildMedo = React.memo(() => {
    return <Child1 step={step} count={count}  /> 
});

 3. useMemo

用于函数组件,减少计算一些中间值,废话别太多,Show you code。

export default function useMemoDemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    
    // 使用useMemo的话 当input的值改变了,count没有变化,func 还是拿到之前的缓存的值,如果计算过程很复杂,就节省了性能的再次计算的开销
    const func= useMemo(() => {
       let result = Math.random() * count;
        return result;
    }, [count]);
 
    return <div>
        <h4>{count}-{func}</h4>
        {val}
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}4. 使用<>和React.Fegment


当在组件中返回多个子元素时,可以使用 React.Fragment 或空标签来包裹它们,而不需要额外的 DOM 节点。这可以减少不必要的 DOM 层级

5. 懒加载组件

对于较大或不常用的组件,可以使用 React.lazy 和 Suspense 进行懒加载。这样可以延迟组件的加载时间,提高初始加载速度

6. 使用 React DevTools 进行性能分析

使用 React DevTools 进行性能分析:React DevTools 是一款浏览器扩展工具,可以帮助你分析 React 应用的性能瓶颈。它提供了组件层级、渲染时间和重新渲染次数等有用的信息,帮助你定位和解决性能问题。

7. redux中的优化

对于 Redux 来说,每当 store 发生改变时,所有的 connect 都会重新计算。在一个大型应用中,浪费的重复计算可想而知。为了减少性能浪费, 我们想到对 connect 中的这些 selector函数做缓存。

Redux 拥抱了函数式编程,而在函数式编程中,纯函数的众多好处之一就是方便做缓存。那么,如何用纯函数做缓存呢?在数学上,如果自变量不变,因变量总是不变。同样,用相同的参数执行纯函数多次,每次返回的结果一定相同。也就是说,如果纯函数的参数不变的话,可以把之前用同样的参数计算出来的结果直接返回。 (摘自《深入React技术栈》)

我们可以直接使用reselect库帮我们完成。

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null
let lastResult = null
return (...args) => {
if (
lastArgs !== null &&
lastArgs.length === args.length &&
args.every((value, index) => equalityCheck(value, lastArgs[index]))
) {
return lastResult
}
lastResult = func(...args)
lastArgs = args
return lastResult
}
}

defaultMemoize 函数运用了闭包的原理,使纯函数的参数和结果缓存在内存中。为了让
defaultMemoize 函数中缓存的数据常驻内存,我们需要让 defaultMemoize 处于全局作用域,或者
用其他作用域链连接到全局作用域。

8.高阶reducer分析出哪些reducer或action耗费的时间太长,预警, 这样之后就可以有针对性优化。

export default function logSlowReducers(reducers, thresholdInMs = 8) {
Object.keys(reducers).forEach((name) => {
const reducer = reducers[name];
// 将每个 reducer 用高阶函数包装
reducers[name] = (state, action) => {
const start = Date.now();
const result = originalReducer(state, action);
const diffInMs = Date.now() - start;
if (diffInMs >= thresholdInMs) {
console.warn(`Reducer ${name} took ${diffInMs} ms for ${action.type}`);
}
return result;
};
});
return reducers;
}

9. 特定的action

Redux 中,每个 action 被分发,所有的 reducer 都会被执行一次。虽然每个 reducer 仅仅只
是执行一个 switch 判断,但所有的 reducer 加起来的执行时间也不容小觑。
大多数情况下,应用的 action 都是和某个 reducer 对应。因此,我们可以指定特殊情况,让
Redux 在特殊情况之外只执行与 action 对应的那个 reducer 。

10. 合并多个action

比如有5个同步action,依次触发,每一个action都需要一个很大的redux流程,以及dom树的更新,我们可以通过一些办法合并他们,只产生一个最终的state,最后再一次性更新组件树。

11. 提高编程技巧,避免属性每次都传入一个新对象

比如字面量prop = {a:b},每次都会产生要给新的对象

比如我们经常使用的合成事件的绑定。()=> {}, 或者onchange.bind(this), 最好存储为类的成员属性做缓存。

12. 其他

使用redux标准接入组件方式,connect方法的第4个参数options={pure:true},默认也会对传入属性做浅比较,避免被包装组件重复不必要的渲染。

其实还有很多。

 

二 刻意去迎合diff算法

1.  同级别的元素使用唯一key,减少diff比较,增强复用,比如使用绑定元素的id。

2.  避免(组件结构)节点的跳跃变动,因为diff算法只会同级别比较,一旦发现不同就会大刀阔斧地剪枝或增枝。

3. 同级别节点中,避免因为代码问题,产生太多的移动。

 比如有5个兄弟元素,只有最后一个元素(索引为4)调到第一个位置来,其他节点索引都变大1位,react的diff算法是,索引变小不移动,索引变大需要移动,so,这就会增加出其余4个元素向后移动4次真实DOM操作。

当然react的diff算法的时间复杂度已经从O(n^3)优化到 O(n),从2/8原则的角度来看,绝大多数场景的性能已经很棒了。

posted @ 2020-10-17 23:38  lswtianliang  阅读(137)  评论(0编辑  收藏  举报