◆ Fastify も他のフレームワーク同様 http.Server を中で使ってる
◆ なのに http.Server で普通に書いたコードよりも速い
◆ JSON.stringify と res.setHeader が遅いことが原因

Fastify が速い

速度重視の Framework の Fastify の Readme を見るとベンチマーク結果が載っています
https://github.com/fastify/fastify

FrameworkVersionRouter?Requests/sec
Express4.17.115,978
hapi19.1.045,815
Restify8.5.149,279
Koa2.13.054,848
Fastify3.0.078,956
http.Server12.18.270,380

Fastify 速いですね
Express が hapi より遅いのは意外でした
Koa は本体だけだと Express 以上に何もしないので その分速めです

よくみると驚きなのが http.Server より Fastify が速いというところ
http.Server はフレームワークを使わない Node.js 自体の API です
Koa や hapi はこれを中で使っています
これより速いということは Fastify は http.Server は使わずに net などを使って ソケット通信部分を直接操作しているのでしょうか?

http.Server を使ってない?

Fastify のソースコードを見た限りでは 特に設定をしなければ http.createServer でサーバを作成しています
つまり http.Server を使っています

https://github.com/fastify/fastify/blob/v3.7.0/lib/server.js#L14-L36
  } else {
server = http.createServer(httpHandler)
server.keepAliveTimeout = options.keepAliveTimeout
}

ベンチマークコードの問題?

同じ http.Server を使っているのに いろいろな機能がある Fastify のほうが速いというのはよくわかりません
http.Server 側のベンチマークコードで余計なことをしていたりとかでしょうか?

ベンチマークのコードを見てみます

まずは Fastify の方です

https://github.com/fastify/benchmarks/blob/master/benchmarks/fastify.js
'use strict'

const fastify = require('fastify')()

const schema = {
schema: {
response: {
200: {
type: 'object',
properties: {
hello: {
type: 'string'
}
}
}
}
}
}

fastify.get('/', schema, function (req, reply) {
reply.send({ hello: 'world' })
})

fastify.listen(3000)

「{hello: "world"}」 という JSON を返すだけのようです

対して http.Server 側は

https://github.com/fastify/benchmarks/blob/master/benchmarks/bare.js
'use strict'

const server = require('http').createServer(function (req, res) {
res.setHeader('content-type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ hello: 'world' }))
})

server.listen(3000)

特に余計なことをしてるようには見えません
use strict も両方についていますし……

計測してみる

もしかして 表の数値は参考値程度で バージョンアップのときに別々に計測して更新していたりとか?

ベンチマークのリポジトリも npm パッケージとして公開されていてインストールできるのでインストールしてみます
https://github.com/fastify/benchmarks
https://www.npmjs.com/package/fastify-benchmarks

benchmark コマンドを実行すると実行するものを選べるので bare (http.Server) と Fastify を実行しました

結果は…… Fastify の方が速かったです

リクエストごとの処理量の差

同じ環境で測っても Fastify が速いことが確かめられましたが 謎は深まるばかりです
http.Server のコードで リクエストごとに処理される部分はこれだけです

function (req, res) {
res.setHeader('content-type', 'application/json; charset=utf-8')
res.end(JSON.stringify({ hello: 'world' }))
}

対して Fastify の場合に reply.send 関数を実行するまでに Fastify がリクエストごとに毎回処理している内容をスタックトレースで表すと

Object.<anonymous> (c:\tmp\fastify\index.js:21:3)
preHandlerCallback (c:\tmp\fastify\node_modules\fastify\lib\handleRequest.js:124:28)
preValidationCallback (c:\tmp\fastify\node_modules\fastify\lib\handleRequest.js:107:5)
handler (c:\tmp\fastify\node_modules\fastify\lib\handleRequest.js:70:7)
handleRequest (c:\tmp\fastify\node_modules\fastify\lib\handleRequest.js:18:5)
runPreParsing (c:\tmp\fastify\node_modules\fastify\lib\route.js:391:5)
Object.routeHandler [as handler] (c:\tmp\fastify\node_modules\fastify\lib\route.js:349:7)
Router.lookup (c:\tmp\fastify\node_modules\find-my-way\index.js:367:14)
Server.emit (events.js:203:13)"

となっていました
スタックトレースなので下の行の関数が上の行の関数を呼び出しています

一番上の index.js の 21 行目が reply.send の処理です
一番下の Server.emit は request イベントの emit です
ここで呼び出されている Router.lookup 関数は http.createServer(listener) の listener で渡す関数です
http.Server の場合は ここで呼び出す関数が 上に書いた中身が 2 行の関数で この 2 行しか実行していません

これだけではなく Fastify の方はここから reply.send 実行時の処理や ルートのハンドラ関数呼び出し後の処理が残っています
こんなにルーティングなど多くの処理が入っているのに Fastify の方が速いです

JSON.stringify?

http.Server 側での処理を見ると ヘッダーのセットとボディの送信だけです
これで Fastify より遅くなりそうなところというと JSON.stringify くらいでしょうか?

Fastify では事前にスキーマ定義を行っているので Object.entries や for-in みたいな処理はなくて良さそうです
とは言え JSON 化する対象は 「{ hello: "world" }」 と とても小さなデータです
それに JSON.stringify に指定するオプションもありません
JSON.stringify は組み込み関数なので JavaScript で for-in などを実行するよりは遥かに速いはずです
それでここまで差が出るものでしょうか?

とりあえず Fastify のベンチマークコードから schema 定義を消して JSON.stringify を使うようにしました

reply.send(JSON.stringify({ hello: 'world' }))

これで再度比較してみます


結果は…… 差がほとんどなくなりましたが まだ Fastify の方が少し勝っている状態です

setHeader

Fastify 側のスキーマ定義をなくしたので Content-Type が application/json になっていません
http.Server の方では

  res.setHeader('content-type', 'application/json; charset=utf-8')

がある分 遅いということでしょうか

ですが Fastify の内部処理を見ると Content-Type の有無を確認してなければデフォルトの 「text/plain; charset=utf-8」 を指定しています
https://github.com/fastify/fastify/blob/v3.7.0/lib/reply.js#L141

どちらも header を送っているのになぜか Fastify が速いです


もう少し調べてみると Fastify はヘッダーの書き込みに res.writeHead を使っています
Node.js の http モジュールのコードを見た感じでは res.setHeader を使うほうが res.writeHead よりも処理は多そうです
https://github.com/nodejs/node/blob/master/lib/_http_server.js
https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js

ドキュメントによると setHeader を使わず writeHead を呼び出すと直接レスポンスを書き込むので内部的にキャッシュされないようです
キャッシュされないと getHeader で取得したり 後から変更できないので これらの機能を使う可能性があるなら setHeader を使うほうが良いみたいです
そういった仕組みを通さない分 writeHead だけのほうが速度的には速いようです

Fastify を使うならヘッダーは Fastify 側で管理されていて Node.js 側でキャッシュは不要なので writeHead で問題ありません
とは言っても ヘッダーの管理は Fastify 側で同じようなことをやってるので 生の Node.js と比べてそこまで速度は変わらないと思うのですけど

とりあえず http.Server のベンチマークコードから setHeader をなくして再比較をしてみたところ…… http.Server の方が Fastify よりも速くなりました
色々工夫されてるんですね

JSON 化の速度

Fastify の方が速い理由は JSON 化の速度とヘッダー書き込み方法の違いということがわかりました
Fastify のいろいろな処理があっても 生の Node.js より速くなるのですから速度差が大きい場所です
特に JSON.stringify では大きく速度が変わりました

Fastify の JSON 化の処理は fast-json-stringify というパッケージで行われています
JSON.stringify より 2 倍速いと主張していますね
特に small payload で有効なようです
JSON.stringify は呼び出しコストが大きいのでしょうか?

Fastify では事前にスキーム定義で型やプロパティ名を指定しているので ほぼ単純なに文字列結合と考えると速いのは納得です
極端に言うとこういうことですからね

const fn1 = (obj) => `{"hello": "${String(obj.hello)}"}`
const fn2 = (obj) => JSON.stringify(obj)

hello プロパティだけが入ったオブジェクトを fn1 と fn2 に渡して実行するときにどっちが速いかというと fn1 でしょう
一応どれくらいの差があるのか試しに実行してみました

const fn1 = (obj) => `{"hello": "${String(obj.hello)}"}`
const fn2 = (obj) => JSON.stringify(obj)

const measure1 = (value) => {
console.time(1)
for (let i = 0; i < 10_000_000; i++) {
fn1(value)
}
console.timeEnd(1)
}

const measure2 = (value) => {
console.time(2)
for (let i = 0; i < 10_000_000; i++) {
fn2(value)
}
console.timeEnd(2)
}

const value = { hello: "wolrd" }
measure1(value)
measure2(value)
measure1(value)
measure2(value)
measure1(value)
measure2(value)
1: 108.867ms
2: 3896.154ms
1: 94.697ms
2: 3979.037ms
1: 92.277ms
2: 4003.761ms

40 倍くらい違ってますね
このコードではエスケープを考慮していないので 実際にはもう少し差は縮まるはずです

それでも思っていた以上に JSON.stringify は遅いようです
個人的に JSON.stringify はけっこう多用しているのですが ちょっとしたものなら自分で JSON 作ったほうがいいのかなと思い始めました
Fastify で使われてる JSON 化ライブラリを使うのもありですね
スキーム定義が型定義みたいで書くのが面倒ですけど あったほうがいいと思えるところもあります

API のレスポンスの場合 ルートの処理で得られたオブジェクトをそのままクライアント側に返したいのに クライアント側に見せたらダメなデータが含まれていたり 余分なデータがあってレスポンスサイズが大きくなっていたりすることがあります
それらを除外してからレスポンスに指定するわけですが それも面倒ですし 後から修正を加えたときに別の return ではフィルタ処理が抜けていたり ということがありそうです
Fastify のスキーマ定義では 指定していないプロパティは無視されてレスポンスの JSON に含まれません
レスポンスフォーマットがリクエストの内容によって色々変化するものでなければ JSON 化のところでフィルタしてもらったほうが便利かもしれません

あとスキーマ定義は JavaScript コードで書かれているので それを元に API ドキュメントを作るようなこともできそうです