- SSRにNode.jsが選ばれる理由は「JavaScriptが書けるから」ではありません
- SSRリクエストの実際の流れ
- スレッド型サーバの問題
- Node.jsのイベントループ
- SSRとイベントループの相性
- よくある誤解
- SSRでNode.jsを使う際の注意点
- SSRサーバのボトルネックはレンダリングではない
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性能ではなく、どれだけ同時に待てるかで決まります。