g770728y / valor-blog

下里巴人的个人博客
4 stars 1 forks source link

[ react 性能 ] 性能优化随笔 #18

Open g770728y opened 4 years ago

g770728y commented 4 years ago

数组型组件中, 单个itemisSelected 在何处计算

对这种高频问题, 应当形成肌肉记忆, 不要每次都要花几分钟找问题

问题

经常遇到如下代码:

const ArrComp = (items) => items.map(item => <ItemComp item={item}/>)

const ItemComp = (item) => {
    const {selection} = useContext(...);
    const isSelected = selection.includes(item.id);
    return <div style={{color:  isSelected?'red':'black'}}>content</div>
}

每当store发生变化, selection 都要重新计算, 从而引起每个item渲染

解决

很明显, 将isSelected上移:

const ArrComp = (items) => {
    const {selection} = useContext(...);
    return items.map(item => <ItemComp item={item} isSelected={selection.includes(item.id)}/>)
}

const ItemComp = (item, isSelected) => {
...
}

优化: 在ui组合上, 维护一个全局的SelectionWidget

回想之前做过的项目, selection有什么用处? 最大用处就是显示一个SelectionWidget, 可用于:

老做法: 在每个widget外, 套一个SelectionWidget

存在问题:

推荐做法: 全局SelectionWidget

g770728y commented 4 years ago

context, context, context

问题

mobx的写法

个人的惯用写法: 定义时:

const storeRef = useRef(new Store())
return <StoreContext.Provider value={storeRef.current}>...</StoreContext.Provider>

使用时:

const store = useContext(StoreContext);
return <div>{store.x}</div>

关键点:

  1. store的引用始终不变 , 也就是: useContext始终不会引起rerender!!!
  2. 组件的render靠 proxy

react.context的写法

定义:

const [state, setState] = useState<Context>({});
const handleClick=() => {setState(...) } 

return <Context.Provider value={state}>
...
</Context.Provider>

使用:

const store = useContext(Context);
...

这里的主要问题是: 一旦context发生了变化, 那么: 所有使用useContext的组件, 都会rerender!!!

这是机制使然:

  1. render只能靠useContext驱动
  2. 没有proxy驱动renderer
g770728y commented 4 years ago

优先编译到ES6

某些场景性能差10倍!

典型场景:

const a = 'aaa';
const x = {...x, [a]: 1} 

如果 编译成es5, 将编译出_objectSpread, 并使用defineProperties 方法, 性能非常低 如果 直接使用上述语法, 性能与Object.assign({}, x, [a]:1}基本相同, 都比前者快10倍 数组也尽量使用xs.concat(ys); 所以,如果 浏览器兼容性足够, 尽可能使用es6

浏览器兼容性

Chrome:51 版起便可以支持 97% 的 ES6 新特性。 Firefox:53 版起便可以支持 97% 的 ES6 新特性。 Safari:10 版起便可以支持 99% 的 ES6 新特性。 IE:Edge 15可以支持 96% 的 ES6 新特性。Edge 14 可以支持 93% 的 ES6 新特性。(IE7~11 基本不支持 ES6) X5微信浏览器需要关注, 例如并不支持Array.includes

g770728y commented 4 years ago

connect高阶组件的使用, 会增大计算工作量

当然, 使用connect, 最大的好处是: 清晰, 自然, 每个组件只需关心自己的数据 所以, 在完成功能期间, 尽量大量使用connect, 不要考虑性能问题

connect原理说起

四步:

  1. store.state发生改变时
  2. 会自动计算每个connect组件的mapProps
  3. 与oldProps进行比较
  4. 若第三步不相等, 则render 试想, 一个excel应用,有5000+单元格, 每个单元格都connectstore.state 当state发生改变, 则: 第2步: 计算5000个单元格的mapProps 第3步: 将这5000个newProps与oldProps进行比较 即便render不会发生, 这10000次运算也少不了, 基本不可能达到60FPS了 这也就是为什么明明没有发生渲染, react的性能面板也会显示大量细线的原因

如何解决

  1. 合理进行组件分层, 象上面的情形, 可以分成RowGroup -> Row -> Cell三级
  2. 所有属性全部从上向下传递, 不使用connect 这样一来: state发生变更, 只需要考虑RowGroup的属性是否有变动, 若无变动, 则完全不刷新 若有变动, 则检查Row, 若无变化, 则该row不刷新 若有变动, 则检查Cell 假设每行50个单元格, 共100行, 5000个单元格 修改一个单元格: RowGroup计算1次, 渲染1次 Row计算100次, 渲染1行, Cell计算50次, 渲染1个单元格 共计算551次, 这里的计算, 大部分可以ShallowEqual, 性能很高 基本上只需要耗费1/10的时间 甚至, 可以将RowGroup与Row之间, 再引入SubGroup, 进一步减少计算

附: connect可能存在的bug

  1. A组件下有A1组件, 若二者都connect, 则可能造成A1重复渲染
  2. A组件下有A1组件, 若A1先于A渲染, 则可能A下传的属性是老属性, 从而发生错误
g770728y commented 4 years ago

reflow问题

在开发树形excel组件时, 发现一个问题: 无论优化得如何好, 在滚动页面时, 或点击单元格时, 总是要卡2秒 可以肯定别的优化都很好, 没有什么优化空间, 那么这个2秒是从哪里来的呢?

在chrome的性能监控里, 发现时间完全耗费在render上, 具体是Update Render Tree 查资料, 发现其实是在重新计算布局

因为整个组件在滚动时, 并未发生引起reflow的事件, 但确实reflow了 是什么原因呢?

一番折腾, 发现: 在容器节点上, 增加position:relative后, 可以实现流畅滚动 没google到相关原因, 只能怀疑是css bug 这个问题后面可以深挖

<div className={styles['container']}>
      <div className={styles['header']}>{header}</div>
      <div style={{ overflow: 'auto', height: '100%' }}>
        <div
          style={{
            display: 'flex',
            justifyContent: 'center',
            position: 'relative' ,             <==== 关键!!!!
            height: '100%',
          }}
        >
          <div>
            {editor}
            <div style={{ height: 100, width: 1 }} />
          </div>
        </div>
      </div>
    </div>
g770728y commented 4 years ago

大数据量慎用reduce

对于数据量过200以上的, 慎用reduce!!! ( 当然也可前期全部reduce, 优化时再处理 )


直接上数据说话:

  idArray: T[],
  idField: string = "id"
): { [id: string]: T } {
  console.log("测试记录条数:", idArray.length);
  console.time("idMap");
  const result1 = idArray.reduce(
    (acc, obj) => ({ ...acc, [(obj as any)[idField]]: obj }),
    {}
  );
  console.timeEnd("idMap");

  console.time("idMap2");
  const result2 = idArray.reduce((acc, obj) => {
    const _acc = Object.assign({}, acc);
    const idName = (obj as any)[idField];
    (_acc as any)[idName] = obj;
    return _acc;
  }, {});
  console.timeEnd("idMap2");

  console.time("idMap3");
  let result = {} as any;
  for (let i = 0; i < idArray.length; i++) {
    const obj = idArray[i];
    const idName = (obj as any)[idField];
    result[idName] = obj;
  }
  console.timeEnd("idMap3");

  console.time("idMap4");
  let result4 = {} as any;
  idArray.forEach(obj => {
    const idName = (obj as any)[idField];
    result4[idName] = obj;
  });

  console.timeEnd("idMap4");

  console.time("idMap5");
  const result5 = R.reduce(
    (acc, obj) => ({ ...acc, [(obj as any)[idField]]: obj }),
    {},
    idArray
  );
  console.timeEnd("idMap5");
  console.log("result5", result5);
  return result;
}

结果: 出乎意料: reduce与普通forEach循环慢了数百倍:

测试记录条数: 3367
reduce+解构语法: idMap: 2620.43115234375ms
reduce+Object.assign idMap2: 2568.407958984375ms
for循环 idMap3: 0.81005859375ms
forEach循环 idMap4: 0.775146484375ms
Rambda的R.reduce idMap5: 2626.396240234375ms

区区3367条语句, reduce执行了整整2.6秒!!! 而forEach不到1ms

g770728y commented 4 years ago

对象value修改,使用R.map . Array.filter 使用R.filter

这一点真的跟直觉相反. rambda的方法居然快得多 对于object的修改, 使用 Object.keys(obj).forEach, 比R.map慢40倍! 同样, 直接使用Array.filter, 也比 R.filter慢80倍! 对比图