基于Vue-SSR优化方案归纳总结
Vue-SSR相信大家都不陌生,与传统 SPA 相比,服务器端渲染 (SSR) 能够具备更好的SEO,方便搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,除此之外,SSR能够在更短的时间内渲染出页面内容,通过在服务端填充数据吐出到客户端的方式,让用户有更好的用户体验。
前言
基于VueSSR的页面优化常有,而针对VueSSR的再优化不常有。前段时间有幸作为 宇宙无敌上级特派看门员 参加了前端tweb大会,听取了腾讯视频Web高级工程师lucien(段隆贤) 分享了针对SSR场景下的一些优化,由于笔者之前也有在项目中实现SSR渲染,所以也针对Vue-SSR的优化进行了实践和归纳总结,并且在前人的基础上进行了新的优化尝试,当然,不同的项目不同的场景下,优化效果、优化方案可能不尽相同,需要读者们自行选取~(本文将讨论常见的SSR优化方案以及笔者个人的优化尝试)
CSR与SSR的区别
首先,还是要不厌其烦地过一遍CSR和SSR的区别,在理清整个流程后,才能发现性能瓶颈以及关键耗时在哪里。
CSR一般由静态资源服务器(CDN)等直接返回HTML资源,之后浏览器解析HTML加载CSS、JS资源(CSS加载结束后页面会尽快进行首屏渲染FP),JS依赖加载结束后,Vue实例初始化,拉取页面数据,页面渲染(FMP)。
SSR由nodejs服务器来直出页面,请求到达后端后,后端拉取cgi接口数据,根据直出bundle生成render对象,render对象将执行客户端代码构建VDOM,生成HTML string,填充进模板HTML,返回HTML资源,浏览器解析后加载CSS、JS资源,(在CSS加载结束后触发FP和FMP),Vue实例初始化,接管后端直出的HTML,页面可响应。
(以下流程图引自: https://www.jianshu.com/p/10b6074d772c)
时序图
(注: FP即First paint,首屏渲染,可能是没有数据的状态。 FMP即First meaningful paint,处于已经渲染数据的状态。 可交互: 页面数据填充结束且可响应。 )
SSR存在的缺陷:
1、对服务器提出更高的要求,生成虚拟DOM如果相对较长的运行和计算耗时;
2、由于cgi拉取和vdom直出后才吐出HTML页面,FMP虽然提前了,但是FP相对延迟了;
3、相比CSR,SSR渲染后,由于仍然需要进行依赖、vue初始化,页面可交互时间并没有较大改善。
常见优化方法
虽然SSR仍有许多不足之处,但是也不是没有改善的空间。
一、缓存优化
1、页面级别缓存:vuessr官网给我们提供了一种方法,如果页面并非千人千面,总是为所有用户渲染相同的内容,我们可以利用名为 micro-caching 的缓存策略,来大幅度提高应用程序处理高流量的能力。这通常在 Nginx 层完成,也可以在 Node.js 中实现。
2、组件级别缓存:通过对组件设置serverCacheKey的方式,如果组件serverCacheKey相同,将复用之前渲染的组件产物,不需要重新渲染。具体是类似这样的:
export default {
name : ‘myComponent’ , // 必填选项
props: [ ‘item’ ],
serverCacheKey: props => props.item.id,
render (h) {
return h ( ‘div’ , this.item.id)
}
}
3、cgi接口缓存:如果部分cgi接口返回的数据是固定的, 我们可以在node后端拉取cgi的时候,设置cgi缓存,缓存至memcache或其他轻量存储服务,当然,你也需要设置好缓存更新策略。
二、代码实现优化
1、减少组件嵌套层次,优化HTML结构:由于组件最初需要在node后端进行VDOM计算和渲染,优化组件层次结构,减少过深曾经的DOM嵌套,可以减少VDOM计算耗时。
2、减少首页渲染数据量:根据业务调整用户首屏可见的所需渲染的数据,其他数据懒加载或异步加载。
三、资源加载
1、流式传输:vuessr官网给我们介绍了一种方法,render对象会暴露renderToStream方法,把原有的直出结果以流的形式输出,让我们可以更快的响应数据到客户端,能减少首屏渲染时间,更早开始加载页面资源。(流式传输需要在asyncData执行结束后开始,否则没有数据,这意味着流失传输受限于cgi拉取耗时)
2、分块传输:lucien大佬在tweb大会上给我们带来了新的思路,由模板的语法树, 分析代码的上下文,分析数据和模板间的依赖,用异步数据分割模板,分块逐步输出。(相比流式传输,前置位的cgi数据一旦ready,就会渲染输出,而不需要等待所有的gi拉取到后才开始渲染输出,但是该方案改造成本较大)
一张图说明白这两者的区别:
四、改造SSR算法
SSR算法改造:在tweb大会上lucien给我们介绍了一个新的思路,改造直出算法,不用vue-loader而用自研的aga-loader,将vdom渲染转换为字符串模板,具有更高的渲染性能。
性能提高的同时,由于没有完整的组件运行环境,也带来了部分语法上的约束,同时,也不支持vuex。
思考
看到这里,读者们应该对SSR了如如来神掌且熟悉了常见的优化方法,但是回头思考一下,Vue-SSR的优化无非是在 cgi拉取 和 VDOM直出渲染 上下功夫,因为这两者就是node后端最耗时的步骤,其次,由于这种耗时会同步阻塞页面的FP,所以更进一步的方法是流式输出或分块,减少首屏渲染时间。
然而,但是并不是所有的cgi都能缓存,类似拉取用户个人信息的cgi就无法缓存,SSR算法改造成本大,约束也大。再看看流式传输和分块传输,两者虽然都对FP时间优化了,但流式传输受限于cgi拉取时间,分块传输改造成本大。而且两者存在的一个共性问题,那就是可交互时间仍然没有优化。
当然,这里并不是要否定所有的优化方法,而是方法各有优劣,比较优缺点大家才能根据自己的业务需求和优化场景选取合适的优化方法。受流式传输和分块传输的启发,我们能不能在这上面下功夫?在请求到来时,先返回一份完整的HTML空页面,让客户端更快的FP,其次,后端拉取cgi和渲染VDOM 与 前端拉取CSS、JS资源 两者同步进行,之后再吐出直出的HTML string 与 页面store,再次渲染页面,这样的话FP提前了,和CSR的FP时间一毛一样,其次,FMP相比CSR大大提高,更重要的是,由于JS资源的加载让Vue初始化触发的更早,意味着页面可响应时间也会提高。
为了阐明这种区别,我们看一下流程图:
新方案探索与实践
先吐空页面,之后再吐直出后的数据,但是关键是怎么让直出后的数据再渲染上去,同时不要让JS先执行了,导致页面直接变成CSR了。
思考历程:不要让JS执行,等直出数据回来了再执行,这可咋办,笔者最初想实现一个JS加载控制器,不通过script来引入js,而是自己去拉取js代码,eval函数执行,这样js的执行控制权就在自己手上了,但是有几个问题,eval函数解析只是把字符串当js来执行,那错误上报就会出问题,接了sentry错误上报是基于js文件、错误行列来定位的,除此之外,ajax来拉取js代码会不会存在性能问题,和浏览器加载js资源速度上是否存在差异?还有第三方js不能直接ajax拉取,需要设置跨域头。于是笔者开始换一种思路,能不能给每个js文件包裹一层函数,通过setTimeout(fn,0)的方式来延迟调用,但是这又有问题,有多个js文件且文件已经是打包好了的,改了js文件,map映射不就乱了吗?错误上报不就乱了吗?
源码在自己手里,为啥不直接在源码上提供一个调用入口,来触发js执行,最后直出的时候吐出