この記事では、npm i(npm install)が内部で何をしているのかを、
「普段なんとなく使ってる人」向けに できるだけ丁寧に・深掘りして 解説します。
この記事の対象読者
- npm i を毎日打ってるけど、正直よく分かってない
- package.json / package-lock.json の違いが曖昧
- node_modules がなぜあんなに重いのか気になる
- CI や Docker での npm i の挙動を理解したい
npm i でまず起きること(全体像)
npm i を実行すると、ざっくり以下の流れが走ります。
- package.json を読む
- package-lock.json を読む(あれば)
- 依存関係ツリーを解決する
- パッケージをダウンロードする
- node_modules に配置する
- ライフサイクルスクリプトを実行する
ここから一つずつ細かく見ていきます。
① package.json を読む
npm はまず package.json を読みます。
{
"dependencies": {
"react": "^18.2.0"
}
}
ここで見ている主な項目
- dependencies
- devDependencies
- optionalDependencies
- peerDependencies
- scripts
- engines(node のバージョン制約)
📌 まだこの時点ではインストールは始まっていないです。
② package-lock.json を読む(超重要)
package-lock.json がある場合、npm はこちらを 最優先 で使います。
なぜ lock ファイルが重要?
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-..."
}
ここには
- 正確なバージョン
- 取得元 URL
- 改ざん検知用ハッシュ
- 依存の依存(孫依存)
がすべて固定で書かれています。
📌 npm i = 「lock ファイル通りに再現する」コマンド
③ バージョン解決(^ や ~ はどう扱われる?)
package.json では、次のような指定をよく見かけます。
"react": "^18.2.0"
この ^ や ~ は バージョン範囲指定 と呼ばれ、
npm が「どこまでのバージョンを許可するか」を判断するためのルールです。
npm はこれを semver(Semantic Versioning) という考え方に基づいて解釈します。
semver(Semantic Versioning)とは?
バージョン番号は基本的に次の形をしています。
MAJOR.MINOR.PATCH
例: 18.2.0
それぞれの意味は以下の通りです。
| 要素 | 意味 | 変更内容 |
|---|---|---|
| MAJOR | メジャー | 破壊的変更(API が壊れる) |
| MINOR | マイナー | 機能追加(後方互換あり) |
| PATCH | パッチ | バグ修正 |
📌 npm は 「MAJOR が変わる=壊れる可能性が高い」 と判断します。
^(キャレット)の意味
"react": "^18.2.0"
これは npm 内部では次のように解釈されます。
18.2.0 以上、19.0.0 未満
つまり:
- 18.2.1 → OK
- 18.9.0 → OK
- 19.0.0 → NG
^ は次の意味を持っています。
「MAJOR を固定したまま、最新を使いたい」
^18.2.0
↑
MAJOR を固定
React のように
メジャーバージョンアップで破壊的変更が起きやすいライブラリでは、
この指定がもっとも一般的です。
~(チルダ)の意味
"react": "~18.2.0"
これは次の範囲を意味します。
18.2.0 以上、18.3.0 未満
つまり:
- 18.2.1 → OK
- 18.2.9 → OK
- 18.3.0 → NG
~ は次のように解釈されます。
~18.2.0
↑
MINOR を固定
👉 「バグ修正だけを取り込みたい」
という、より安全寄りな指定です。
^ と ~ の違いまとめ
| 指定 | 実際の許可範囲 | 特徴 |
|---|---|---|
| ^18.2.0 | < 19.0.0 | わりと積極的 |
| ~18.2.0 | < 18.3.0 | 安定重視 |
| 18.2.0 | 18.2.0 のみ | 完全固定 |
0.x 系ライブラリの特殊ルール
semver では 0.x 系は不安定版扱い です。
"some-lib": "^0.2.3"
この場合、npm は次のように解釈します。
0.2.3 以上、0.3.0 未満
MAJOR が 0 の間は
MINOR アップでも破壊的変更が起きうる
と判断されるためです。
📌 ^0.x は、実質 ~ に近い挙動になります。
npm i 実行時に起きている実際の処理
npm i を実行すると:
- package.json の
^18.2.0を読む - npm registry に存在する全バージョンを確認
- 条件を満たす中で 最も新しいバージョン を選択
- その結果を package-lock.json に固定
package.json : ^18.2.0
実際に入る : 18.2.1
lock に固定 : 18.2.1
📌 次回以降の npm i は ^ や ~ を見ない
📌 package-lock.json の内容をそのまま再現する
「npm i したら勝手にバージョンが上がった」の正体
以下の状態だと、依存関係の再解決が走ります。
- package-lock.json が存在しない
- lock ファイルを削除した
- Git 管理されていない
この場合 npm は:
^ や ~ を再解釈し、
その時点での最新バージョンを選択
という挙動になります。
依存の依存も全部解決する
your-app
└─ react@18.2.0
└─ loose-envify@1.4.0
└─ js-tokens@4.0.0
この ツリー構造の依存関係をすべて解決 します。
📌 ここが npm i の中でも一番頭を使っている部分
④ パッケージのダウンロード
npm は以下の場所からパッケージを取得します。
優先順位
- ローカルキャッシュ(~/.npm)
- npm registry
実体は .tgz
react-18.2.0.tgz
という tar.gz ファイル が落ちてきます。
📌 node_modules に直接落としているわけではない
⑤ node_modules に配置される仕組み
昔(npm v2 以前)
node_modules/
└─ A
└─ node_modules/
└─ B
└─ node_modules/
└─ C
今(npm v3+)
node_modules/
├─ A
├─ B
└─ C
可能な限り フラット化 されます。
なぜnpm iは重い?
- JS ファイル数が多い
- 同じ依存が複数バージョン存在
- OS のファイルシステム負荷
⑥ ライフサイクルスクリプト(npm i が裏で勝手に実行している処理)
npm i を実行すると、
依存関係を入れるだけでなく、スクリプトも自動で実行されます。
これを npm ライフサイクルスクリプト と呼びます。
ライフサイクルとは?
package.json の scripts に書かれた処理のうち、
npm が決められたタイミングで自動実行するものです。
{
"scripts": {
"preinstall": "echo preinstall",
"install": "echo install",
"postinstall": "echo postinstall"
}
}
npm i を実行すると、何も指定していなくても
次の順番で動きます。
preinstall
↓
install
↓
postinstall
📌 ユーザーが npm run しなくても実行されるのがポイント
代表的なライフサイクルスクリプト
| スクリプト | 実行タイミング |
|---|---|
| preinstall | インストール前 |
| install | インストール中 |
| postinstall | インストール後 |
| prepublish / prepare | publish や install 前後 |
| preuninstall / postuninstall | 削除時 |
なぜライフサイクルが存在するのか?
主な理由は以下です。
- ネイティブモジュールのビルド
- 環境ごとの初期セットアップ
- 自動コード生成
代表例が node-gyp を使うライブラリです。
bcrypt
sharp
canvas
これらは:
- npm i
- ↓
- postinstall で C/C++ のビルド
- ↓
- 環境依存のバイナリが生成される
📌 「npm i が遅い」「CI で落ちる」原因の多くはここ
セキュリティ的な注意点
ライフサイクルスクリプトは:
- 任意のコマンドが実行可能
- npm i しただけで動く
つまり理論上は:
"postinstall": "rm -rf ~"
のようなことも可能です。
📌 実際に 悪意のある postinstall が問題になった事例もある
📌 企業環境では --ignore-scripts を使うこともある
npm i --ignore-scripts
「npm i が失敗する」時の典型原因
- postinstall 内で node のバージョンが合わない
- Python / make / gcc が入っていない
- OS 依存のビルド失敗
👉 エラーログに gyp や postinstall が出てきたら、
ほぼライフサイクルが原因
pm ci とは何か?(npm i との正確な違い)
npm ci は npm i と同様に、
package-lock.json に記載された正確なバージョンのパッケージをインストールします。
その点だけを見ると、両者はよく似ています。
しかし、実際の動作には 明確で重要な違い があります。
package-lock.json が必須
npm ci は package-lock.json の存在を前提 としたコマンドです。
- package-lock.json が存在しない
- lock ファイルを削除している
この状態で npm ci を実行すると、即エラーになります。
npm ERR! package-lock.json is required
📌 npm i のように
📌 「なければ再解決して作る」という動きはしません。
package.json と package-lock.json の不整合は許されない
npm ci は、次の状態もエラーとして扱います。
- package.json に依存が追加されている
- しかし package-lock.json が更新されていない
つまり:
package.json と package-lock.json が完全に一致していること
が実行条件です。
依存定義にズレがある
↓
npm ci は即失敗
📌 これは「多少ズレてても直してくれる」
📌 npm i とは真逆の思想です。
node_modules の扱いが決定的に違う
ここが、挙動の一番大きな違いです。
npm i の場合
- 既存の node_modules を残す
- 足りないものだけを追加
- 条件次第で再利用する
👉 柔軟だが、状態に依存しやすい
npm ci の場合
- 既存の node_modules を 完全に削除
- 毎回ゼロからインストール
- 再利用は一切しない
node_modules 削除
↓
package-lock.json 通りに再構築
👉 常にクリーンな状態が保証される
npm ci は何を目的にしたコマンドか
npm ci は一言で言うと:
再現性を最優先したインストール
そのために:
- lock ファイル必須
- 不整合は即エラー
- node_modules は毎回破棄
という 厳しいルール が設けられています。
なぜ CI 環境で npm ci が使われるのか
CI(ビルド・テストの自動実行)では:
- 毎回同じ依存関係であること
- 昨日動いたものが今日も動くこと
が何より重要です。
npm i のように:
- 状態に依存する
- 微妙に結果が変わる
挙動は、CI 環境では致命的になります。
👉 そのため、
- ローカル開発 → npm i
- CI / 自動化 → npm ci
という使い分けが定着しています。
npm i が失敗する典型パターン
よくある原因
- node バージョン違い
- package-lock.json 不整合
- ネットワーク
- postinstall の失敗
デバッグのコツ
npm i --verbose
まとめ(npm i を一言で言うと)
npm i は
「依存関係の完全再現 + 配置 + スクリプト実行」
を一気にやるコマンド
なんとなく打ってた npm i も、
中身を知るとトラブル対応が一気に楽になります。


コメント