- npmエコシステムでは、依存関係ツリーの肥大化が主要な問題として指摘されており、これは古いランタイム対応、原子的パッケージ構造、古いponyfillの使用に起因する
- 旧式エンジン互換性とcross-realm安全性を理由に維持されている小さなユーティリティパッケージが、現代の環境でも不要なまま残っている
- 原子的アーキテクチャは再利用性を高めようとしたが、実際には重複・セキュリティ・保守コストを増やす非効率な構造として機能している
- すでにすべてのエンジンがサポートしている機能向けの古いponyfillパッケージが削除されず、不要なダウンロードと管理負担を招いている
- コミュニティはe18e、knip、module-replacementsのようなツールを通じて、不要な依存関係の整理とネイティブ機能への移行を進めている
JavaScript依存関係肥大化の3つの軸
- e18eコミュニティの成長とともに、性能重視のコントリビューションが増え、不要または保守されていないパッケージを整理するcleanup活動が進行中
- npmエコシステムでは、**依存関係ツリーの肥大化(dependency bloat)**が主要な問題として指摘されており、古いランタイム対応、原子的パッケージ構造、古いponyfillの使用が主な原因とされる
1. 古いランタイム対応(安全性とrealmを含む)
- npmツリーには
is-string、hasownのような小さなユーティリティパッケージが多数存在し、これは次の3つの理由で維持されている
- 非常に古いエンジン(例: ES3、IE6/7、初期Node.js)のサポート
-
グローバル名前空間の改変防止
-
古いエンジン対応
- ES3環境には
Array.prototype.forEach、Object.keys、Object.definePropertyなどのES5機能が存在しない
- このような環境では自前実装またはpolyfillの利用が必要
- 最善の解決策はアップグレードだが、一部の利用者は依然として旧バージョンを維持している
-
グローバル名前空間の改変防止
- Nodeは内部的にprimordialsの概念を使い、グローバルオブジェクトを初期時点でラップして改変から保護している
- たとえば
Mapを再定義するとNode自体が壊れる可能性があるため、Nodeは元の参照を保持する
- 一部のパッケージメンテナはこの方式を一般パッケージにも適用し、
math-intrinsicsのような安全性重視パッケージを利用している
-
Cross-realm値
- iframe間でオブジェクトを渡すと
instanceofチェックが失敗する問題が発生する
- 例:
window.RegExp !== iframeWindow.RegExp
chaiのようなテストフレームワークはObject.prototype.toString.call(val)方式でrealm間の型チェックを行う
is-stringのようなパッケージは、このcross-realm互換性のために存在する
-
問題点
- ほとんどの開発者はモダンなNodeやevergreenブラウザを使っているため、このような互換性は不要
- それでもこれらのパッケージが一般的な依存関係ツリーの「ホットパス」に含まれ、全員がコストを負担することになる
2. 原子的(Atomic)アーキテクチャ
- 一部の開発者は、パッケージを可能な限り小さな単位に分割し、再利用可能なビルディングブロックとして構成すべきだと主張する
- その結果、
shebang-regex、arrify、slash、path-key、onetime、is-wslなど、極端に細分化されたパッケージが多数存在する
- 例:
shebang-regexは1行の正規表現だけを含む(/^#!(.*)/)
-
問題点
- ほとんどの原子的パッケージは再利用されないか、単一の利用者しかいない
- 例:
shebang-regex → shebang-commandだけが使用
cli-boxes → boxen、inkだけが使用
onetime → restore-cursorだけが使用
- この場合、インラインコードと同じでありながら、npmリクエスト、展開、帯域幅などの追加コストが発生する
-
重複の問題
- 例:
nuxt@4.4.2の依存関係ツリーでは、is-docker、is-stream、is-wsl、path-keyなどがそれぞれ2つのバージョンで重複して存在する
- インラインコードに置き換えれば、バージョン衝突や解決コストがなくなり、重複コストはほぼゼロになる
-
サプライチェーンリスクの拡大
- パッケージ数が多いほど、セキュリティ・保守リスクが増大する
- 実際に、あるメンテナが多数の小さなパッケージを管理していた結果、アカウントがハッキングされて数百のパッケージが同時に損なわれた事例がある
- 単純なコード(
Array.isArray(val) ? val : [val])は、別パッケージにする必要はなくインラインで処理できる
-
結論
- 原子的アーキテクチャは意図に反して、非効率で危険な構造へと変質した
- ほとんどの利用者に実質的な利益がないまま、エコシステム全体がコストを負担している
3. 古いPonyfill
- Polyfillはエンジンがサポートしていない機能を環境に追加するコードであり、
Ponyfillは環境を変更せず、直接importして使う代替実装を指す
- 例:
@fastly/performance-observer-polyfillはpolyfillとponyfillの両方を提供する
-
問題点
- Ponyfillは過去には有用だったが、対象機能がすでにすべてのエンジンでサポートされていても削除されない
- 例:
globalthis(2019年からサポート、週4,900万ダウンロード)
indexof(2010年からサポート、週230万ダウンロード)
object.entries(2017年からサポート、週3,500万ダウンロード)
- これらのパッケージの多くは、単に削除されていないため残っている
- すべてのLTSエンジンが機能をサポートしているなら、ponyfillは削除されるべきである
肥大化解消の方法
- 依存関係ツリーの深いネストにより整理作業は難しいが、コミュニティの協力で改善可能
- 各開発者は「このパッケージは本当に必要か?」と自問し、不要であればissueを立てるか代替パッケージを探す必要がある
- module-replacementsプロジェクトは、ネイティブ機能で置き換え可能なパッケージ一覧を提供する
-
knipの利用
- knipは未使用の依存関係とデッドコードを検出するツール
- 直接的な解決策ではないが、整理の出発点として有用
-
e18e CLIの活用
@e18e/cli analyzeコマンドで置き換え可能な依存関係を検出できる
- 例:
chalk → picocolorsへ自動マイグレーション
- 今後は環境に応じて、Nodeの
styleTextのようなネイティブ機能の推奨も予定されている
-
npmgraphの活用
- npmgraph.js.orgは依存関係ツリー可視化ツール
- 例:
eslint@10.1.0ツリーではfind-upブランチが孤立している
- 単純なファイル探索機能に6個のパッケージは不要なため、
empathicのようなより小さな代替を使える
-
module replacementsプロジェクト
- コミュニティが置き換え可能なパッケージとネイティブ機能のマッピングデータセットを維持している
- codemodsプロジェクトを通じて自動マイグレーションも支援する
結論
- 現在の肥大化は、少数の古い互換性や特殊な構造を維持したい利用者のために全体がコストを負担する構造になっている
- 過去には避けられなかったが、モダンなエンジンとAPIが十分に発展した今では不要な負担である
- 今後はこの少数派が別個のスタックを維持し、残りは軽量でモダンなコードベースを使う方向へ移行する必要がある
- e18eやnpmxのようなプロジェクトが文書化とツール整備を通じてこれを支援しており、
各開発者も自分の依存関係を点検し「なぜ必要なのか?」を問うべきである
- みんなで一緒に整理できる
2件のコメント
私もライブラリを作るときはまだ cjs ビルドを提供はしていますが、
2026年になっても esm の例すらなく、全部 require ベースのライブラリは、もう少しアップデートしてほしいとは思います。
Hacker Newsの意見
最近は 依存関係のないJavaScript で開発するのが最善の方向だと思う
JS/CSSの標準ライブラリも優秀だし、静的解析(TypeScriptのJSDocチェック)、ESモジュール、Web Componentsなども十分に強力だ
人はこのやり方が拡張性や保守性に不利だと言うが、私の経験ではむしろ単純で変更しやすい構造を維持できた
フレームワークやビルドツールがやっていることの大半は、ブラウザの組み込み機能と バニラパターン で代替できる
ただしこうしたやり方はまだ馴染みの薄い領域なので、チュートリアルの大半が大規模フレームワーク中心で回っているのが問題だ
実際、Reactのコードを完全にバニラへ移してもモジュール性は保たれ、コード量は約1.5倍になる程度で、依存関係がない分むしろ性能は良くなる
もちろん依存関係が悪いという意味ではない。ただ多くの開発者が「必ず使うべきだ」という 固定観念 に囚われている
たとえば私は地図機能の多いサイトを作っているが、mapbox/maplibre/openlayers のような代替のないライブラリを使わざるを得ない
クライアントも移行コストを一銭も払わずに済んだ
この記事 のように、モデル更新をどう処理しているのか気になる
むしろ 大規模コードベース を少人数で維持するのが楽になった
最近のツールのおかげで昔より自前実装がずっと簡単になり、agentic engineering とも相性が良い
文章がうまく書かれていて、感情的にならずに問題を明確に説明している
JSがまともな 標準ライブラリ を持てていないことが、この状況の一因だと思う
良い記事だが、問題の根本は 不要な追加(=bloat) そのものだと思う
「完璧とは、これ以上加えるものがないときではなく、これ以上取り除くものがないときに達成される」というサン=テグジュペリの言葉を引用したい
ほとんどのソフトウェアは「どうすればもっと優雅に作れるか?」より、「どうすればもっと簡単に追加できるか?」という問いで書かれている
答えはいつも
npm i more-stuffだデモステネスとキケロ の対比のように、これ以上削れないコードこそが良いコードだ
JSは過去と未来のブラウザ互換性の両方を考慮しなければならず、UI中心の言語なのでアクセシビリティ・国際化・モバイル対応などで 肥大化しやすい
多くの場合、これは隠れた 技術的負債 の問題に見える
コンパイルターゲットをESxへ上げず、パッケージや実装を更新しないことが原因だ
ES5はすでに13年間、すべてのブラウザでサポートされている(caniuse.com/es5)
どちらも自分たちの行動を機能だと考え、人気パッケージを多く維持している
だから変わりにくい。ときどきコミュニティが批判するが、彼らにも彼らなりの論理がある
Babelで旧版へトランスパイルするとコードは 肥大化して遅くなり、しかも古いブラウザではCSSやJS機能の制限で結局動かない
しかも polyfill が問題を起こしたこともあった(BigIntを処理できなかった指数演算子のpolyfill)
コンソール、TV、旧型Android、iPod touch、Facebook内蔵ブラウザなど様々な環境が存在する
だから外部モジュールは1つだけにして、残りはトランスパイラ設定で解決している
昔は非同期追跡のためにsetTimeoutなどをオーバーライドしていたが、今は signals ではるかに単純に処理できる
一部のパッケージ作者は ダウンロード数を増やすために 依存ツリーを人為的に細かく分割しているのだと思う
7行のパッケージが存在するなんて話にならない。lockfileのメタデータの方がコードより大きい
以前、create-react-appの依存関係の5%がある作者のミニパッケージだった
has-symbols, is-string, ljharb のような例がある
たとえばAnthropicはnpmダウンロード数の多いオープンソース保守者に 無料のClaude を提供している
ダウンロード数競争はむしろリスクを増やす
しかし他の文化では、それがむしろ良いことと見なされる
JSエコシステムを批判する前に 30 years of br tags を読むとよい
JSとツールの 進化の過程 を理解できる
単に「JS開発者が問題だ」と言うのは工学的思考の欠如だ
私たちは常により良い理論と実践を考えるべきだ
ソフトウェアの世界は急速に変わるので、自分で「偽の葬式」を行って古い慣行を捨てる必要がある
9年もののNode.jsコードベースを管理しているが、依存関係は8個しかなく、しかもすべて 下位依存なし だ
Node組み込み機能を優先して使い、必要な部分だけ自分で実装している
以前よりはるかに安定し、ストレスも少ない
Denoの 標準ライブラリ も素晴らしく、ランタイムの基本機能と合わせれば数個のパッケージだけでも十分にアプリを作れる
JSは 慎重に扱えばかなり良い言語 だ
is-stringのようなパッケージが主張する cross-realm安全性 は理解できるが、実際にそういう状況はまれだnpmがあまりにも簡単に公開を許したことで、「モジュールを分割して公開しよう」という哲学が 過剰拡大 したのが問題だ
利用者は依存ツリーを監査せずにそのままインストールするので、任意のコストが基本コストになってしまう
ponyfill の問題は自動化で解決可能だ
たとえばNode LTSバージョンですでにサポートされている機能を自動検知して削除する Renovate風ボット が役立ちそうだ
社内PWAの原則はただ一つだ:
「Chromeを最新版にアップグレード しろ。それでも問題があるならそのとき考える」
Safariの方がメモリを食わないのは理解するが、ポリシーとして統一する方が効率的だ
「ES3(IE6/7相当)まで対応しなければならない」という話は本当に理解しがたい
セキュリティ上、銀行サイトでさえそんな旧式ブラウザはブロックすべきだ
Webpack、Babel、polyfillスタックをアップグレードするのは大仕事なので、そのまま放置する
「壊れていないなら直すな」という文化だ