スピードが重要 - Hexoを30%高速化した方法

Hexo開発チームメンバーのSukka簡体字中国語で執筆し、自ら英語に翻訳しました。

Hexoにとってスピードは常に重要です。3年前、Hexo 3.2はテンプレートのプリコンパイルにより生成速度を2倍に向上させました。そして現在はHexo 4.2となり、いくつかのパフォーマンス改善により、Hexo 3.2と比較して生成速度を30%向上させることに成功しました。

ベンチマーク

ベンチマークの設定方法をご紹介します。

  • Travis CI - Ubuntu Xenial 16.04
    • CPU:2コア
    • RAM:7.5GB
  • Hexoのデフォルトテーマ:landscape
  • ランダムに生成された300件の記事。各記事には、一般的に使用されるMarkdown構文と、`highlight.js`をテストするためのコードブロックが含まれています。各記事のFront Matterには、固有のカテゴリと3つのタグも設定されています。

Hexo 3.2以降、レンダリングされたコンテンツは`warehouse` (`db.json`)にキャッシュされるため、コールド生成(`hexo g`の前に`hexo clean`を実行)とホット生成(`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`を使用して`<head>`に`<meta name = "generator" content = "Hexo [version]">`を挿入するため、`cheerio`はHexoによって生成されたすべてのHTMLをメモリにロードしてDOMに解析する必要があります。

`cheerio`は高速ですが、数百のHTMLファイルをトラバースする際にはパフォーマンスのボトルネックが発生します。#3677で、`cheerio`をネイティブAPIに置き換える提案を行いました。#3671#3680#3685では、`open_graph()`ヘルパー、`meta_generator`フィルター、`external_link`フィルターの`cheerio`を正規表現に置き換え、hexo-util#137#3850では、`cheerio`をより高速な`htmlparser2`に置き換えました。これで、Hexo 4.2では`cheerio`を完全に削除することができました。

レンダリング済みHTMLのキャッシュメカニズムの改善

レンダリング済みHTMLのキャッシュは、Hexo 3.0.0-rc4(`e8e45ed`)で導入されました。これは、レンダリング結果をキャッシュすることでHexoの生成パフォーマンスを向上させる試みでした。ただし、`hexo g`の実行中は各ルートが1回だけ使用されるため、パフォーマンスが向上しない一方でメモリが消費されます。#3756では、`hexo g`ではレンダリング済みHTMLのキャッシュが無効化され、`hexo s`では有効化されたため、`hexo g`のメモリ使用量が削減されました。

HexoからのLodash依存の削除

Lodashは、配列、数値、オブジェクト、文字列の操作を容易にする最新のJavaScriptユーティリティライブラリです。しかし、ES6に新しい機能が追加されるにつれて、Lodashの機能のほとんどはネイティブJavaScriptで置き換えられるようになりました。

Hexoは実際には1年前からLodashの依存関係を減らし始めており、#3285#3290warehouse#18などがあります。#3753では、You don’t (may not) need Lodash/Underscoreに従って、LodashをネイティブJavaScriptに徐々に置き換えることを提案しました。#3785#3786#3788#3790#3791#3809#3810#3813#3826#3845hexo-util#141#3880#3969を経て、LodashをHexoから正常に削除することができました。また、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では、ユニットテストの一部としてCIにベンチマークを追加しました。それ以来、ベンチマークは(#3807#3833のような)潜在的なパフォーマンスの低下を何度か発見するのに役立ち、#3129のような深刻なパフォーマンスの低下を回避するのに役立っています。そして、#4000では、ユニットケースにフレームグラフを追加するためにさらに一歩踏み出す予定です。これは、Hexoの生成プロセスをより最適化するのに役立ちます。Hexoにとって、スピードは常に重要です。