SSRでNode.jsが使われる理由とイベントループ

SSRにNode.jsが選ばれる理由は「JavaScriptが書けるから」ではありません

SSR(Server Side Rendering)と聞くと、「フロントエンドと同じ言語が使えるからNode.jsが便利」という説明をよく見かけます。
確かに開発体験としては重要ですが、実運用でNode.jsが採用される本当の理由はそこではありません。

結論を言うと、SSRはI/O待ちが極端に多い処理であり、イベントループ型の実行モデルと相性が良いからです。

SSRは「HTMLを作る処理」ではありません。
実際には、レンダリングの前に大量の非同期処理が走っています。

  • APIリクエスト
  • DBアクセス
  • 認証確認
  • セッション取得
  • キャッシュ確認

つまりSSRはCPU処理ではなく、待ち時間の多いサーバ処理です。ここでNode.jsのイベントループが効きます。

SSRリクエストの実際の流れ

1回のSSRリクエストを分解すると、次の順番になります。

1. HTTPリクエスト到着
2. Cookie解析
3. セッション確認
4. API通信
5. データ整形
6. JSXレンダリング
7. HTML返却

この中でCPUを使うのは最後のレンダリングだけです。
それ以外の大半は「外部待ち」です。

例えばAPIを3つ呼ぶだけで、サーバは数百ms待ちます。
ここでスレッド型サーバだと問題が起きます。

スレッド型サーバの問題

従来のバックエンド(例:PHP-FPMやJavaサーブレット)は、1リクエスト=1スレッドです。

API待ちの間、スレッドは何もしていません。
それでもメモリは確保され続けます。

例えば:

  • 同時接続 1000人
  • API待ち 300ms

1000本のスレッドが待機します。
メモリとコンテキストスイッチが増え、スケールしにくくなります。

SSRは同時アクセスが集中しやすい(特に初回表示)ため、この問題が顕著になります。

Node.jsのイベントループ

Node.jsはスレッドを増やしません。
1つのスレッドでイベントループを回します。

処理の流れは次のようになります。

  • リクエスト受信
  • API呼び出し(非同期)
  • 待機中に別のリクエストを処理
  • API完了時にコールバック実行

つまりNode.jsは「待たないサーバ」です。

app.get('/', async (req, res) => {
  const user = await fetchUser()
  const posts = await fetchPosts()
  res.send(render(user, posts))
})

awaitと書かれていますが、実際にはスレッドは停止していません。
Promiseが解決されるまで、イベントループは他のリクエストを処理します。

SSRとイベントループの相性

SSRの特徴は「短いCPU処理 + 長いI/O待ち」です。
これはNode.jsの得意分野です。

特に次の処理で効果が出ます。

APIフェッチの並列化

const [user, posts, comments] = await Promise.all([
  getUser(),
  getPosts(),
  getComments()
])

Node.jsは3つの通信を同時に待てます。
スレッドを3つ使うわけではありません。

ストリーミングSSR

React 18ではストリーミングレンダリングが導入されました。

  • 先にヘッダを送信
  • データ到着ごとにHTMLを送る

これはブロッキングI/Oでは実装しにくいモデルです。
イベントループ型サーバが適しています。

よくある誤解

Node.jsはCPU処理が遅いのでは?

その通りです。
画像処理や大量計算は苦手です。

しかしSSRは重い計算をほとんど行いません。
テンプレート展開と文字列生成が中心です。ボトルネックは通信です。

つまり、SSRはCPU性能より待ち時間管理が重要です。

JavaやGoではSSRできないのか

できます。実際に行われています。
ただし設計が変わります。

  • スレッドプール調整
  • 非同期I/O
  • コネクション管理

を慎重に行う必要があります。
Node.jsは最初からその前提のランタイムです。

SSRでNode.jsを使う際の注意点

メモリリークがそのまま全ユーザに影響

Node.jsは単一プロセスです。
SSR中にグローバル変数へデータを保持すると、次のリクエストに混入します。

例:

let currentUser

app.get('/', async (req, res) => {
  currentUser = await getUser(req)
})

これはユーザ情報の混線事故になります。

同期処理の危険

fs.readFileSync('big.json')

この処理はイベントループを停止させます。
1人のアクセスが全ユーザを止めます。

SSRでは同期処理を避ける必要があります。

SSRサーバのボトルネックはレンダリングではない

SSRをチューニングするとき、多くの人はReactのレンダリングを疑います。
しかし実際の遅延の多くは次です。

  • 外部API
  • 認証サーバ
  • データベース
  • キャッシュミス

Node.jsが採用されるのはレンダリングが速いからではありません。
待ち時間を効率的に隠せるからです。

SSRサーバはテンプレートエンジンではなく、I/Oオーケストレーターです。
HTMLを生成しているように見えて、実際には複数の非同期処理を調停しています。

SSRとNode.jsの関係を理解する鍵は「レンダリング」ではなく「イベントループ」です。
サーバが速いかどうかはCPU性能ではなく、どれだけ同時に待てるかで決まります。