如何實現一個模板引擎二:優化

發表於2017-04-08

前言

上一篇文章中講了怎麼來實現一個模板引擎,而寫完上一篇文章的時候,我的模板引擎也確實是造出來了,不過起初的實現還是比較簡陋,就想著做一下效能優化,讓自己的輪子,真正能成為可用的元件,而不僅僅是一個 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

相關文章