简聊首屏性能优化方案一些记录(备份)

原文一个月前发布在简聊(https://jianliao.com/)博客上, 这边作一下备份
https://jianliao.com/blog/jian-liao-shou-ping-xing-neng-you-hua-fang-an-xie-ji-lu/php


Single Store 的重要性

首先整个改进方案的基础是 Redux 提出的 Single Store 架构
按照 Redux 的理念对应用进行抽象之后, 架构回归到 MVC 很是原始的理念,
也就是: 一个 Model, 一个 View, 以及剩下的 Controller 代码react

我认为存在问题的方案是对象化的封装, 特别是每一个 View 存在独立的相似 MVC 的对象,
具体来是简聊早期使用的 Backbone 架构, 数据分散在各个 Collection 当中, 难以管理git

到了 Redux 的架构中, 只有一个 Store, 对数据进行统一管理就方便多了
而 immutable-js 的加入, 更让整个架构的数据流变得很是清晰
Redux 方案里, 数据层是 Store, 界面是 React Component 组成的 View
而 Controller 的只能由 Actions 和 Reducer 来承担, 这里只是作个类比github

为了从简单的数据结构和函数构建复杂的应用, 每一个部分都要进行抽象和复合
首先 View 借助 React Components 从小到大很灵活地进行组合
而 Store 经过 Map 和 Reducer 函数也能组合(其实这一点咱们没有贯彻)
此外, 应用和服务端存在数据同步的需求, 也须要考虑抽象(后面讨论). 总体架构大体就是这些api

数据界面的分离

Redux 最知名的是它的 Time Travel Debugging 功能, 也就是记录 Actions 和 Store 进行回溯. 实际上这也是检验"数据界面分离是否充分"这样一个架构是否完善的一个考验
当一个单页面应用能自由地回滚数据状态而不引起异常, 才能够更有信心地说应用的行为和数据流很是清晰, 很好预测
同时, 界面和数据没有复杂的双向的操做, 特别是渲染界面时致使数据更新
React 组件任意地渲染更新界面, 并且数据不受影响, 应用才不会走向混乱浏览器

数据同步问题

在实际的应用编写当中, 切换页面的加载数据过程, 存在具体的问题
早先咱们的架构当中切换路由, 是先切换界面, 而后界面初始化时加载数据
可是这个作法就违背上面定下来的方案了, 就是渲染过程存在数据操做
另外一个实际的影响是, 请求数据的逻辑是在组件挂载时才调用的, 并很差优化
设想一下当咱们想加载数据, 却要先去渲染界面, 这样的架构是否清晰?
固然, 对我来讲最头疼的仍是前面的, 阻碍我从更高层次对架构进行优化的问题缓存

好比说简聊切换话题, 点击话题, 地址改变, 就须要加载数据和渲染界面
按照默认的 react-router 的行为, 地址改变将直接致使界面重绘
也就是新的话题的界面立刻就被渲染出来,然而次数话题的数据尚未请求到
话题的组件被迫渲染没有数据的话题, 等到数据加载完成, 再从新渲染一遍
这个界面不少行为超出控制, 难以优化, 特别是路由不受控制
正确的流程, 应当是先加载数据, 再渲染界面, 以及加载过程作一些提示性能优化

基于上边的架构设计和具体问题, 简聊采用了本身实现的路由组件
这个路由组件的行为经过 Single Store 中的数据控制, 以及相应的 loading 状态
如今版本的简聊, 点击话题, 会先标记 loading 状态, 同时在后台发起数据请求,
请求完成, 重置 loading 状态的同时, 请求到的数据在界面上被现实出来
以及, 如今也能够对数据请求操做进行合并, 这点将方便将来的优化
包括切换团队, 如今也能作到优化, 达成先加载数据而后渲染界面的效果网络

为了处理好这个过程, 还须要知道的是, 对应的路由须要哪些数据
好比说, 在话题 A 里边, 咱们须要 topicA, memberA, team1, contact1
而在话题 B 里边, 咱们须要 topicB, memberB, team1, contact1 的数据
从 A 切换到 B, 就须要分析本地混存已有哪些数据, 缺乏那些数据
而后才能准确地抓取缺乏的数据, 在界面须要的数据抓取完成时开始渲染
Facebook Relay 就是以此为目的的一套方案, 只是并不适合咱们
所以项目中本身实现了简单的数据依赖分析代码, 达成了这个目的数据结构

首屏渲染的花招

先说简聊有一个针对断网和从新链接作的特殊处理, 会强制更新一遍本地的数据
具体说就是清楚掉内存缓存的旧的消息和话题, 或者或标记全部的缓存失效
而后, 会按照前文描述的方案分析当前须要的数据, 进行一次数据抓取
其实这个过程的关键是, 须要根据路由分析出当前界面须要哪些数据
由于简聊主要的状态是存储在 Store 当中, 至少在架构上不存在障碍
通过这样的操做, 网络从新链接之后, 简聊能够恢复到一个同步更新的状态

而首屏加载是类似的问题, 用户一段时间没有登陆, 这时须要进行一次同步
区别只是在于, 首屏加载本地并无对应缓存, 须要拿到数据才能开始渲染
抓取更新数据的问题, 前面讲的网络重连的代码彻底能够重用
而后是缓存, 跳得远一点, 前面说了, 简聊是 Single Store, 主要数据存放在一块儿的
因而, 就颇有可能, 把 Single Store 存储下来, 做为下次页面打开的缓存使用
也就是说, 至关于关闭浏览器时把一切缓存下来, 下次打开时一切复原到关闭时的情景
而实际上用户经过 jianliao.com 访问应用, 极可能就命中这个缓存了

基于这样的出发点, 简聊在在关闭应用时将 Store 转化为 JSON 字符串存进 localStorage
应用再次打开时, 若是条件合适命中缓存, 就直接将缓存渲染出来
而且在后台分析依赖开始抓数据, 最终在得到新数据以后再次更新界面
对于用户来讲, 浏览器输入 jianliao.com 就当即开始渲染, 省去很多的等待时间
若是不是太长时间没有登陆简聊, 新数据更新的界面也不会太明显
最直接的效果就是不少状况下简聊能够更快地打开了, 也就更方便

渲染性能优化

在这个花招使用的先后, 简聊加载的顺序发生了一些变化

以前: 加载资源, 运行代码, 请求数据, 渲染界面
以后: 加载资源, 运行代码, 渲染界面, 后台请求数据, 局部更新界面

去掉了基数和波动较大的网络请求时间, 剩下的就是 JavaScript 执行和渲染的时间了
想要让简聊首屏更快地出如今用户面前, 就要开始优化启动和渲染速度

经过 Chrome 的 Timeline 调试工具, 我逐步收集到的大概有这些点
主要是 DOM reflow 的开销, 不稳定然而一般致使较长的时间消耗
(下面的列表是我主要基于记忆列出的, 并非完整的):

  • getBoundingClientRect 调用, 关系到一些菜单定位(能够优化)

  • focus 操做, 有时会触发 DOM 的 reflow(有的不能优化, 但能够延时处理)

  • scrollTop 读取和操做比较明显(然而部分调用不能优化, 不然影响到用户体验)

  • jQuery 初始化过程会读写 DOM, 可能触发 reflow (目前还不能去掉)

  • React 初始化过程会作一些 DOM 的探测(没法优化)

  • rangy 初始化过程有 DOM 读写(没法优化)

  • favico.js 初始化有 DOM 的读写(没法优化)

问题的关键固然是 DOM 操做触发了一些 reflow 的问题
其次 JavaScript 初始化各类 timer 和复杂计算也会消耗时间
但实际上纯 JavaScript 自己性能, 只要代码不写出问题, 也不会慢了
只是对 DOM, 仍是要关心挺多, 这方面直接推荐网上的资料了:

http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/
https://gist.github.com/paulirish/5d52fb081b3570c81e3a

考虑到这是剩下的主要是 CPU 密集的计算, 实际上受到 CPU 性能影响较大CPU 性能越好, 渲染也就越快. 至少如今已经部分地去掉了网络慢的影响另外, 咱们目前对渲染作的优化只是初步, 相信后续的深刻优化之后初次渲染的性能还会有一些提高