前言
window.__APP_DATA__.RESOLVERS.userInfo(error);
即此时应用初始化完毕后可以无视首屏数据的完成度,直接进入首屏渲染节点,组件在数据 promise 被 resolve 后渲染即可:window.__APP_DATA__.userInfo.then(data => component.render());
通过对数据片段的promise化改造,使得应用初始化节点也加入了并行队列。
整个首屏呈现 timeline 变化如下:
根据变化后的节点我们得到首屏呈现时间为:1800ms
首屏呈现耗时的通用计算公式变为:
下载静态片段 + Max(下载资源文件 + 应用初始化,请求首屏数据) + 首屏初始化 + 首屏渲染
优化小结
经过上述2个步骤改进,我们应用首屏呈现时间从 2800ms -> 2350ms -> 1800ms,总体效果约为36%,可以看到是收益还是很可观的。
在实际项目中耗时是在1600ms左右,比1800ms还要小,主要原因如下:
用户在请求入口页中半个RTT时间,服务器就开始了数据请求。
数据请求在服务端进行减少了浏览器与服务端的请求创建开销,同时数据请求在内网进行,总体调用速度也会加快。
当首屏数据请求数超过浏览器并发请求数时,该方案收益会更明显,因为 NodeJS 端没有并发限制,甚至在NodeJS端与后端服务的交互中可以采用更高效的协议如HTTP2来提高调用速度。
与服务端同构渲染对比
看到这里,相信很多人会问,为啥不用服务端渲染直出HTML呢,或者和服务端渲染方案相比有何优势?
事实上,一开始我和大多数人想到的优化方案就是服务端渲染,但真正的障碍在于服务端渲染依赖视图层框架的支持,而我们的项目历史悠久,视图层框架并不支持这一点,为了优化而丧失产品的稳定性得不偿失。
当然,在另辟蹊径使用了数据渐进式预加载方案后,我总结该方案与与服务端同构渲染对比如下。
优势
对客户端代码来说数据渐进式预加载方案实现成本非常简单,基本可以做到透明化,我们在实际的开发过程中采用基于 uIoC(ecomfe/uioc ) 提供的AOP拦截方案,通过配置化的方式让客户端的代码改造仅局限在配置文件,应用代码基本未改动。
对NodeJS端来说,分层合理的应用只需要将数据层简单适配下 NodeJS 端即可完成数据渐进式预加载,这对底层基础框架在视图层没有支持同构的应用来说,整个改造成本可以说大大减小,且收益明显。我们目前的应用基于自有的一套MVC框架,仅仅是将 Model 层简单适配 NodeJS 端执行输出数据。
服务端渲染方案如果未能提供较基于 BigPipe 的渲染,总体的页面呈现速度还是不如数据渐进式预加载的,且目前我也暂时还没有在三大框架中发现有一套基于BigPipe的服务端渲染方案。
不足
整体呈现速度可能不如结合了BigPipe的服务端渲染方案,但这点没有经过论证,毕竟数据渐进式预加载与服务端同构渲染的区别仅仅在于渲染环节放在客户端还是服务端:渲染看的是CPU,服务端的CPU资源是有限的,要服务诸多请求,而客户端渲染则基本无此压力,渲染能力未必弱于服务端。
总结
我们在单页应用的性能优化上基于很朴素的并行化理念实施了首屏数据渐进式预加载方案,在实际项目中也得到了较为明显的效果,减少了1.2s的加载时间,整体的节点变化如下:
优化前:
优化后:
最终数据渐进式预加载方案的首屏呈现时间计算公式为:
下载静态片段 + Max(应用资源加载 + 应用初始化,请求首屏数据) + 首屏初始化 + 首屏渲染
这里忽略了影响很小的片段传输时间,有打算尝试的朋友可以将自己应用的相关节点数据代入计算即可。
数据渐进式预加载,服务端同构渲染,客户端渲染三种方案各有优缺和场景,个人未来计划是将三种方案结合实时流量数据动态切换:在服务器压力不大时用同构渲染;服务器压力较大时用数据预加载;服务器压力很大时用客户端渲染。