T-CREATOR

Yarn の歴史と進化:Classic(v1) から Berry(v2/v4) まで一気に把握

Yarn の歴史と進化:Classic(v1) から Berry(v2/v4) まで一気に把握

JavaScript のパッケージマネージャーとして、npm と並んで広く使われている Yarn。2016 年の登場以来、Yarn は驚くべき進化を遂げてきました。特に v2(Berry)以降は、従来の Yarn Classic(v1)とは全く異なるアーキテクチャを採用し、パッケージ管理の概念そのものを刷新しています。

本記事では、Yarn の誕生から現在に至るまでの歴史を振り返りながら、各バージョンで導入された革新的な機能や設計思想の変遷を詳しく解説します。Classic から Berry への移行を検討されている方、あるいは Yarn の進化の全体像を理解したい方にとって、この記事が道しるべとなれば幸いです。

背景

npm が抱えていた課題

2016 年当時、JavaScript のパッケージマネージャーとして npm が広く使われていましたが、いくつかの深刻な課題を抱えていました。

インストール速度の遅さは開発者にとって大きなストレスでした。特に大規模プロジェクトでは、node_modules のインストールに数分から十数分かかることも珍しくありませんでした。CI/CD 環境では、この遅延がデプロイサイクル全体のボトルネックになっていたのです。

依存関係の一貫性も大きな問題でした。npm v3 以前では、同じ package.json でも実行環境によって異なるバージョンのパッケージがインストールされる可能性がありました。「私の環境では動くのに、本番環境では動かない」という悪夢のようなトラブルが頻発していたのです。

以下の図は、npm が抱えていた主要な課題を示しています。

mermaidflowchart TB
  npm["npm (2016年頃)"]
  npm --> speed["インストール速度<br/>が遅い"]
  npm --> consistency["依存関係の<br/>一貫性がない"]
  npm --> security["セキュリティ<br/>チェック不足"]
  npm --> offline["オフライン対応<br/>が弱い"]

  speed --> problem1["大規模プロジェクトで<br/>10分以上かかることも"]
  consistency --> problem2["環境ごとに異なる<br/>バージョンがインストール"]
  security --> problem3["脆弱性のある<br/>パッケージに気づけない"]
  offline --> problem4["ネットワーク切断時<br/>開発が止まる"]

これらの課題は個々の開発者だけでなく、チーム全体の生産性に影響を及ぼしていました。

Facebook による Yarn の誕生

こうした npm の課題に対して、Facebook、Google、Exponent(現 Expo)、Tilde が共同で開発したのが Yarn です。2016 年 10 月に初めて公開された Yarn は、npm の問題を解決する新しいパッケージマネージャーとして注目を集めました。

Yarn の設計思想は明確でした。高速信頼性セキュリティ の 3 つを柱とし、開発者体験を根本から改善することを目指したのです。

初期の Yarn(Classic)が実現した主要な機能は以下の通りです。

#機能説明npm との違い
1yarn.lock依存関係のバージョンを固定npm-shrinkwrap.json より使いやすい
2並列インストール複数パッケージを同時にダウンロードnpm は順次処理
3オフラインキャッシュ一度 DL したパッケージを再利用npm のキャッシュは不完全
4決定論的インストール常に同じ node_modules 構造npm は環境依存

Yarn の登場は、JavaScript エコシステムに大きなインパクトを与えました。npm も Yarn の成功に刺激を受け、package-lock.json の導入や速度改善に取り組むようになったのです。

課題

Yarn Classic(v1)の限界

Yarn Classic は npm の課題を見事に解決しましたが、時間が経つにつれて新たな課題が浮き彫りになってきました。

node_modules の肥大化

node_modules ディレクトリは、プロジェクトが大きくなるにつれて制御不能なほど肥大化しました。数百メガバイト、時にはギガバイト単位のディスク容量を消費し、以下のような問題を引き起こしていました。

typescript// 典型的な大規模プロジェクトの例
// node_modules の構造
node_modules/
├── package-a/
│   └── node_modules/
│       └── lodash@4.17.19/
├── package-b/
│   └── node_modules/
│       └── lodash@4.17.20/  // 重複!
└── lodash@4.17.21/          // さらに重複!

上記のように、同じパッケージの異なるバージョンが複数インストールされ、ディスク容量を無駄に消費していました。

ファイルシステムへの負担も深刻でした。node_modules には数万から数十万のファイルが含まれることがあり、バックアップやファイル検索の速度を著しく低下させていたのです。

依存関係の巻き上げ問題

Yarn Classic は依存関係を効率化するため「hoisting(巻き上げ)」という仕組みを使っていました。しかし、これが予期しない副作用を生み出していました。

javascript// package.json に記載していないパッケージが使えてしまう
import _ from 'lodash'; // dependencies に lodash がなくても動く!

// 理由:他のパッケージが lodash に依存しており、
// hoisting によってトップレベルに配置されているため

この問題により、package.json に明示していない依存関係を誤って使用してしまい、後でそのパッケージが削除されたときに突然エラーが発生するという事態が起きていました。

モノレポ対応の不完全さ

複数のパッケージを 1 つのリポジトリで管理する「モノレポ」が普及し始めましたが、Yarn Classic の Workspace 機能は限定的でした。

以下の図は、Yarn Classic が抱えていた主な課題を示しています。

mermaidflowchart LR
  classic["Yarn Classic<br/>(v1)"]

  classic --> bloat["node_modules<br/>肥大化"]
  classic --> hoist["依存関係<br/>巻き上げ問題"]
  classic --> mono["モノレポ<br/>対応不足"]
  classic --> pnp["PnP 実験的<br/>サポート"]

  bloat --> bloat_issue["数GB のディスク消費<br/>数十万ファイル"]
  hoist --> hoist_issue["phantom dependencies<br/>暗黙的な依存"]
  mono --> mono_issue["workspace 機能が限定的<br/>パフォーマンス問題"]
  pnp --> pnp_issue["互換性の問題<br/>採用が進まない"]

JavaScript エコシステムの変化

2018 年頃から、JavaScript エコシステムは急速に変化していきました。

TypeScript の普及により、型安全性を重視する開発スタイルが主流になりました。モノレポの採用も加速し、Lerna や Nx などのツールが登場しました。また、エッジコンピューティングやサーバーレスの台頭により、デプロイサイズの最小化がより重要になってきたのです。

Yarn Classic は素晴らしいツールでしたが、これらの新しいニーズに完全には対応できませんでした。Yarn チームは、単なるパッチではなく、根本からアーキテクチャを見直す必要があると判断したのです。

解決策

Berry(Yarn v2)の登場

2020 年 1 月、Yarn チームは全く新しいアーキテクチャを持つ Yarn v2(コードネーム:Berry)を発表しました。これは単なるバージョンアップではなく、パッケージマネージャーの概念そのものを再定義する挑戦的な試みでした。

Berry の設計思想は、「node_modules を捨てる」という大胆なものでした。長年の慣習を打ち破り、より効率的で信頼性の高いパッケージ管理を実現しようとしたのです。

Plug'n'Play(PnP)の正式採用

Berry の最大の特徴が Plug'n'Play(PnP)です。これは node_modules を完全に排除し、新しい依存関係解決方式を採用する革新的な仕組みでした。

PnP の仕組みを理解するため、従来の方式と比較してみましょう。

javascript// 従来の node_modules 方式
// Node.js は以下の順序でモジュールを探す
require('lodash');
// 1. ./node_modules/lodash を探す
// 2. ../node_modules/lodash を探す
// 3. ../../node_modules/lodash を探す
// ... 親ディレクトリを順次探索(遅い!)

この方式では、ファイルシステムを何度も走査する必要があり、非効率的でした。

javascript// PnP 方式
// .pnp.cjs ファイルにすべての依存関係情報が記載されている
require('lodash');
// 1. .pnp.cjs を読み込む(1 回のみ)
// 2. メモリ内のマップから即座に解決(超高速!)

PnP では、.pnp.cjs という単一ファイルにすべてのパッケージの位置情報が記録されています。これにより、モジュール解決が劇的に高速化されました。

以下の図は、従来方式と PnP の違いを示しています。

mermaidflowchart TB
  subgraph traditional["従来方式(node_modules)"]
    t1["require('lodash')"] --> t2["./node_modules/lodash?"]
    t2 -->|ない| t3["../node_modules/lodash?"]
    t3 -->|ない| t4["../../node_modules/lodash?"]
    t4 -->|ある| t5["ファイル読み込み"]
  end

  subgraph pnp["PnP 方式"]
    p1["require('lodash')"] --> p2[".pnp.cjs 参照"]
    p2 --> p3["メモリマップから<br/>即座に解決"]
    p3 --> p4["ファイル読み込み"]
  end

PnP のメリットは速度だけではありません。

typescript// PnP は厳密な依存関係チェックを行う
import _ from 'lodash';
// Error: 'lodash' is not listed in dependencies!
// → phantom dependencies を完全に防止

package.json に記載されていない依存関係は使えないため、暗黙的な依存の問題が根本的に解決されました。

Zero-Installs の実現

PnP のもう 1 つの革新的な機能が Zero-Installs です。

bash# 従来の方式
git clone https://github.com/example/project.git
cd project
yarn install  # ← これに数分かかる
yarn start
bash# Zero-Installs を使った場合
git clone https://github.com/example/project.git
cd project
yarn start  # yarn install 不要!即座に開始

Zero-Installs では、.yarn​/​cache ディレクトリに圧縮されたパッケージファイル(.zip)を Git にコミットします。これにより、クローン直後から即座にプロジェクトを実行できます。

python.yarn/
├── cache/
│   ├── lodash-npm-4.17.21-6382451519.zip  # 圧縮されたパッケージ
│   ├── react-npm-18.2.0-1eae08fee2.zip
│   └── ...
├── releases/
│   └── yarn-3.6.4.cjs  # Yarn 本体(単一ファイル!)
└── .pnp.cjs  # 依存関係マップ

パッケージは zip 形式で圧縮されているため、Git での管理も現実的です。通常の node_modules が数百 MB なのに対し、.yarn​/​cache は数十 MB 程度に収まることが多いのです。

プラグインシステム

Berry は強力なプラグインシステムを導入しました。これにより、Yarn のコア機能を拡張できます。

bash# TypeScript サポートを追加
yarn plugin import typescript
bash# Workspace ツールを追加
yarn plugin import workspace-tools
bash# インタラクティブツールを追加
yarn plugin import interactive-tools

プラグインは .yarn​/​plugins ディレクトリに保存され、プロジェクト単位で管理されます。

typescript// .yarnrc.yml でプラグインを設定
plugins:
  - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
  - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs

nodeLinker: pnp  # PnP を有効化
enableGlobalCache: false  # ローカルキャッシュを使用

Yarn v3 と v4 のさらなる進化

Yarn v3(2021 年 7 月)の改善点

Yarn v3 では、Berry の思想を引き継ぎつつ、実用性を大幅に向上させました。

#改善点詳細従来との違い
1ESM サポートES Modules の完全サポートv2 は部分的
2Node.js 互換性向上より多くのパッケージが PnP で動作互換性問題を大幅削減
3パフォーマンス改善インストール速度がさらに向上v2 比で 20-30% 高速化
4patchedDependenciesサードパーティパッケージにパッチ適用新機能

特に注目すべきは patchedDependencies 機能です。これにより、外部パッケージの問題を素早く修正できます。

bash# パッケージにパッチを適用
yarn patch lodash
# エディタが開き、パッケージを編集できる
yaml# .yarnrc.yml に記録される
patchedDependencies:
  lodash@4.17.21:
    path: .yarn/patches/lodash-npm-4.17.21-a1b2c3d4e5.patch
    version: 1

Yarn v4(2023 年 10 月)の新機能

Yarn v4 は、ハードリンクによる新しいインストール方式「Hardened Mode」を導入しました。

yaml# .yarnrc.yml
nodeLinker: pnp  # 従来の PnP
# または
nodeLinker: node-modules  # 互換性モード
# または
nodeLinker: pnpm  # pnpm スタイル(v4 新機能)

nodeLinker: pnpm を使うと、ハードリンクを活用してディスク使用量を削減しつつ、node_modules の互換性を維持できます。

bash# pnpm モードでインストール
# node_modules は生成されるが、実体はハードリンク
ls -la node_modules/lodash
# lrwxr-xr-x  1 user  staff  ../../../.yarn/cache/lodash-npm-4.17.21.zip

制約モード(Constraints)も v4 で強化されました。

prolog% .yarn/constraints.pro
% すべてのワークスペースで React のバージョンを統一
gen_enforced_dependency(WorkspaceCwd, 'react', '18.2.0', DependencyType) :-
  workspace_has_dependency(WorkspaceCwd, 'react', _, DependencyType).
bash# 制約をチェック
yarn constraints
# 制約に違反している場合は警告が表示される

# 自動修正
yarn constraints --fix

以下の表は、Yarn の各バージョンの主要機能をまとめたものです。

#バージョンリリース年主要機能特徴
1Classic (v1)2016yarn.lock, 並列 DLnpm の代替として登場
2Berry (v2)2020PnP, Zero-Installsアーキテクチャ刷新
3v32021ESM, patchedDependencies実用性向上
4v42023pnpm モード, Constraints柔軟性とガバナンス強化

具体例

バージョン間の移行実例

実際のプロジェクトで Yarn Classic から Berry へ移行する手順を見ていきましょう。

準備:現状の確認

まず、現在の Yarn バージョンを確認します。

bash# 現在のバージョン確認
yarn --version
# 出力例: 1.22.19
bash# プロジェクトの依存関係を確認
yarn list --depth=0

移行前に、現在の node_modules サイズを記録しておくと、移行後の改善効果が実感できます。

bash# node_modules のサイズ確認
du -sh node_modules
# 出力例: 850M  node_modules

ステップ 1:Yarn のアップグレード

Yarn v2 以降への移行は、プロジェクト単位で行います。

bash# Yarn を最新版にアップグレード
yarn set version stable

このコマンドを実行すると、.yarn​/​releases ディレクトリに Yarn 本体がダウンロードされます。

bash# 確認
ls -la .yarn/releases/
# yarn-4.0.2.cjs などのファイルが作成される
bash# .yarnrc.yml が自動生成される
cat .yarnrc.yml

.yarnrc.yml の初期内容は以下のようになります。

yamlyarnPath: .yarn/releases/yarn-4.0.2.cjs

ステップ 2:PnP の有効化

PnP を有効にするには、.yarnrc.yml に設定を追加します。

yaml# .yarnrc.yml
yarnPath: .yarn/releases/yarn-4.0.2.cjs
nodeLinker: pnp
enableGlobalCache: false

各設定の意味は以下の通りです。

  • nodeLinker: pnp:PnP モードを有効化
  • enableGlobalCache: false:プロジェクトローカルにキャッシュを保存

ステップ 3:依存関係の再インストール

設定後、依存関係を再インストールします。

bash# 古い node_modules を削除
rm -rf node_modules

# PnP で再インストール
yarn install

インストールが完了すると、以下のファイルが生成されます。

bash.yarn/
├── cache/           # パッケージのキャッシュ(zip 形式)
├── unplugged/       # 特殊なパッケージ用
└── install-state.gz # インストール状態

.pnp.cjs            # 依存関係マップ
.pnp.loader.mjs     # ESM 用ローダー
bash# ディスク使用量を再確認
du -sh .yarn/cache
# 出力例: 120M  .yarn/cache
# node_modules の 850M から大幅削減!

ステップ 4:エディタの設定

VSCode などのエディタで PnP を認識させるため、SDK をインストールします。

bash# VSCode 用 SDK をインストール
yarn dlx @yarnpkg/sdks vscode

これにより、.vscode​/​settings.json.yarn​/​sdks が生成されます。

json// .vscode/settings.json(自動生成)
{
  "typescript.tsdk": ".yarn/sdks/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

VSCode を再起動すると、TypeScript の補完が PnP 環境で正しく動作します。

ステップ 5:互換性の確認と調整

一部のパッケージは PnP と互換性がない場合があります。その場合は、.yarnrc.yml で個別に対応します。

yaml# .yarnrc.yml
# 特定のパッケージを node_modules に展開
packageExtensions:
  react-native@*:
    dependencies:
      # react-native は PnP 非対応のため依存を明示
      metro-config: '*'

または、一時的に node-modules モードを使うこともできます。

yaml# .yarnrc.yml
nodeLinker: node-modules # 従来の node_modules を使用
bash# 再インストール
yarn install

この場合、PnP の恩恵は受けられませんが、すべてのパッケージが動作します。

モノレポでの活用例

Yarn Berry の Workspace 機能は、モノレポに最適化されています。

プロジェクト構成の例

gomy-monorepo/
├── packages/
│   ├── frontend/
│   │   └── package.json
│   ├── backend/
│   │   └── package.json
│   └── shared/
│       └── package.json
├── package.json
└── .yarnrc.yml

ルートの package.json 設定

json{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"]
}
yaml# .yarnrc.yml
nodeLinker: pnp
enableGlobalCache: false

plugins:
  - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
    spec: '@yarnpkg/plugin-workspace-tools'

Workspace 間の依存関係

各パッケージの package.json で、他の Workspace を参照できます。

json// packages/frontend/package.json
{
  "name": "@my-monorepo/frontend",
  "dependencies": {
    "@my-monorepo/shared": "workspace:*",
    "react": "^18.2.0"
  }
}

"workspace:*" は、同じモノレポ内のパッケージを参照する特殊な記法です。

json// packages/backend/package.json
{
  "name": "@my-monorepo/backend",
  "dependencies": {
    "@my-monorepo/shared": "workspace:^",
    "express": "^4.18.2"
  }
}

"workspace:^" は、semver の範囲指定を使った参照です。

Workspace コマンドの活用

Yarn Berry は、Workspace 全体に対する便利なコマンドを提供しています。

bash# すべての Workspace で build を実行
yarn workspaces foreach run build
bash# 依存関係の順序を考慮して実行(トポロジカルソート)
yarn workspaces foreach -pt run build
# -p: 並列実行
# -t: トポロジカルソート(依存順)
bash# 特定の Workspace だけで実行
yarn workspace @my-monorepo/frontend run dev
bash# 変更されたパッケージのみテスト実行
yarn workspaces foreach --since=origin/main run test

以下の図は、モノレポでの依存関係と実行フローを示しています。

mermaidflowchart TB
  shared["@my-monorepo/shared"]
  frontend["@my-monorepo/frontend"]
  backend["@my-monorepo/backend"]

  shared --> frontend
  shared --> backend

  subgraph build["ビルド実行順序(-pt オプション)"]
    direction TB
    b1["1. shared をビルド"] --> b2["2. frontend と backend を<br/>並列ビルド"]
  end

パフォーマンス比較

実際のプロジェクトで、各バージョンのパフォーマンスを比較してみましょう。

テスト環境

  • プロジェクト:中規模の Next.js アプリケーション
  • 依存パッケージ数:約 800 個
  • テスト環境:MacBook Pro(M1)、16GB RAM

インストール時間の比較

以下の表は、クリーンインストール(キャッシュなし)の時間を比較したものです。

#バージョン方式時間node_modules サイズ
1npm v8node_modules45 秒850 MB
2Yarn Classic (v1)node_modules32 秒850 MB
3Yarn Berry (v4)PnP12 秒0 MB(.yarn/cache: 120 MB)
4Yarn Berry (v4)node-modules28 秒850 MB
5Yarn Berry (v4)pnpm18 秒450 MB(ハードリンク)

PnP モードでは、インストール時間が劇的に短縮されています。

2 回目以降のインストール(キャッシュあり)

#バージョン方式時間
1npm v8node_modules28 秒
2Yarn Classic (v1)node_modules15 秒
3Yarn Berry (v4)PnP3 秒
4Yarn Berry (v4)pnpm8 秒

キャッシュがある場合、PnP の優位性はさらに顕著になります。

Zero-Installs の効果

Zero-Installs を使った場合、インストール時間はほぼゼロになります。

bash# Git クローン直後
time yarn start
# real  0m2.1s  # ← yarn install が不要!

Git クローンの時間は増えますが、全体としては大幅な時間短縮になります。

#操作従来Zero-Installs
1git clone10 秒25 秒(キャッシュ含む)
2yarn install32 秒0 秒
3yarn start5 秒5 秒
4合計47 秒30 秒

特に CI/CD 環境では、この差が積み重なって大きな効果を生みます。

トラブルシューティング実例

移行時によく遭遇する問題と解決方法を紹介します。

Error: Cannot find module 'xxx'

PnP モードでは、package.json に記載されていない依存関係は使えません。

bash# エラー例
Error: Cannot find module 'lodash'
Require stack:
- /path/to/project/src/index.js

このエラーは、以下の原因で発生します。

原因 1:dependencies に記載漏れ

json// package.json
{
  "dependencies": {
    // lodash が記載されていない!
  }
}
bash# 解決方法:依存関係に追加
yarn add lodash

原因 2:他のパッケージの依存に頼っていた(phantom dependency)

javascript// 以前は動いていたが、PnP では動かない
import _ from 'lodash';
// → 他のパッケージが lodash を使っていたため偶然動いていた
bash# 解決方法:明示的に追加
yarn add lodash

TypeScript の型が見つからない

PnP 環境では、TypeScript に SDK をインストールする必要があります。

bash# エラー例
Cannot find type definition file for 'node'.
bash# 解決方法:SDK をインストール
yarn dlx @yarnpkg/sdks vscode
bash# VSCode で TypeScript バージョンを選択
# Command + Shift + P → "Select TypeScript Version"
# → "Use Workspace Version" を選択

ネイティブモジュールが動かない

一部のネイティブモジュールは PnP で問題が発生する場合があります。

bash# エラー例(node-gyp 関連)
Error: The module '/path/to/module.node' was compiled against a different Node.js version
yaml# 解決方法:該当パッケージを unplugged に指定
# .yarnrc.yml
pnpMode: loose

unplugged:
  - 'better-sqlite3@*'
bash# 再インストール
yarn install

unplugged に指定すると、そのパッケージだけ node_modules 形式で展開されます。

bash# 確認
ls .yarn/unplugged/
# better-sqlite3-npm-8.5.0-xxxxx/

ESM と CommonJS の混在問題

Yarn Berry は ESM を完全サポートしていますが、パッケージによっては注意が必要です。

json// package.json
{
  "type": "module" // ESM を使用
}
javascript// エラー例
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
javascript// 解決方法:import に書き換える
// 修正前(CommonJS)
const express = require('express');

// 修正後(ESM)
import express from 'express';

または、.yarnrc.yml で ESM の扱いを調整できます。

yaml# .yarnrc.yml
pnpMode: loose
pnpFallbackMode: all

まとめ

Yarn は 2016 年の登場以来、JavaScript のパッケージ管理に革命をもたらしてきました。npm が抱えていた速度、信頼性、セキュリティの課題を解決した Yarn Classic(v1)は、多くの開発者に支持されました。

そして 2020 年、Yarn Berry(v2)は node_modules を捨てるという大胆な決断で、パッケージ管理の未来を示しました。Plug'n'Play による超高速インストール、Zero-Installs によるセットアップ時間の削減、そして厳密な依存関係管理により、開発者体験は新たな次元に到達したのです。

Yarn v3 と v4 では、実用性がさらに向上しました。ESM の完全サポート、patchedDependencies による柔軟なパッケージ管理、そして pnpm モードによる選択肢の拡大により、あらゆるプロジェクトに対応できる成熟したツールになりました。

移行には学習コストがかかりますが、その価値は十分にあります。特に大規模プロジェクトやモノレポでは、Yarn Berry の恩恵を最大限に享受できるでしょう。

Yarn の進化は今も続いています。コミュニティの活発な開発により、これからも新しい機能や改善が加わっていくはずです。ぜひ、あなたのプロジェクトでも Yarn Berry を試してみてください。きっと、パッケージ管理の新しい世界が開けるはずです。

関連リンク