如何实现一个模板引擎二:优化

前言

上一篇文章中讲了怎么来实现一个模板引擎,而写完上一篇文章的时候,我的模板引擎也确实是造出来了,不过起初的实现还是比较简陋,就想着做一下性能优化,让自己的轮子,真正能成为可用的组件,而不仅仅是一个 demo。于是就有了这篇文章。

工具

在做组件优化的时候,总不可能自己觉得那样写会提升性能就那样写了,很多时候,瞎尝试可能会带来反效果,所以我们需要一个工具来验证自己的优化是否有效,业界最常用的就是 benchmark.js 了,因此,我也是用 benchmark 来做验证。

除了 benchmark 之外,我们最好还需要一个用来对比的东西,才知道要优化到什么程度才可以。而我的组件的语法是参考 nunjucks 做的,因此我就理所当然的选择了 nunjucks 来做对比了。

优化之前

在做优化之前,我先写了几个 benchmark 来测一下。

简直全方面被吊打。简单说一下这几个 benchmark 的测试例子是怎样的:renderExtend 是测试有 extend 其他模板文件的测试例子,renderNormal 是渲染一段比较多嵌套的模板,renderSimple 是渲染一段非常简单,只有变量的模板。

具体可以看 https://github.com/whxaxes/mus/tree/master/benchmark

优化实现

1. 能在 ast 阶段做的事,尽量在 ast 阶段做好

做模板渲染之前,都会先生成 ast 并且缓存起来,从而将一切准备工作准备好,尽量减少渲染时候的计算量,从而提升性能。

在此前的实现中。如果有看过上一篇文章的人应该有印象,在进行变量渲染的时候,会把表达式用方法字符串包装起来,并且创建一个方法实例,但是这个行为是在渲染阶段做的。也就是以下这段:

但是其实创建方法实例,是完全可以在构建 ast 阶段就准备好的,而渲染阶段,就只需要执行已经准备好的 render function 即可。

上面贴的 benchmark 结果其实是已经做了这个优化的了,在做这个优化之前,renderNormal只有 7000ops/sec 而已。

2. path.resolve

刚开始,上面的 benchmark 中有一点让我特别疑惑,就是 renderSimple 的差距,只是一个变量渲染而已,怎么会差那么多,经过排查,发现代码中在读取模板文件的时候,每次都会进行 path.resolve 来获取文件的绝对路径。于是立马对该操作进行了缓存。跑分立马就上去了。

3. for 循环的优化。

在此前的实现中是这样的:

注意到每个 for 循环中都会重新做一次对象的浅拷贝,而其实完全没必要,因为在每个 for 循环中需要的对象都是类似的,因此只需要做一个浅拷贝即可。就改成了:

4. 对表达式进行预处理

此前的实现中,无论什么样的表达式,都一股脑,直接拼成方法来处理,而且此前的都是用 with 来包裹的,而被 with 包裹的代码,在 js 引擎解析的时候是没法做优化的,执行效率特别慢。

因此可以在构建 AST 阶段,对表达式做预处理:

  1. 如果是简单的字符串,或者数字,就完全都不需要创建 function 了,直接返回即可。
  2. 如果是简单的变量输出,比如{{ test }}或者{{ test.value }}之类的,就不需要用 with 包裹。直接拼成{{ _$o.test }},然后再创建 function。
  3. 剩下的就是有运算符之类的,这种不太好解析,就直接用 with 包裹了。

5. filter 的优化

在第4点中,我会对表达式做一个类型判断,但是还不够,按照此前实现的 filter 的逻辑,有 filter 的表达式,会被组装成_$f('nl2br')(test)的格式,一旦被组装后,到第四点中的表达式判断的时候,就会被认为是比较复杂的类型从而选择使用 with 来组合渲染方法。所以这个也是可以优化的点。然后就把 filter 的处理部分改成:

把 filter 的 function string 分为左半边以及右半边来进行收集,在做完类型检查之后,再把 filter 组合起来。这样的话,filter 就不影响类型检查了。

除了以上几个,还有将所有的 for 循环改成了 while 循环,经过一系列优化后再次跑 benchmark:

在已有的测试例子中,分数都超过 nunjucks 。也算是优化成功了。

写本文更多是记录一下自己的优化过程。可能没啥干货,有兴趣的看看,没兴趣的也请勿喷。

最后再贴上项目地址:https://github.com/whxaxes/mus

1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部