本文的项目是一个实时聊天系统。
技术栈(项目比较旧,react native、react版本没跟上):
react native 0.48.4
react native web
redux
seamless-immutable
reselect
由于是公司项目,这里就不贴源码了。
滚动长列表
优化前
滚动消息列表时,FlatList在每次update任务中,所有已渲染的CellRenderer都会被重新update。
这个有点不寻常,按道理已经渲染的Cell不应该update,因为消息数据并没有变化。
检查了一下代码,发现原来ListItem的props上有一个onItemLayout属性,这个属性在每次reconciler的过程中都是变化的,这就会导致PureComponent失效,每次都会进行update。
优化后
避免使用闭包,因为它每次都会生成一个新的函数,改为通过public class fields syntax来声明函数。
可以看到,优化后,已渲染过的CellRenderer的颜色条变小了很多,整体的update时间从200多毫秒减少到60多毫秒!
发送消息
优化前
发送消息时也存在同样的问题,FlatList在update时,所有已渲染的CellRenderer也被重新渲染了。
检查了一下代码,发现原来传了一个index属性,导致在发送消息时,每条消息的index都变了,所以ListItem都会被update。
由于这里的FlatList是一个reverted FlatList,所以在prepend消息时,index会整体往后移,所以导致整个列表都重绘了。
优化后
去掉index,依赖index的逻辑改为通过message id去查找计算出来。
可以看到,优化后,只有最新的一条消息会被重新update,因为它依赖了NextMessage和PrevMessage,而其他的消息就不会被update,整体update时间从200多毫秒减少到50多毫秒!
加载历史消息
优化前
消息列表滚到底部后,竟然还有一个将近600毫秒的update任务。
按道理所有消息都已经加载完了,加载历史消息时,如果没有拉到新消息,应该只会更新下图所示的ListHeader:
为什么会出现这么耗时的一个update任务呢?
检查了一下redux log才发现,原来是加载历史消息的逻辑有点问题。
如果本地现在已经有100条消息,当首次触发加载历史消息时,因为offset丢了,那就会一次性拉100 + 10 = 110条消息,然后query成功后,覆盖掉本地所有消息。
React收到新消息后,就会对整个消息列表进行update。
备注:因为offset没有做本地持久化,所以用户首次进入到聊天窗口,offset丢了。
优化后
这是offset算法设计上的失误,改成按最后一条消息的message id来拉取。
可以看到,现在确实只update了ListHeader,更新任务的时间从600毫秒减到6毫秒!
小结
以上性能问题,其实都是React常见的bad case:
- PureComponent可能会失效,例如存在children时,每次dom diff,children都是新的element,详见 facebook/react#8669 ,常见的优化思路是把children常量化,社区也有一些专门的[babel插件](jamiebuilds/babel-react-optimize)。
- 慎用index,特别是倒序列表,或者列表存在prepend操作等场景。
- 慎用inline callback
最重要的还是要在实践过程中,充分利用chrome devtools等工具发现问题,优化性能问题。