昨晚试用了下自己写的 FlashCard. 手机端, 出乎意料地卡顿.每次点 NG 或者 OK, 都有一定概率返回列表页时,直接白屏或者卡住,导致我没法进一步使用.

今天尝试给每个 UI 组件, 都加了一个日志. 发现: UI重绘的频率,要远比自己预想的要大.

page render too much

很诡异.为了减少不必要的重绘, 我还专门给列表加了 “key“:

return <Card key={card.cardId} card={card} cardTitle={cardTitle}></Card>

很明显, 并没有什么用.又深入查了下, 发现: 函数组件, 默认就是: 如果父组件重绘, 自己也会每次都重绘的….即使 props 完全一样.而如果不想重新绘制,就需要额外借助于 memo

memo lets you skip re-rendering a component when its props are unchanged.

代码不多, 索性就都直接改了. 基于已知的信息, 我梳理了几条能尽量减少不必要的UI重绘的经验:

  • 函数式组件, 都用 memo 包裹下. 这是最主要的方式. 这样组件的行为, 更符合我的直觉或者预期.
  • 针对 props 中有 另一个函数作为参数的情况, 需要在父组件,特殊处理下.这时需要用到 useCallback.
  • 在需要时, 将某个需要动态刷新的部分,单独与组件分离–或者说抽成更小的组件, 单独控制刷新逻辑. 我这次页面之所以卡, 其实是 Audio 标签太多引起的.所以针对它单独优化了下:
const QuestionAudio = memo(({ questionMedia }) => {
    console.log("QuestionAudio Render...")
 
    return (
        <div className="pt-2">
            <audio controls autoPlay="">
                <source src={questionMedia} type="audio/mpeg" />
            </audio >
        </div>
    )
})
  • 另外,就是,组件件传值时, 尽量只传递必须的数据, 尽量传递基本数据类型. 比如我原来是在内部计算的问题标题,需要往里面传入所有卡片.现在直接改成在外部计算了:
<div className='bg-gray-500'>
  {
    showReviewingCards.map(card => {
      let cardTitle = `问题 ${reviewingCards.indexOf(card) + 1}/${reviewingCards.length}`
      if(card.reviewToday) {
        cardTitle = cardTitle + " (NG)"
      }
 
      return <Card key={card.cardId} card={card} cardTitle={cardTitle}></Card>
    })
  }
</div>

问题本身, 倒是算不上太复杂. 但是刚开始的方向,有点不太对. 刚开始以为是 Redux 状态管理, 哪里用的不对, 导致UI频繁刷新.后来才发现, 即使输入完全一致, 函数组件还是会全部重绘. 感觉怪怪的.

无论怎样, 问题算是解决了.既然不是 Redux 的问题, 就继续用着. 流行,总是有原因的; 在自己不熟悉的领域, 适当 “随大流”, 坑应该总会少一点吧…