スマホの Linux(Termux/PRoot)で npm・pnpm・bun の依存インストール時間を測った(5個・30個、各条件5回)
スマホの中の Linux で JavaScript を書くとき、パッケージマネージャーは何を選んでも同じ待ち時間なのか。速いと言われる pnpm や bun は、Android の Termux に PRoot で Ubuntu を入れた環境でも速いのか。
この記事で確かめたのは、非力なスマホ上の Linux で、npm・pnpm・bun に同じ依存を入れるのに何秒かかり、差はどれくらいかだ。同じ package.json(依存の版は固定)を3つに入れ、「キャッシュなしの初回」「キャッシュあり」「lockfile あり」の3条件を、依存5個の小と依存30個の中で各5回測った。測ったのは 2026年10月1日(JST) の1台の端末だけで、ここに出す数字は「この端末・この時期・この依存セット」の値だ。
答えを先に書く。
- 初回(キャッシュなし)は、bun、pnpm、npm の順に短かった。 5回すべてのラウンドで同じ順で、5回の範囲も重ならなかった。中央値は、依存5個で bun 1.55秒・pnpm 4.23秒・npm 8.41秒、依存30個で bun 13.95秒・pnpm 26.88秒・npm 48.26秒だった。
- bun の「キャッシュあり」「lockfile あり」は、この端末では測れなかった。 0.04〜0.9秒で終わり、終了コードも成功だったが、
node_modulesにはファイルが1つも入っていなかった(空のフォルダだけ)。速いのではなく、入っていない。 - キャッシュや lockfile があっても、npm と pnpm の短縮は限られた。 npm の中は約2割短くなり(48.26秒→37.77秒)、小ではほぼ変わらなかった(8.41秒→8.43秒)。pnpm の中もほぼ同じだった(26.88秒→28.41秒)。
原因を測っていないことが多い。本文では、測ったことと、測っていないまま考えていることを分けて書く。
結果(5回の中央値と最小〜最大)
時間はコマンドを起動してから終わるまでの壁時計の時間(秒)。条件の中身は次のとおり。
- 初回: キャッシュ・lockfile・
node_modulesのどれも無い状態から入れる。 - キャッシュあり: 初回で溜まったキャッシュは残し、lockfile と
node_modulesは消して入れる。 - lockfile あり: キャッシュと lockfile は残し、
node_modulesだけ消して、lockfile どおりに入れる(npm ci/pnpm install --frozen-lockfile/bun install --frozen-lockfile)。
依存5個(小)
| 初回 | キャッシュあり | lockfile あり | |
|---|---|---|---|
| npm 12.2.0 | 8.41(7.17〜8.85) | 8.43(7.96〜12.79) | 8.43(7.76〜10.60) |
| pnpm 12.8.1 | 4.23(3.80〜5.01) | 3.28(2.27〜3.62) | 3.51(1.34〜3.59) |
| bun 1.4.2 | 1.55(0.84〜1.78) | 測れず(注) | 測れず(注) |
依存30個(中)
| 初回 | キャッシュあり | lockfile あり | |
|---|---|---|---|
| npm 12.2.0 | 48.26(37.82〜73.28) | 43.41(24.75〜66.36) | 37.77(22.66〜55.43) |
| pnpm 12.8.1 | 26.88(22.25〜33.81) | 26.64(23.63〜31.97) | 28.41(15.26〜33.15) |
| bun 1.4.2 | 13.95(13.46〜14.12) | 測れず(注) | 測れず(注) |
(注)bun の「キャッシュあり」「lockfile あり」は、5回とも node_modules にファイルが入らなかった(小は0.04〜0.07秒、中は0.64〜0.90秒で終了)。完全なインストールの時間ではないため、中央値を出していない。詳しくは下の節にある。
依存の一覧と、測り方・環境は付録にある。各セルの5回の値はすべて付録Cに載せた。
初回は bun、pnpm、npm の順に短かった
初回の条件では、5ラウンドのすべてで「bun が最短、次が pnpm、最長が npm」だった。小でも中でも同じだ。中央値で見ると、中(30個)で bun は npm の約3.5分の1(13.95秒と48.26秒)、pnpm は npm の約1.8分の1(26.88秒と48.26秒)の時間だった。小(5個)では bun が npm の約5分の1、pnpm が約2分の1だった。
この順序は、測った範囲では崩れなかった。一方、npm の中は37.82〜73.28秒と幅が大きく、同じ条件で最長と最短に約1.9倍の差がある。幅の出方には傾向があり、npm の中ではラウンド2がどの条件でも最も短く(初回37.82・キャッシュあり24.75・lockfile あり22.66)、ラウンド4がどの条件でも最も長かった(73.28・66.36・55.43)。条件よりラウンドの側に共通の要因があったと読めるが、その要因は特定していない。
要因の候補として、各回の前に端末の温度(6か所の読み値のうち最大)とメモリの空きを記録した。温度は測定全体で43〜70°C、中サイズの初回の回だけ見ると43〜54°Cで、npm の遅いラウンド4(49°C)と速いラウンド2(50°C)に差が無く、この読み値では説明できなかった。ネットワークは、各ラウンドの前に公式レジストリへ小さな GET を3回送って応答時間を記録しただけで、帯域は測っていない(付録A)。
bun の初回が短いことについて、この記事で言えるのは測定値までだ。bun の公式ページには、npm install から bun install に替えると「最大25倍速い(up to 25x faster)」という記述があるが、比較の相手や条件がこの記事の測定と同じとは限らず、その数字を確かめたものではない。
キャッシュや lockfile で、どれだけ短くなったか
同じラウンドの中で、初回→キャッシュあり→lockfile あり の順に測っているので、ラウンドごとの並びも見られる。
- npm の中(30個): キャッシュありは初回より5ラウンドすべてで短く、lockfile ありはキャッシュありより5ラウンドすべてで短かった。中央値は48.26秒→43.41秒→37.77秒で、初回から lockfile ありまでで約2割の短縮だった。
- npm の小(5個): 中央値は8.41秒・8.43秒・8.43秒で、ほぼ同じだった。キャッシュありが初回より短かったのは5ラウンド中2回だけだった。
- pnpm の小: 初回4.23秒に対しキャッシュあり3.28秒で、5ラウンドすべてで短かった。lockfile あり(3.51秒)はキャッシュありと同程度だった。
- pnpm の中: 26.88秒・26.64秒・28.41秒で、一定の向きの差は見えなかった(キャッシュありが初回より短かったのは5ラウンド中3回)。
npm の小と pnpm の中では、キャッシュや lockfile があっても所要時間がほとんど変わらなかった。キャッシュや lockfile で減るはずの取得・解決の分が変わらなかったので、時間の大きな部分は別のところにあると考える(ただし、キャッシュありの回にレジストリへどれだけ問い合わせたかは測っていない)。
その候補の1つが、ファイルを書く時間だ。中の node_modules は約1.8万ファイル・約170MB ある。npm で入れたその木を cp -a でコピーするだけでも、5回の中央値で25.24秒かかった(24.18〜25.98秒。付録A)。npm の lockfile あり(37.77秒)や pnpm のキャッシュあり(26.64秒)は、この時間に近い。ただし、測ったのは cp の時間だけで、各マネージャーの時間の内訳(ファイルの書き込み・展開・待ち)は測っていない。書き込みが支配的だと断定はできない。
bun の初回は13.95秒で、約1.7万ファイルを作るのに cp より短い。bun の公式ページは、Linux の既定の backend が hardlink だと説明している。測定でも、bun が入れた lodash.js のリンク数は2だった(リンク数は後の節)。ハードリンクなら中身を書き写す時間がかからないので、短いことと関係していると考えるが、切り分けてはいない。
bun は、キャッシュがあると空のフォルダしか入れなかった
bun の「キャッシュあり」「lockfile あり」の回は、小で0.04〜0.07秒、中で0.64〜0.90秒で終わり、画面には「N packages installed」と出て、終了コードも0だった。ところが node_modules を調べると、パッケージ名のフォルダはあっても、中にファイルが無かった(ファイル数0。初回の回は小2,366・中17,465ファイル)。5ラウンド×小・中×2条件の20回すべてで同じだった。
原因を切り分ける小さな確認(lodash 1個)をした。
| 手順 | node_modules/lodash のファイル数 |
|---|---|
| キャッシュなしで入れる | 1,051 |
| キャッシュから入れ直す(backend 指定なし) | 0 |
同 --backend hardlink |
0 |
同 --backend copyfile |
0 |
同 --backend symlink |
0 |
キャッシュ側(lodash@4.18.1@@@1 のフォルダ)には1,051ファイルが残っていた。キャッシュが壊れているのではなく、bun がキャッシュから node_modules にファイルを持ってくる段階で、どの backend でも何も入っていない。bun の公式ページは、copyfile を「他の backend が失敗したときの代替」と説明しているが、copyfile を指定しても空だった。
原因は特定できていない。この端末の PRoot は、起動時に --link2symlink(proot --help の説明は「ハードリンクをシンボリックリンクに置き換え、本物のハードリンクのふりをする」)を付けて動いていた。ハードリンクの扱いが特殊な環境であることは確かめたが、それが bun の空のインストールの原因かどうかは確かめていない。ほかの PRoot の設定や別の版の bun で同じことが起きるかも確かめていない。
なお、bun の公開リポジトリには、PRoot 上の aarch64 Linux で bun 1.4.2 の bun install が空のパッケージフォルダを残すという Issue(#43788、2026年9月22日に open として確認)がある。報告の内容は、bun add の直後から空になるというもので、この記事で空になったのはキャッシュがあるときだけ(初回は完全に入った)という点は異なる。この Issue が今回の結果の原因を説明するかどうかは、この記事では確かめていない。
この結果から言えるのは、この端末・この bun(1.4.2)では、キャッシュを使う2条件の時間を、意味のある数字として出せなかったことだ。完全に入ったのは、キャッシュが空の状態から入れた回(初回の条件)だけだった。初回の bun はファイル数が npm・pnpm と同程度で、初回の数字は有効と考える。
ハードリンクの実際と、ディスク容量
node_modules にある lodash.js のリンク数を、初回の回で見た。npm は1、pnpm は2または3、bun は2だった。リンク数が2以上なので、少なくとも pnpm と bun では link() の呼び出しは通っている。ただし PRoot は前の節のとおりハードリンクを別の仕組みで扱っており、本物のハードリンクかどうかは確かめていない。
容量は、初回のあとに du -sk で測った。
| 小: node_modules | 小: キャッシュ/store | 中: node_modules | 中: キャッシュ/store | |
|---|---|---|---|---|
| npm | 15.5MB | 6.9MB | 171.9MB | 111.2MB |
| pnpm | 14.7MB | 1.8MB | 154.1MB | 40.4MB |
| bun | 15.5MB | 16.0MB | 166.0MB | 176.6MB |
node_modulesの大きさは、同じ依存なら3つで大きくは変わらなかった(中で154〜172MB)。- キャッシュ(pnpm は store)の大きさは大きく違った。bun は
node_modulesとほぼ同じ大きさのキャッシュを持ち、pnpm の store はnode_modulesよりかなり小さい値だった。 - pnpm の store が
node_modulesより小さいのは、ハードリンクの扱いが PRoot で特殊であることと関わっている可能性があるが、duの値が実際のディスク使用量と一致するかは確かめていない。容量の表は「duがこの環境で返した値」として読んでほしい。
入ったパッケージの数も3つで少し違った。中の node_modules で、package.json を持つフォルダは npm が276、pnpm と bun が266だった。node_modules の直下に見える名前の数は npm と bun が233、pnpm が30(pnpm は依存した30個だけが直下に見え、残りは .pnpm フォルダの中にある)。同じ依存と同じ版を指定したが、展開の仕方が違うため、同じ中身にはなっていない。数え方は付録Aにある。
測ったこと・測っていないこと
- 測ったのは: 1台の端末、1日(2026年10月1日、JST 03:04〜03:56)、2つの依存セット、3つのマネージャー(npm 12.2.0・pnpm 12.8.1・bun 1.4.2)、3条件、各5回。
- 5回は狭い標本だ。 中央値の差が小さい場合(たとえば pnpm の中の3条件)は、差があるとは言えない。初回の3者の順序は範囲が重ならなかったが、それも5回の範囲での話だ。
- ラウンドごとに初回→キャッシュあり→lockfile あり を続けて測っており、5回は独立ではない。後の2条件は初回で溜まったキャッシュを使う。ラウンドごとにマネージャーの順番を回したが、端末やネットワークの状態がラウンドで動いたことは npm の中で見えている。
- 測っていない条件: キャッシュなし+lockfile あり(CI でよくある組み合わせ)、Node に同梱の npm(10.9.8)、pnpm・bun の他の版、ネイティブの拡張を含む依存(小・中とも含まれない)、開発中の追加インストール(
add)、他の端末。 - 端末の状態: 測定中、同じ端末で筆者の作業環境も動いていた(重い作業は避けたが、ほかのプロセスを完全に止めてはいない)。メモリの空きは1,995〜3,729MBで推移した。負荷平均は PRoot が固定値(0.12)を返したため記録として使えなかった。CPU 時間(user/sys)も記録したが、所要時間と大きく食い違い、PRoot の仕組みが絡む可能性があるため本文では使わない。
- 外部への負荷: 各条件は計画の回数(5回)に収めた。本計測でレジストリから実際に取得した初回の回は、5ラウンド×2セット×3マネージャーの30回。止めた測定や
cpの確認用のnpm installでも、別に取得している。
付録A 測定の条件
端末・環境
| 項目 | 内容 |
|---|---|
| 端末 | Android スマホ(Termux + proot-distro)。機種名・SoC は取得していない |
| Linux | Ubuntu 26.04(aarch64)、カーネルの表示は 6.17.0-PRoot-Distro。PRoot は --link2symlink 付きで起動 |
| CPU | nproc・使えるコア数(sched_getaffinity)は4(Python の os.cpu_count() は8) |
| メモリ | 約10.8GB(測定中の空きは1,995〜3,729MB) |
| Node.js | v22.23.2 |
| npm | 12.2.0(npm i npm で入れた版。Node 同梱の10.9.8ではない) |
| pnpm | 12.8.1(npm i pnpm で入れた版。corepack 経由は使わない) |
| bun | 1.4.2(npm i bun で入れた版) |
| 測定日時 | 2026-10-01 03:04〜03:56(JST)。cp の確認は04:00ごろ、bun の再現確認は04:01ごろ |
| ネットワーク | 端末の回線(接続の種類は記録していない)。各ラウンドの前に registry.npmjs.org/lodash/latest への GET を3回測定し、1回目が0.25〜0.37秒、2・3回目が0.07〜0.09秒だった(1回目は接続の確立を含むと考えられる)。帯域は測っていない |
| 温度 | 6か所のうち最大の読み値が、測定全体で43〜70°C |
回し方(scripts/pm_bench.py)
- マネージャーごとにキャッシュ・作業フォルダを空にして、
package.json(依存は版を固定)だけを置く。キャッシュは測定専用のフォルダに向けた(npm はnpm_config_cache、pnpm はnpm_config_store_dirとXDG_CACHE_HOME、bun はBUN_INSTALL_CACHE_DIR)。 - 初回: そのまま入れる。
- キャッシュあり:
node_modulesと lockfile を消して入れる。 - lockfile あり:
node_modulesを消して、lockfile どおりに入れる。 - これを依存セット(小・中)ごと、マネージャーごとに5ラウンド。ラウンドごとにマネージャーの順番を回した(1→npm,pnpm,bun、2→pnpm,bun,npm、3→bun,npm,pnpm、以降同じ繰り返し)。
コマンド: npm は npm install --no-audit --no-fund(lockfile ありは npm ci --no-audit --no-fund)、pnpm は pnpm install(lockfile ありは --frozen-lockfile)、bun は bun install(lockfile ありは --frozen-lockfile)。npm だけ audit・fund の通信を外した。
集計: 時間はコマンドの起動から終了までの壁時計(Python の perf_counter)。中央値と最小・最大を出した。node_modules のファイル数が初回の9割未満の回は「不完全なインストール」とし、中央値から外して件数を別に記録した(bun のキャッシュあり・lockfile あり20回すべて)。node_modules の容量は du -sk、ファイル数は os.walk(シンボリックリンクは辿らない)。パッケージ数は、node_modules 直下(@scope の下を含む)のフォルダのうち package.json を持つものの数で、入れ子になったものも数えた。
cp の確認: npm で入れた中サイズの node_modules(18,554ファイル、176,055KB)を cp -a で別の場所にコピーする時間を5回測った(24.85・25.66・24.18・25.24・25.98秒)。
PRoot でのつまずき(記録): (1) 最初に測ったとき、pnpm が corepack の shim 経由で動き、測定用に分けたキャッシュ先を見て別の版(v12.8.1)を取り直していた。測定を止めて、3つとも npm i で入れた版を絶対パスで呼ぶ方式に変えて測り直した(止めた測定の記録は保存してあるが、数値には使っていない)。(2) bun のキャッシュあり・lockfile ありは、本文のとおり空のインストールになった。
付録B 使った依存(固定した版)
小(5個): lodash 4.18.1、dayjs 1.11.23、zod 4.6.5、chalk 6.0.1、commander 15.0.0(すべて MIT。測定で入ったのはこの5個だけ)
中(30個): 小の5個に加えて、express 5.2.1、axios 1.20.0、react 19.3.0、react-dom 19.3.0、uuid 14.0.2、yargs 18.2.0、semver 7.8.5、glob 13.0.6、minimist 1.2.8、debug 4.4.3、moment 2.31.0、date-fns 4.4.0、ramda 0.32.0、rxjs 7.8.2、cheerio 1.2.0、marked 18.0.14、js-yaml 5.4.2、dotenv 18.0.5、ws 8.22.0、fastify 5.12.5、nanoid 6.0.1、tslib 2.8.1、eslint 10.11.0、prettier 3.9.9、typescript 7.0.2
ライセンスは npm view で確認した。MIT が24個、Apache-2.0 が2個(rxjs・typescript)、ISC(semver)、BlueOak-1.0.0(glob)、BSD-2-Clause(dotenv)、0BSD(tslib)が1個ずつ。測定ではインストールしただけで、再配布はしていない。
付録C 全測定値(秒、5ラウンドの順)
依存5個(小)
| 初回 | キャッシュあり | lockfile あり | |
|---|---|---|---|
| npm | 7.58, 8.56, 8.85, 7.17, 8.41 | 8.31, 8.43, 7.96, 12.79, 8.75 | 8.23, 8.43, 7.76, 10.60, 8.44 |
| pnpm | 5.01, 3.81, 4.48, 4.23, 3.80 | 2.53, 3.62, 3.50, 3.28, 2.27 | 1.34, 3.51, 3.56, 3.59, 3.41 |
| bun | 0.84, 1.63, 1.78, 1.25, 1.55 | 不完全(0.04〜0.07) | 不完全(0.04〜0.06) |
依存30個(中)
| 初回 | キャッシュあり | lockfile あり | |
|---|---|---|---|
| npm | 51.07, 37.82, 48.26, 73.28, 47.47 | 43.41, 24.75, 43.89, 66.36, 42.85 | 37.77, 22.66, 37.67, 55.43, 39.81 |
| pnpm | 30.40, 22.25, 33.81, 25.58, 26.88 | 25.93, 29.01, 31.97, 26.64, 23.63 | 23.45, 31.48, 33.15, 28.41, 15.26 |
| bun | 14.10, 13.66, 13.95, 13.46, 14.12 | 不完全(0.70〜0.90) | 不完全(0.64〜0.86) |
生の記録(各回の時間・容量・ファイル数・測定前の温度・メモリ)は data/cache/pm-bench/run_20260930T180417Z.jsonl、集計は同フォルダの summary_20260930T180417Z.json にある。
出典
- 測定値(所要時間・容量・ファイル数・版): すべて筆者が 2026-10-01 に上の環境で
scripts/pm_bench.pyにより記録したもの。 - npm のドキュメント「npm ci」(lockfile が必要、
node_modulesを先に消す、lockfile を書き換えない。ページ表示 Version 11.20.0。取得日: 2026-10-01) - npm のドキュメント「npm install」(lockfile の扱い、audit・fund の既定。取得日: 2026-10-01)
- bun のドキュメント「bun install」(backend・キャッシュの場所・
--frozen-lockfile・速度の記述。取得日: 2026-10-01) - bun の GitHub Issue #43788「bun install leaves empty package directories in PRoot environment (Linux aarch64)」(存在と報告内容の確認のみ。取得日: 2026-10-01)
- pnpm のドキュメント「pnpm install」(
--frozen-lockfile。取得日: 2026-10-01) - pnpm のドキュメント「Node-Modules Settings」(
packageImportMethodの既定。取得日: 2026-10-01) - PRoot の
--link2symlinkの説明: 端末のproot --helpの表示(取得日: 2026-10-01)。 - 依存の版・ライセンス: npm レジストリ(
npm view <名前> version / license、取得日: 2026-10-01)