实测Vue SSR的渲染性能:避开20倍耗时
前端技术年年有新宠,Vue.js 2.0以其轻量级、渐进式、简洁的语法在MVVM框架中脱颖而出,一经推出便很受业界青睐。
QQ空间Hybrid业务也在积极推动MVVM框架重构H5页面。为了提高首屏渲染速度,wns缓存+直出 是必不可少的。在Vue 1× 时代,没有 server-side-render 方案,直出需要专门给写一份首屏非Vue语法的模板。Vue2.0 server-side-render(简称Vue SSR)的推出,成功地让前后端渲染模板代码同构。
不过对于海量PV级的业务,直出模板的渲染效率直接影响服务端的压力,在对业务代码重构的同时,直出模板的性能是需要衡量的关键指标。
当前常用的模板渲染方案可以归结成两类:
a类:string-based (基于字符串拼接)
b类:virtual-dom-based(基于虚拟dom对象)
Vue SSR的模板是virtual-dom-based,所以QQ空间Hybrid业务做Vue 2.0的改造的同时,模板类型也从之前的a类转换成b类。
本文是在实际业务场景中对Vue SSR的渲染性能做测试,并解析渲染步骤,给出尝试优化的方案和最终结论。
还是先介绍下常用的string-based模板的写法和编译后的样子, 帮助不太熟悉HTML模板原理的同学可以更容易理解文章后面优化的过程。
首先看一下H5页面常用的HTML模板写法:
上面的模板应该是前端开发者很熟悉的一种模板语法,通过<%%>
来包裹逻辑,<%=%>
来赋值。
下面来看看模板代码被编译后的JS是什么样的:
JS中执行 tmpl.test(data) 即可拿到渲染出来的HTML片段。tmpl.test 就相当于是一个test模板的render函数。
那么这个render函数是如何通过上面的script模板转换过来的呢,此时需要一个模板的编译工具来实现,编译工具的核心是一个正则表达式:
let jscReg = new RegExp(/(?:(?:\r\n|\r|\n)\s*?)?<%([=-]?)([\w\W\r\n]*?)%>(?:\r\n|\r|\n)?/gmi);
用这个正则表达式,可以将模板代码中的 JS逻辑与JS属性值 分割出来,再通过'引号来将静态dom串包裹,拼接,最后加上函数的头尾,就能得到一个完整的render函数。编译过程可以通过预编译在线下实现,也可以在线上实时动态完成。
看到现在应该都理解了string-based HTML模板在JS中渲染的方式了吧,这种类型的模板渲染效率其实是最高的,因为它的render函数是不存在任何冗余逻辑,completely字符串拼接。
首先在QQ空间礼物商城列表首屏来做个实验,使用手机QQ扫描下方二维码即可前往查看(建议手机QQ扫一扫,微信中打开只是静态展示):
Vue SSR渲染调用代码如下:
传统模板渲染调用
实测循环 1w 次渲染耗时对比:
传统模板:143ms
Vue SSR:2685ms
Vue SSR的耗时是传统模板的近20倍!
20倍差距是如何产生的?我们来分析一下Vue SSR完整的渲染过程。
Vue模板片段:
Vue模板语法大部分都是指令试的伪代码,既不是HTML语法,也不是JS语法,这也是virtual-dom类模板渲染较复杂的原因之一。
在上文中可以看到,步骤1(模板字符串通过正则解析成virtual-dom对象)与步骤2(生成with绑定上下文对象的render函数)都已经被缓存,在本次对比中直接忽略其耗时,问题只能出在步骤3、4:
步骤3:执行render函数生成vnode-tree对象;
步骤4:递归遍历vnode-tree将其转化拼接成HTML;
相比传统string-based模板以最直接的方式拼接HTML,逻辑包含了 with、call、对象创建、递归拼接 是制约性能的关键,由于vnode对象是包含了业务数据,不能通过缓存vnode来解决,即便是缓存了vnode,步骤4的拼接耗时也是瓶颈所在。
既然我们已经知道什么样的render函数是最快的,那么就做个工具直接把Vue模板编译成string-based类的render函数即可,目标:
引入jscHelper
将vue-server-render在 第1步 生成的 virtual-dom对象 拼成 string-based语法的模板字符串,genEl是先将Vue模板语法转换成jsc语法:
1)值的转换
2)for循环转换
3)if判断转换
4)处理属性
5)Vue指令转换
6)样式class和内联代码转换
7)递归处理子dom
8)标签闭合
这样Vue模板就被jscHelper转换成了传统的string-based类模板。
再通过文章开头介绍的正则表达式:
let jscReg = new RegExp(/(?:(?:\r\n|\r|\n)\s*?)?<%([=-]?)([\w\W\r\n]*?)%>(?:\r\n|\r|\n)?/gmi);
将模板编译成render函数,加以缓存。详细过程请看GitHub上的jscHelper源码。
最终优化效果非常明显:
传统模板: 160 ms
Vue SSR : 2963 ms
Vue SSR + jscHelper: 210 ms
注:由于jscHelper需要对Vue的语法做转换,复杂的语法会增加耗时,所以耗时还是略高于传统模板的。
通过jscHelper对vue-server-render的性能做提升,需要持续地维护对Vue语法的兼容,而且目前并不支持类似<transition>
, <keep-alive>
, <router-view>
等高级语法,对组件的渲染方式也需要兼容。作为使用方,我们更希望Vue作者本身能多提供一种简单的string-based渲染方式来作为高性能的直出渲染方案。
我已经在GitHub上面提了相关的issue ,希望能在Vue.js未来版本中看到更好的渲染实现。
郭佳伦,腾讯QQ空间前端高级工程师,5年前端相关经验,现负责手机空间Hybrid业务,专注于H5框架在业务上的演进及性能优化。
本文系腾讯QQ空间前端团队在前端之巅开设的专栏文章,欢迎关注后续内容。
今日荐文
点击下方图片即可阅读
Slack团队切换至TypeScript,简化大型JS代码库管理
InfoQ主办的移动和前端开发领域的精品大会【GMTC 2017】将于6月9~10日在北京举行,作为首届以“大前端”为主题的大会,GMTC涉及移动、前端、跨平台、AI应用等多个技术领域,帮助你方方面面提高技术水平。扫描下图二维码或戳阅读原文,前往官网了解详细信息!
「前端之巅」是InfoQ旗下关注前端技术的垂直社群,加入前端之巅学习群请关注「前端之巅」公众号后回复“加群”。推荐分享或投稿请发邮件到editors@cn.infoq.com,注明“前端之巅投稿”。