この記事何?
エンジニア歴1年半。業務で利用しているHonoが大好きだが、TSもプログラミング知識も弱々すぎて上手く使いこなせない。速いはずが多分生かせてない。
ちゃんと理解するためにソースコードリーディングしたい。
でも目的なくソースコード眺めても意味ないし……ねむい……
そうだ!PRを全部読めば、変遷やなぜ変更されているかストーリー的に分かるのでは。
と思ったのでチャレンジしてみる。PR2000以上あるので、全部できるかは知らん。
https://github.com/honojs/hono
#6 Develop
example/basic/index.js
- ルーティングの構成例を追加
src/hono.js
- 単一ハンドラから 複数ハンドラ(ミドルウェアチェーン)対応 へ
-
addRoute(method, path, handler)->addRoute(method, path, ...handler) -
dispatchも1つのハンドラ実行から ハンドラ配列を順次実行する仕組み に変更 -
dispatchの 引数を(request) → (request, response) に変更してレスポンスを受け渡せるように修正 - 戻り値も最後に生成された response を返すように変更
-
あってるか??
src/hono.test.js
- ミドルウェアのテスト追加
src/node.js
こちらも単一ハンドラから複数ハンドラに対応
class Result {
constructor({ handler, params } = {}) {
- this.handler = handler
+ this.handler = handler || []
this.params = params || {}
}
}
ミドルウェアチェーンとかってどうやって実装するのかなと思ってたけどシンプルに配列に関数を入れてforで回して順次実行なのね。
学び。
#7 Feature/performance
コミットメッセージに「Dont use proxy」「Make it
Fastest!」とあるので、Proxyを使わないようにしてパフォーマンス改善?
Proxyなんだっけ。使うとなぜ遅くなるんだろう。
src/hono.js
変更前:
const proxyHandler = {
get: (target, prop) => (...args) => {
// Proxyで動的にメソッドを解決
}
}
const WrappedApp = (router = new App()) => {
return new Proxy(router, proxyHandler)
}
変更後:
class Hono {
get(...args) { return this.addRoute('GET', ...args) }
post(...args) { return this.addRoute('POST', ...args) }
// 明示的にメソッドを定義
}
- Proxyは遅い - JavaScriptのProxyは便利だが、メソッド呼び出しごとにオーバーヘッドがある
- 明示的な方が速い - .get()などを直接定義すると、V8エンジンが最適化しやすい
Proxyを使った場合にV8エンジン内部で起きていること
- app.getにアクセス
- Proxyハンドラーを探す
- getハンドラーを実行
- propが"get"だと確認
- target.constructor.prototype.hasOwnProperty("get")をチェック
- falseなので、elseブロックへ
- args.lengthをチェック
- target.addRouteを実行
修正後のV8エンジン内部で起きていること
- app.getにアクセス
- getメソッドを直接実行
- this.addRoute('GET', ...)を実行
URL解析方法の変更
変更前:
const getPathFromURL = (url) => {
url = new URL(url)
return url.pathname
}
変更後:
const getPathFromURL = (url) => {
// XXX
const match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/)
return match[5]
}
- new URL()は重い - URLオブジェクトの生成はコストが高い
- 正規表現の方が速い - 必要なのはpathnameだけなので、正規表現で直接取り出す
- コメントは「これは一時的なハック」という意味(後で改善される可能性がある)
src/node.js
クラスから関数にしたら早くなるの??なんで??そうなん??
コンパイルはclassの方が物量多くなるから純粋にボリュームでclassの方が遅いんだっけ。
変更前:
class Node {
constructor({ method, label, handler, children } = {}) {
this.label = label || ''
// ...
}
insert(method, path, handler) { /* ... */ }
}
変更後:
function Node(method, handler, children) {
this.children = children || {}
this.method = {}
// ...
}
Node.prototype.insert = function(method, path, handler) { /* ... */ }
なんでか聞きまくってたらCalude Codeくんが実際ベンチマーク測ってくれた。すごい。
-
new ClassNode() と new FunctionNode() を100万回実行して、どちらが速くインスタンスを作れるか測定する
- Class: 7.63ms
- Function: 3.57ms
-> Functionの方が2倍近く速い
-
既に作ったNodeインスタンスに対して、search()メソッドを100万回呼び出す
- Class: 60.44ms
- Function: 60.83ms
-> ほぼ同じ
-
ルーティングツリーに新しいパスを1万個追加する
- Class: 3.814秒
- Function: 4.000秒
-> ほぼ同じ
-
分割代入 vs 通常の引数100万回試行
- 分割代入 + デフォルト引数: 10.389ms
- 通常の引数: 2.435ms
-> 圧倒的通常の引数
分割代入結構コストかかるのか。そりゃそうか。渡すだけと処理が必要なものは違うよね。
ループの最適化
変更前:
for (const p of splitPath(path)) {
// for...of ループ
}
変更後:
const parts = splitPath(path)
for (let i = 0; i < parts.length; i++) {
const p = parts[i]
// 従来のforループ
}
- 従来のforループの方が速い - for...ofはイテレータを使うのでオーバーヘッドがある
- 配列インデックスアクセスが最速 - V8エンジンが最も最適化しやすい
#8 feat(doc): add a simple readme to help new user get start
お、初めてのコントリビューターからのPR。metrueさん。
READMEに新規ユーザーが使い始めるのに役立つ簡単な説明の追加と、package.jsonにコマンドをいくつか追加。
#9 feat(ci): setup github action to enable ci
これも#8と同じmetrueさんからの提案PR。
.github/workflows/ci.ymlの新規追加。
もっと競プロとかやんないとダメかも。なんとなくの知識しかないから調べないとコードの変更の意図が読むだけじゃわかんない。細部まで速さ求めててすごい。