速度是關鍵 - 我們如何讓 Hexo 加快 30%

最初由 Sukka (現任 Hexo 開發團隊成員之一) 以 簡體中文 撰寫,並由他本人翻譯成英文。

速度一直是 Hexo 的關鍵。3 年前,Hexo 3.2 通過模板預編譯將生成速度加快了 2 倍。而現在我們已來到 Hexo 4.2,通過幾項性能改進,我們成功地使生成速度比 Hexo 3.2 快了 30%。

效能基準

以下是基準的設定方式

  • Travis CI - Ubuntu Xenial 16.04
    • CPU:2 核心
    • RAM:7.5 GB
  • Hexo 預設主題:landscape
  • 300 篇隨機生成的文章。每篇文章都包含所有常用的 Markdown 語法和用於測試 highlight.js 的程式碼區塊。這些文章的前言 (Front Matter) 中也設定了唯一的分類和三個標籤。

自 Hexo 3.2 起,渲染的內容將被快取到 warehouse (db.json) 中,因此在基準測試中會測試冷啟動生成 (hexo clean 後執行 hexo g) 和熱啟動生成 (沒有 hexo clean) 的性能。每次基準測試都會以 冷啟動 => 熱啟動 => 冷啟動 的順序執行。記憶體使用量會使用 time 來測量,並取常駐記憶體集 (Resident Set Size, RSS) 的值。

您可以在這裡找到基準測試腳本。

Node.js 8

Hexo 3.2 Hexo 3.8 Hexo 4.2
冷處理 13.585 秒 0% 18.572 秒 +37% 9.210 秒 -32%
冷啟動生成 13.027 秒 0% 50.528 秒 +284% 8.666 秒 -33%
記憶體使用量 (冷啟動) 815.754MB 0% 1416.309MB +69% 605.312MB -26%
熱處理 0.668 秒 0% 0.712 秒 +6% 0.732 秒 +7%
熱啟動生成 11.734 秒 0% 46.339 秒 +295% 7.821 秒 -33%
記憶體使用量 (熱啟動) 702.535MB 0% 1450.719MB +106% 821.512MB +17%

Node.js 10

Hexo 3.2 Hexo 3.8 Hexo 4.2
冷處理 11.875 秒 0% 15.985 秒 +35% 8.043 秒 -29%
冷啟動生成 10.308 秒 0% 41.339 秒 +301% 7.450 秒 -28%
記憶體使用量 (冷啟動) 805.633MB 0% 1440.297MB +79% 599.008MB -26%
熱處理 0.700 秒 0% 0.676 秒 -3% 0.731 秒 +4%
熱啟動生成 8.322 秒 0% 35.453 秒 +326% 6.420 秒 -23%
記憶體使用量 (熱啟動) 679.082MB 0% 1447.109MB +113% 789.527MB +16%

Node.js 12

Hexo 3.2 Hexo 3.8 Hexo 4.2
冷處理 11.454 秒 0% 15.626 秒 +36% 8.381 秒 -27%
冷啟動生成 10.428 秒 0% 37.482 秒 +260% 7.283 秒 -30%
記憶體使用量 (冷啟動) 1101.586MB 0% 1413.359MB +28% 580.953MB -47%
熱處理 0.724 秒 0% 0.790 秒 +9% 0.790 秒 +9%
熱啟動生成 8.994 秒 0% 35.116 秒 +293% 6.385 秒 -29%
記憶體使用量 (熱啟動) 696.500MB 0% 1538.719MB +120% 600.398MB -14%

Node.js 13

Hexo 3.2 Hexo 3.8 Hexo 4.2
冷處理 11.496 秒 0% 14.970 秒 +29% 8.489 秒 -26%
冷啟動生成 10.088 秒 0% 36.867 秒 +265% 7.212 秒 -28%
記憶體使用量 (冷啟動) 1104.465MB 0% 1418.273MB +28% 596.233MB -46%
熱處理 0.724 秒 0% 0.776 秒 +7% 0.756 秒 +4%
熱啟動生成 7.995 秒 0% 33.968 秒 +325% 6.294 秒 -21%
記憶體使用量 (熱啟動) 761.195MB 0% 1516.078MB +99% 812.234MB +7%

移除 Hexo 的 cheerio 相依性

如您從基準測試結果所見,Hexo 3.8 中存在嚴重的效能倒退。結果發現,在 #3129 中引入的 meta_generator 過濾器是罪魁禍首。#3129 使用 cheerio<meta name = "generator" content = "Hexo [版本]"> 插入到 <head> 中,因此 cheerio 必須將 Hexo 生成的所有 HTML 載入到記憶體中並解析成 DOM。

cheerio 速度很快,但當遍歷數百個 HTML 檔案時仍然會出現效能瓶頸。在 #3677 中,我們提出使用原生 API 來取代 cheerio。在 #3671#3680#3685 中,我們使用 regex 取代 cheerio 用於 open_graph() 輔助函數、meta_generator 過濾器和 external_link 過濾器,並在 hexo-util#137 & #3850 中,我們使用更快的 htmlparser2 取代 cheerio。現在我們已在 Hexo 4.2 中完全移除 cheerio

改進「渲染的 HTML 快取」機制

「渲染的 HTML 快取」機制是在 Hexo 3.0.0-rc4 中引入的 (e8e45ed),這是嘗試通過快取渲染結果來提高 Hexo 的生成效能。然而,在執行 hexo g 時,每個路由只會使用一次,因此會消耗記憶體,但卻沒有獲得效能提升。在 #3756 中,「渲染的 HTML 快取」機制被停用用於 hexo g,並啟用用於 hexo s,因此 hexo g 的記憶體使用量已降低。

移除 Hexo 的 Lodash 相依性

Lodash 是一個現代 JavaScript 工具庫,它使處理陣列、數字、物件和字串更加容易。然而,隨著越來越多新功能被引入到 ES6 中,Lodash 的大部分功能都可以被原生 JavaScript 取代。

Hexo 實際上在一年前就開始減少對 Lodash 的依賴,例如 #3285#3290 & warehouse#18。在 #3753 中,我們建議通過遵循 You don’t (may not) need Lodash/Underscore 來逐步使用原生 JavaScript 取代 Lodash。經過 #3785#3786#3788#3790#3791#3809#3810#3813#3826#3845hexo-util#141#3880 & #3969 之後,我們成功地從 Hexo 中移除了 Lodash。我們還在 You don’t (may not) need Lodash/Underscore 開啟了一個新的 PR,將我們的 _.assignIn 替代方案帶回社群。

快取工具函數的返回值

hexo-util 中有許多工具函數,例如:用於計算相對路徑的 relative_url(from, to)、用於將相對路徑轉換為 URL 的 url_for(path)full_url_for(path)、用於從電子郵件地址計算 Gravatar URL 的 gravatar(mail),以及用於判斷給定的 URL 是否為外部連結的 isExternalLink(url)。我們發現這些函數在 Hexo 生成過程中可能會被調用數千次,同時可能會重複傳遞相同的參數,因此可以快取參數和返回值的鍵值對。這個想法在 hexo-util#162 中實現。

未來

我們已在 #3776 中,將效能基準測試 (Benchmark) 作為單元測試的一部分加入 CI。自此之後,效能基準測試已多次協助我們找出潛在的效能回歸問題(例如 #3807 & #3833),並避免了如 #3129 這類嚴重的效能回歸。我們將進一步在 #4000 中,將火焰圖 (flamegraph) 加入單元測試案例,這將有助於我們更好地最佳化 Hexo 的產生過程。對於 Hexo 來說,速度始終是關鍵。