Next.js production server in Go (no JavaScript runtime)
こちらの投稿は 12/3 の Go 言語と Next.js の Advent カレンダー用になります。
背景
- Next.js のパフォマンスは Go に追いついていないためサーバーをより速くしない
- deno や bun なども試しましたが数倍違う程度で、Go とは数百倍の違いがある
- Go で DB アクセス含むリクエストよりも Next.js でページ作るのが遅い場合がある
- メモリー使用率を減らしたい、など
前提と本記事の範囲
- Next.js サーバーといっても全ての機能を対応できるものではないため以下の技術のみになります。
- getServerSideProps を Go で処理する (HTMLのBodyのレンダリングはクライアント)
- HTMLの Head 部分のレンダリングを Go で処理する(SEOのためサーバーサイドで処理)
- title や meta タグ、各種 script、css などの挿入
-
__NEXT_DATA__
オブジェクトを挿入
- Next.js pages のルーティングを Go で行う(app router は本記事では対応しておりません)
- SSG(Server Side Generation)、こちらはただのファイルサーバー的なものなため詳細の説明は省きます。
-
_next/static
配下のファイルサーバー(.css, .js集)、上記同様
HTML 構成
まずは HTML 構成をみてみましょう
<!DOCTYPE html>
<html lang="ja"> <!-- _document.tsx/jsx 参照 -->
<head>
<!-- 共通ヘッダー -->
<meta charSet="utf-8"/>
<meta name="viewport" content="width=device-width"/>
<meta name="next-head-count" content="2"/>
<!-- .css ファイルなど ->
<noscript data-n-css=""></noscript>
<!-- .js ファイルなど ->
</head>
<body> <!-- _document.tsx/jsx 参照 -->
<div id="__next">
<!-- ここはクライアント側でレンダリングする -->
</div>
<script id="__NEXT_DATA__" type="application/json">{
"props": {
"pageProps": /* getServerSideProps の結果の JSON */
}
// ...
}
</script>
</body>
</html>
<div id="__next">
の中身以外はサーバー側で生成可能です。
生成方法
- 最初のステップとして next build します。
- 成果物のフォルダ(通常 .next フォルダ) から以下の3つのファイルが必要になります。
- BUILD_ID: ただのランダムなIDですが、データルート
_next/data
や アセット_next/static
のパスに含まれるIDになります。(テキストファイル) - build-manifest.json
- 各ページ内に含まれるスクリプトやCSSのパスが記述されているJSONファイル
- routes-manifest.json
- ルーティングに利用するパスとページの詳細が記述されているJSONファイル
- BUILD_ID: ただのランダムなIDですが、データルート
- build-manifest.json を Go の構造体に読み取ります。
type BuildManifest struct {
PolyfillFiles []string `json:"polyfillFiles"`
LowPriorityFiles []string `json:"lowPriorityFiles"`
Pages map[string][]string `json:"pages"`
}
- routes-manifest.json も Go の構造体に読み取ります。
type RouteManifest struct {
StaticRoutes []map[string]string `json:"staticRoutes"`
DynamicRoutes []map[string]string `json:"dynamicRoutes"`
DataRoutes []map[string]string `json:"dataRoutes"`
}
-
ルーティング
-
Next.js のページのルーティングは StaticRoutes または DynamicRoutes に含まれます。
- 基本的にはページ名に変数がある場合
[pageId]
はDynamicRoutes、それ以外は StaticRoutes に記述されています。
- 基本的にはページ名に変数がある場合
-
StaticRoutes の場合、
{ "page": "/", "regex": "^/(?:/)?$", }
こちらページが
“/”
なため、index.jsx (.tsx) 相当になります。正規表現も^/$
にあたりますのでルートのページを示してます。(?:/)?
はパスの最後のスラッシュを無視しているだけです。全ての正規表現に含まれるので気にしないでいきましょう。- Go でこちらのルートをマッチする場合は、http.Request オブジェクトの RequestURI や URL.Path 利用して上記の正規表現でマッチことが可能です。以下 net/http の URL構造体
type URL struct { //... Path string // path (relative paths may omit leading slash) //... }
-
DynamicRoutes の場合、
{ "page": "/help/[pageId]", "regex": "^/help/([^/]+?)(?:/)?$", "routeKeys": { "nxtPpageId": "nxtPpageId" }, "namedRegex": "^/help/(?<nxtPpageId>[^/]+?)(?:/)?$" }
こちらも正規表現(2つも)が用意されているのでパターンマッチすればページのパラメータの pageId を取得することが可能です。
nxt
という Prefix が付けられていますが、固定なため特に処理には影響ないと思います。 -
最後に dataRoutes の場合です。こちらは SSR 時には利用されず、クライアント側でのルーティングが発生するときに呼び出されます(本来の Next.js では getServerSideProps が呼ばれます)
{ "page": "/", "dataRouteRegex": "^/_next/data/BUILD_ID/index.json$" }
こちらはトップページをクライアント側でルーティング時に呼び出されます(index.json)
実際の中身は getServerSideProps の結果を一層上に pageProps としてラップした状態で返せばよいのです。
{ "pageProps": { /// getServerSideProps の結果のJSON }, "__N_SSP": true }
ここの
__N_SSP
は SSR の意味します。ここのルーティングはパース仕方としては
/_next/data/BUILD_ID/
を省けば index.json に当たるので page の “/” とマッピングすればどのデータを渡せばよいかわかります。Echo フレームワークは場合、こんな感じでデータルートはハンドラーをかけます。
e := echo.New() // ... dataHandler := func(c echo.Context) error { // Go での処理 } e.GET("/_next/data/*", dataHandler) e.HEAD("/_next/data/*", dataHandler)
SSGの場合、HTTP メソードの HEAD のみまたは HEAD / GET 両方のリクエスト来る場合があるので、どちらも対応します。
-
-
ルーティングの情報が揃ったところで、サーバー側でHTMLを作成します。
_document.jsx (.tsx)
の箇所は Go で処理することになります。- DOCTYPE や <html>, <head> タグの生成
<!DOCTYPE html><html lang="ja"><head>
- buildManifest の poliFills を書き出す
- buildManifest の
_app
という名前のリストから css / js を書き出す(全てのページ共通) - ルーティングで決まった該当ページのスクリプトを書き出す
- buildManifest の
LowPriorityFiles
を書き出す。(基本的に_buildManifest.js
と_ssgManifest.js
2つ、全ページ共通) -
</head>
を閉じて、</head><body>
HTML Body と__next
という IDの DIVタグを空で書き出す。<div id="__next"></div>
-
__NEXT_DATA__
を書き出す<script id="__NEXT_DATA__" type="application/json">
- script と HTMLを閉じて終わり
</script></body></html>
- DOCTYPE や <html>, <head> タグの生成
-
後は React がクライアント側でいい感じに Hydration してくれて、ページが描画されます。
パフォマンス測定
以下の JSXの例。(本来の Next.js / Node.js の場合)
export async function getServerSideProps(context) {
const res = await fetch('http://127.0.0.1:9090/q/index', {
method: "POST"
});
const repo = await res.json();
return {
props: repo
};
}
export default function Page(props) {
return (
<main className={`min-h-screen`}>
<h1 className="text-3xl">Blogs</h1>
<ul className="list-disc">
{props.blogs.map(x => (
<li key={x.blogId}>{x.title}</li>
))}
</ul>
</main>
)
}
Next.js のベンチマーク結果(約 630 RPS)
Running 10s test @ http://127.0.0.1:4040/
9 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 307.34ms 64.73ms 662.65ms 87.52%
Req/Sec 73.83 40.32 212.00 65.32%
Latency Distribution
50% 282.40ms
75% 316.73ms
90% 382.98ms
99% 570.92ms
6337 requests in 10.06s, 18.12MB read
Requests/sec: 629.91
Transfer/sec: 1.80MB
バックエンドだけパフォマンス (約 43万RPS)
Running 10s test @ http://127.0.0.1:9090/q/index
9 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 511.70us 488.87us 42.75ms 87.86%
Req/Sec 48.48k 4.75k 160.57k 97.47%
Latency Distribution
50% 423.00us
75% 714.00us
90% 1.05ms
99% 1.86ms
4378918 requests in 10.10s, 5.67GB read
Requests/sec: 433569.72
Transfer/sec: 575.16MB
Go で HTML レンダリングして返す場合のパフォマンス(約 39 万RPS)
Running 10s test @ http://127.0.0.1:9090/go/
9 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 563.99us 484.10us 14.08ms 77.21%
Req/Sec 43.79k 3.61k 132.80k 97.91%
Latency Distribution
50% 470.00us
75% 793.00us
90% 1.17ms
99% 2.04ms
3960649 requests in 10.10s, 6.37GB read
Requests/sec: 392169.99
Transfer/sec: 646.28MB
おまけに
今回 HTML のボディーのレンダリングはクライアント側で行っていますが、将来的には Rust や Go などでもできるようになればいいと思います。Next.js 内部で使っている Rust 製の swc とかはJSX / TSX をちゃんと理解していますし、実際変換できるじゃないかと思ってます。 例えば以下の JSX は 普通の JSに変更可能です。
export default function Page(props) {
return (
<main className={`min-h-screen`}>
<h1 className="text-3xl">Blogs</h1>
<ul className="list-disc">
{props.blogs.map(x => (
<li key={x.blogId}>{x.title}</li>
))}
</ul>
</main>
)
}
JS
export default function Page(props) {
return React.createElement("main", {
className: `min-h-screen`
}, React.createElement("h1", {
className: "text-3xl"
}, "Blogs"), React.createElement("ul", {
className: "list-disc"
}, props.blogs.map((x) => React.createElement("li", {
key: x.blogId
}, x.title))));
}
相当のコードは Go でも記述可能です。
func Page(props *PageProps) string {
return react.CreateElement("main", attrs{
"className": "min-h-screen",
},
react.CreateElement("h1", attrs{
"className": "text-3xl",
}, "Blogs"),
react.CreateElement("ul", attrs{
"className": "list-disc",
}, props.Blogs.Map(func(x *Blog) string {
return react.CreateElement("li", attrs{
"key": x.BlogId,
}, x.Title)
}),
),
)
}
今回はここまでとします。最後まで読んでいただきありがとうございました。
Discussion