第2部では「何であるか」を見ました。ここでは「どう動いているか」を見ます。
挙動の理由がわかると、AI が出したコードの誤りを見抜けるようになります。
Node の挙動のほぼすべてが、この一点から説明できます。
職員が1人しかいない事務所
A社の件で、年金事務所へ照会を出しました。返事が来るのは30分後です。
従来型のやり方は「電話の前に座って30分待つ」です。その間、B社もC社も手つかずになります。3件を同時に進めたければ、職員を3人雇うしかありません。
Node のやり方は「照会を出したら、すぐB社の書類に取りかかる」です。年金事務所から連絡が入ったら、そこで手を止めてA社の続きに戻る。職員は1人のまま、3件が並行して進みます。
ただし手は1組しかありません。3時間かかる集計を始めたら、その間はB社もC社も一切進みません。照会と違って、集計は自分の手が塞がるからです。これが「重い計算に弱い」ということの中身です。
Node は JavaScript を1本のスレッドで実行します。スレッドとは「作業を進める筋道」のことで、1本しかないというのは、上のたとえでいう職員が1人という状態にあたります。つまり、同時に2つの処理を走らせていません。
にもかかわらず、何千という同時接続をさばけます。なぜか。待つのをやめたからです。
従来型のサーバーは「データベースに問い合わせて、返事を待つ」間、そのスレッドが停止します。同時に100人来れば、スレッドを100本立てることになります。
Node は違います。問い合わせを出したら、返事を待たずに次の仕事へ行きます。返事が来たら「終わりました」と通知が入り、そこで初めて続きを処理する。この方式を非同期(asynchronous)と呼び、通知を受け取って順番に処理する仕組みをイベントループと呼びます。
料理人にたとえるなら、湯を沸かしている間に別の下ごしらえをする一人の料理人です。人を増やしたのではなく、待ち時間を使い切っている。
| 仕事の性質 | Node の適性 | 理由 |
|---|---|---|
| 待ち時間が長い(DB、API、ファイル、通信) | 非常に得意 | 待ち時間を他の仕事で埋められる |
| CPU を長時間使う(画像変換、大規模計算) | 苦手 | 1本しかないスレッドを占有してしまう |
後者が重要です。重い計算を始めると、その間すべてのリクエストが止まります。1本のスレッドが塞がるからです。Web サーバーとしては致命的になりえます。
「Node は速い」「いや遅い」という議論が噛み合わないのは、両者が別の仕事の話をしているためです。仕事の性質を指定しないと、どちらの主張も正しくなります。
逃げ道は3つあります。別プロセスに出す(Python スクリプトを呼ぶなど)、worker_threads を使う(Node に別スレッドを立てる仕組みがあります)、外部サービスに委ねる。
第3部で見た「Excel の処理は Python で」という判断も、実はこの延長線上にあります。適材適所を、言語ではなく仕事の性質で決める、ということです。
前章の非同期を、コード上で表現するための記法です。
await は「止まる」ではなく「順番を譲る」async が付いた関数は、結果ではなく引換券を返すforEach の中の await は効かない。ここだけは覚える価値があるクリーニングの引換券
服を預けると引換券をもらいます。券そのものは服ではありません。「あとで服と交換できる」という約束です。
async が付いた関数は、必ずこの引換券を返します。中身がまだ無くても、券はその場で即座に出ます。この券のことを Promise と呼びます。
await は、その券を出して実物を受け取る操作です。ただし窓口で突っ立って待つのではありません。「できたら呼んでください」と言って、他の用事を済ませに行く。呼ばれたら戻ってくる。
前章の「照会を出したら別の仕事へ行く」が、コードの上ではこの形で書かれます。
まず言葉から。await は「待つ」と読めますが、実際の意味は「ここで一旦、他の仕事に順番を譲る」です。スレッドを止めているのではありません。
async が付いた関数は、必ず Promise というものを返します。Promise は「結果はまだ無いが、いずれ出る」という引換券のようなものです。await は、その引換券を実物と交換する操作にあたります。
| 世代 | 書き方 | 問題 |
|---|---|---|
| コールバック | 関数を引数として渡し、終わったら呼んでもらう | 入れ子が深くなり読めなくなる |
| Promise | .then() でつなぐ | まだ縦に長い |
| async / await | 上から下へ普通に書ける | ─(現在の標準) |
古い記事に出てくる .then() の連鎖や、関数を引数に渡す書き方は、すべて同じことを別の記法で表現しています。中身は変わっていません。
AI が生成したコードでも実際に出ます。知らないと絶対に気づけない種類のバグです。
forEach は中の関数が非同期かどうかを見ていません。渡された関数を呼びっぱなしにして、次へ進みます。
なぜそうなるのか。forEach は「渡された作業を順に呼び出す」だけの仕組みで、非同期という考え方が JavaScript に入る前に作られました。だから中の関数が引換券を返しても、それを見ずに捨てます。「呼んだから終わり」と判断して、次の1件へ行ってしまう。
たとえでいえば、8人分のクリーニングを預けて引換券を8枚受け取り、その券を全部その場に置いたまま帰った状態です。服は受け取れていません。
下の Promise.all は、前章の非同期の利点をそのまま使う書き方です。100件のデータベース問い合わせを1件ずつ待てば100件分の時間がかかりますが、まとめて投げれば、最も遅い1件の時間で済みます。
AI が書いたコードを読むとき、async が付いている関数の呼び出しに await があるかを確認してください。付け忘れると、エラーも出ずに「途中の状態で先に進む」という、最も追いにくい不具合になります。
TypeScript を使い、リンタを入れていると、この種の付け忘れは機械が指摘してくれます。第2部で TypeScript を勧めた実質的な理由がこれです。
「import できない」というエラーの正体です。
JavaScript はもともとブラウザの中で小さな仕掛けを動かすための言語で、ファイルを分割する仕組みを持っていませんでした。1つのファイルに全部書く前提だったからです。
第2部で見たとおり、2009年に Node が JavaScript をブラウザの外へ持ち出しました。サーバー用のプログラムは大きくなるので、分割の仕組みがどうしても必要になります。そこで Node が独自に作ったのが CommonJS です。
その後、JavaScript 言語の側が公式の仕組み(ES Modules)を定めました。後から標準ができてしまったため、両方が残っています。
第1部で見た「歴史的な経緯がそのまま設計に残る」の一例です。技術的な優劣の問題ではなく、順番の問題です。
その2系統を並べると、こうなります。
| CommonJS(CJS) | ES Modules(ESM) | |
|---|---|---|
| 読み込み | const fs = require('fs') | import fs from 'fs' |
| 公開 | module.exports = ... | export default ... |
| 出自 | Node が独自に作った旧方式 | JavaScript 言語の正式な標準 |
| 拡張子 | .cjs | .mjs |
.js というありふれた拡張子がどちらとして扱われるかは、package.json の記述で決まります。
第3部で作った検証用スクリプトを check.mjs という名前にしたのは、この設定に依存せず確実に ESM として動かすためです。
Cannot use import statement outside a module ─ ESM の記法を CJS として読み込もうとしている。.mjs にするか "type": "module" を足す。
require() of ES Module ... not supported ─ 逆方向。ESM のパッケージを古い記法で読もうとしている。import に書き換える。
近年の Node は両者の相互運用を改善しており、以前ほど頻繁には詰まりません。ただしネット上の記事は両方の時代のものが混在しているため、コピーしたコードが動かない原因の上位であり続けています。
新規に書くなら ESM で統一するのが素直です。Next.js も ESM 前提で設計されています。
バージョン表記の記号には、はっきりした意味があります。
^ や ~ は「どこまで勝手に新しくなってよいか」の指定package.json は範囲しか書いていない。1点を記録しているのが lock ファイル「最新版の様式でお願いします」
職員に「最新版の様式で作って」と指示したとします。あなたが指示した日と、職員が実際に着手した日の間に様式が改訂されていたら、指示は同じでも出てくる書類は違います。
package.json の ^ はこの「最新版で」という指示にあたります。範囲を示しているだけで、一点を指定していません。
package-lock.json は「令和8年4月1日改訂版」と版を書き留めたメモです。これがあれば、誰がいつ作っても同じ書類が出てきます。
まずバージョン番号の読み方です。semver(セマンティック・バージョニング)という取り決めがあります。「意味を持たせた版番号の付け方」という程度の意味です。
| 位置 | 名称 | 上がるとき |
|---|---|---|
1.2.3 | メジャー | 互換性が壊れる変更。要注意 |
1.2.3 | マイナー | 機能追加。既存の使い方は壊れない |
1.2.3 | パッチ | 不具合修正のみ |
package.json に付く記号は、どこまでの自動更新を許すかの指定です。
| 表記 | 意味 | 許容範囲 |
|---|---|---|
^1.2.3 | マイナーまで許す(既定) | 1.2.3 以上 2.0.0 未満 |
~1.2.3 | パッチのみ許す | 1.2.3 以上 1.3.0 未満 |
1.2.3 | 完全固定 | 1.2.3 のみ |
ここで package-lock.json の意味が定まります。package.json は範囲しか書いていません。同じ package.json でも、npm install した時期によって入るバージョンが変わります。
package.json に "next": "^15.0.0" と書いてあるとします。「15.x.x の範囲なら新しいものでよい」という意味です。
先月あなたが npm install したときは 15.2.1 が入りました。今月、別の職員が同じリポジトリで npm install すると、その間に公開された 15.3.0 が入ります。
同じ package.json なのに、手元の中身が違う。15.3.0 に不具合があれば、片方の環境だけ動きません。「私の環境では動くのですが」という状況の典型的な発生源です。
lock ファイルは、実際に入った1点を記録したものです。これがあるから、来月も、別のパソコンでも、Vercel のビルド環境でも、同じものが入ります。
rm -rf node_modules package-lock.json という手順が「再現性を捨てる操作」だと書いたのは、このためです。lock を消すと、範囲の中の別のバージョンが入りうる。「昨日まで動いていたのに」の原因になります。
本番やビルド環境では npm ci を使ってください。lock ファイルどおりに厳密に入れ、範囲の解釈をやり直しません。
第2部で「node_modules が巨大になるのは部品が部品を呼ぶから」と書きました。これを推移的依存と呼びます。自分が選んだのは3個でも、実際に取り込まれるのは数百個です。
ここには実際のリスクがあります。直接選んでいないパッケージが、自分の製品の中で動いているということだからです。過去には、広く使われていた小さなパッケージが悪意ある管理者に渡り、それを使う全プロジェクトに影響が及んだ事例が複数あります。
最後の npm ls が有用です。「このパッケージはどこから来たのか」を辿れます。顧客データを扱うシステムでは、依存を1つ増やす判断に、この確認を挟む価値があります。
Next.js を使ううえで、最も中心的な設計判断です。
弁当屋の4つのやり方
昼食をどう出すか、と考えると、そのまま4方式に対応します。
| 方式 | 弁当屋でいうと | 結果 |
|---|---|---|
| SSG | 朝のうちに全部作って並べておく | 出すのは一瞬。中身は朝のまま |
| ISR | 並べておくが、2時間ごとに作り直す | 速いうえ、そこそこ新しい |
| SSR | 注文を受けてから作る | できたては保証。待たせる |
| CSR | 材料と手順書を渡して、客に作らせる | 店は楽。客の待ち時間が長い |
作り置きは出すのが速い。ただし朝の情報のままです。注文後に作れば最新ですが、待たせます。速さと新しさは交換関係にあり、どちらも取ることはできません。
CSR が「客が自分で作る」なのは、ブラウザに材料(JavaScript)とデータを送って、組み立て作業自体をブラウザにやらせるからです。だから最初の待ち時間が長くなります。
問いは一つです。HTML を、いつ作るか。
| 方式 | 表示速度 | データの鮮度 | 向く用途 |
|---|---|---|---|
| SSG | 最速 | ビルド時点で固定 | 会社案内、ブログ、料金表 |
| ISR | 速い | 指定した間隔で更新 | お知らせ一覧、記事一覧 |
| SSR | やや遅い | 常に最新 | ログイン後の画面、検索結果 |
| CSR | 初回が遅い | 常に最新 | 管理画面、ダッシュボード |
2つの問いで、ほぼ決まります。
そのデータは、何秒古くても許されるか。会社案内なら1日古くても構いません。給与計算の結果なら1秒でも古いと困ります。
誰が見ても同じ内容か。全員に同じものを見せるなら、1回作って使い回せます。人によって違うなら、毎回作るしかありません。
| 古くてよい | 全員同じ | 選ぶべき方式 |
|---|---|---|
| はい | はい | SSG |
| 数分なら | はい | ISR |
| いいえ | いいえ | SSR |
顧問先ごとに違う情報を出す画面は、原理的に SSR です。事前に作り置きできないためです。逆に、事務所の紹介ページを SSR にするのは、毎回同じものを作り直しているだけの無駄になります。
ここが「Vercel の料金が思ったより高い」という事態の主な原因でもあります。SSG で済むページを SSR にしていると、アクセスのたびにサーバーが動きます。
App Router の中心的な考え方であり、最も誤解されやすい部分です。
"use client" と書いたものだけが、ブラウザにも送られる事務所の奥と、受付カウンター
奥の執務スペースがサーバーコンポーネントです。顧客からは見えません。だから金庫も開けられますし、他社の資料を机に広げていても問題ありません。
受付カウンターがクライアントコンポーネントです。顧客と直接やりとりする場所で、ボタンを押す、文字を入力するといった応対はここでしか起きません。
そして決定的な点です。カウンターに置いた書類は、顧客が読めます。だから機密書類をカウンターに置いてはいけない。"use client" のファイルに接続情報を書くとブラウザから読めてしまう、というのは、これとまったく同じことです。
奥からカウンターへ書類を出すことはできます(これが props)。逆に、カウンターから奥の金庫を直接開けることはできません。
Next.js の App Router では、すべてのコンポーネントが既定でサーバー側で動きます。ブラウザには送られません。ブラウザでも動かしたい場合に限り、ファイルの先頭に "use client" と書きます。
サーバー側で動くコンポーネントは、そのコードがブラウザに一切送られません。ここから3つの利点が出ます。
データベースに直接触れます。API を経由する必要がありません。接続情報や API キーが漏れません。ブラウザに届かないからです。ブラウザに送る JavaScript が減ります。結果として表示が速くなります。
"use client" は「ブラウザだけで動く」という意味ではありません。初回の HTML はサーバー側でも作られ、その後ブラウザでも動き始めます。両方で動くのが正しい理解です。
なぜ両方なのか。もしブラウザだけで組み立てると、JavaScript を読み終わるまで画面は真っ白になります。それを避けるため、サーバー側で一度組み立てた HTML を先に送り、あとから JavaScript が届いて動き出す。この二段構えになっています。
名前が誤解のもとです。"use client" は「クライアント専用」ではなく、正確には「クライアントにも送る」という指定だと読んでください。
だから "use client" を書いたファイルに接続情報を書くと、ブラウザに送られて誰でも読めます。ここは実害が出る箇所です。
実務上の原則は単純です。クライアント側は、必要な最小の葉先だけにする。
ボタンやフォームなど、実際に操作を受け付ける部分だけを "use client" にし、それを囲むページ全体はサーバー側に置く。逆にページ全体をクライアントにすると、サーバーコンポーネントの利点をすべて失います。
渡せるのは、そのまま文字として表現できる値だけです。関数はそのまま渡せません。この制約が、境界の設計を強制します。
ファイル名そのものが機能を持ちます。
[ ] ( ) _)にも意味がある第1部の原則1「すべてはファイルである」が、最も直接的に現れている部分です。決まった名前でファイルを置くと、Next.js がそれを見つけて呼びに来ます。
「このURLではこの処理を呼ぶ」と設定ファイルに一覧を書く方式もあります。実際、多くのフレームワークがそうしています。
Next.js がそうしないのは、設定と実体がずれることを防ぐためです。設定ファイル方式では、ファイルを移動したのに設定を直し忘れる、という事故が起きます。二重管理だからです。
名前で決まる方式なら、ファイルを移動すれば URL も一緒に動きます。ずれようがありません。
| ファイル名 | 役割 |
|---|---|
page.tsx | そのURLで表示される本体。これがあって初めてURLになる |
layout.tsx | 配下のページを包む枠。画面遷移しても再描画されない |
loading.tsx | データ待ちの間に表示される。自動で差し込まれる |
error.tsx | 配下でエラーが起きたときの受け皿 |
not-found.tsx | 該当なしのときの表示 |
route.ts | 画面ではなく API として応答する |
フォルダ名にも規約があります。
| 書き方 | 意味 | 例 |
|---|---|---|
[id] | 可変部分。値として受け取れる | /staff/1001 |
[...slug] | 階層をまとめて受け取る | /docs/a/b/c |
(group) | 整理用。URL には出ない | (admin)/staff → /staff |
_folder | ルーティング対象から外す | 部品置き場など |
loading.tsx を置くだけで読み込み中の表示が入るのは、規約が機能を持つということの分かりやすい例です。呼び出すコードを書く必要はありません。第2部で触れたフレームワークの性質 ─ こちらが呼ぶのではなく、向こうが呼びに来る ─ がここに出ています。
route.ts を置けば、そこが API になります。フロント側とバックエンドが同じプロジェクトに同居する、という Next.js の性格がここに現れています。
第3部で作った Python スクリプトのような処理も、route.ts に移せばブラウザから呼べるようになります。「スクリプトから画面へ」の移行が、この1ファイルから始まります。
なぜ Vercel だと素直に動くのか。逆に、離れると何が起きるのか。
Next.js を作っている会社が Vercel である、という事実の実務上の意味を見ておきます。
前章までに出てきた機能のいくつかは、実行基盤の協力がないと成立しません。
| 機能 | 基盤側に必要なもの |
|---|---|
| ISR(一定時間ごとの再生成) | 生成済み HTML を保管し、期限を管理する仕組み |
| 画像の最適化 | 変換処理と、変換結果のキャッシュ |
| SSR | リクエストごとにサーバー処理を走らせる環境 |
| ストリーミング表示 | 途中まで送りながら残りを作る通信方式 |
Vercel はこれらを標準で用意しています。だから git push だけで動きます。フレームワークと基盤を同じ会社が設計しているのだから、当然そうなります。
ここで前の章とつながります。方式によって、基盤にかかる負荷がまったく違うからです。
| 方式 | アクセス1回あたりに起きること | 費用の増え方 |
|---|---|---|
| SSG | できあがった HTML を配るだけ | ほとんど増えない |
| ISR | 基本は配るだけ。期限が来たときだけ作り直す | ゆるやかに増える |
| SSR | 毎回サーバーが動いて組み立てる | アクセス数に比例して増える |
SSG で済むページを SSR にしていると、誰かが開くたびにサーバーが同じものを作り直します。「Vercel の請求が思ったより高い」の主因はここです。
裏返せば、事務所の紹介ページのように内容が変わらないものを SSG に寄せるだけで、費用は下がります。設計判断が直接お金に変わる、数少ない分かりやすい例です。
不可能ではありません。ただし、上の表の各項目を自分で用意することになります。ISR のキャッシュをどこに置くか、画像変換をどう処理するか、といった設定が必要です。
Next.js には出力を1つにまとめる設定があり、それを使って自前のサーバーや Docker で動かせます。とはいえ手間はゼロではない、というのが正直なところです。
第2部第6章に書いた「依存を増やすということは、他人の責任範囲を増やすこと」が、ここでも当てはまります。
Vercel に寄せれば運用の手間が消えますが、その分だけ自分でコントロールできない部分が増えます。第3部で見た Gemini CLI の一件と、構造は同じです。
判断材料は用途で分かれます。事務所のサイトや社内ツールなら、寄せたほうが合理的です。顧問先のデータを長期に預かるシステムなら、移せる形にしておく価値が出てきます。
第4部で見たことは、結局こういう形にまとまります。
| 問い | 答え |
|---|---|
| Node が同時接続に強い理由 | 1本のスレッドで、待ち時間を他の仕事で埋めているから |
| Node が重い計算に弱い理由 | 同じ理由。1本しかないスレッドが塞がるから |
await の意味 | 止まるのではなく、順番を譲る |
| lock ファイルが要る理由 | package.json は範囲しか書いていないから |
| レンダリング方式の選び方 | データが何秒古くて許されるか、全員同じ内容か |
"use client" の意味 | ブラウザ専用ではなく、両方で動くという指定 |
どれも、暗記する種類のものではありません。理由がわかっていれば、その場で導けるものばかりです。そして理由がわかっていると、AI が出したコードの誤りに気づけます。第2部の冒頭に書いた「出てきたものを疑う力」は、こういう形で実装されます。