Use Cases (更新: 2026/6/7)

Turborepo入門:turbo.jsonとキャッシュで遅いモノレポを救う

毎回フルビルドで待たされるモノレポを、Turborepoのタスクパイプラインとキャッシュで速くする。turbo.jsonの書き方、リモートキャッシュ、Nxとの違いまで実例で。

Turborepo入門:turbo.jsonとキャッシュで遅いモノレポを救う

pnpm build を叩いて、コーヒーを淹れに行く。戻ってきてもまだ終わっていない。

一行も変えていない packages/utils まで、毎回まるごとビルドし直している——僕のモノレポはずっとこれでした。アプリ2つと共有パッケージ3つ。それだけで、CIは1回8分。PRを出すたびに8分待つ。1日10回出せば80分が消えます。

原因はコードの量じゃありませんでした。**「前と同じ結果を、毎回ゼロから作り直していた」**こと。前回と入力が1バイトも変わっていないなら、出力も同じはず。なのに作り直す。これが遅さの正体です。

Turborepoは、ここを「キャッシュ」と「タスクの依存グラフ」で潰す道具です。設定は turbo.json 1枚から始められて、慣れれば30分で入ります。この記事は、その最初の30分を一緒に歩く話です。

この記事の要点

  • Turborepoは**「入力が変わっていないタスクは実行せず、前回の結果を復元する」**ビルドシステム。遅さの大半はムダな再実行が原因。
  • 設定の中心は turbo.jsontasksdependsOn: ["^build"] で「依存先を先にビルド」、outputs で「何をキャッシュするか」を宣言する。
  • キャッシュにはローカル(自分のPC)とリモート(チーム・CIで共有)の2段階。turbo loginturbo link でチーム全体が他人のビルド結果を使い回せる。
  • pnpm / npm workspaces の上に乗せるだけ。既存のworkspaceを捨てる必要はない。
  • 多機能なNxと迷ったら、設定が少なく軽いTurborepoから。比較はNx記事で。

Turborepoは「結果を覚えておく係」

難しく考えなくて大丈夫です。Turborepoがやっていることは、煎じ詰めると一つだけ。

そのタスクの入力(ソース、設定、環境変数)をハッシュにして、結果とセットで保存しておく。次に同じハッシュが来たら、実行せずに保存済みの結果をそのまま返す。

料理に例えます。レシピと材料がまったく同じなら、もう一度作らなくても、冷蔵庫の作り置きを出せばいい。Turborepoはこの作り置きを、タスク単位で持っている。材料(=ソースコードや設定)が1つでも変われば「作り置きは使えない」と判断して、そのタスクだけ作り直します。

ポイントはタスク単位であること。packages/utils を触っていないなら、そのビルド結果は作り置きのまま。触った apps/web だけ作り直す。さっきの「全部ビルドし直す」が、ここで止まります。

実際にキャッシュが効くと、ターミナルにこう出ます。

 Tasks:    5 successful, 5 total
Cached:    5 cached, 5 total
  Time:    120ms >>> FULL TURBO

>>> FULL TURBO。これが出たら、全タスクが作り置きから復元された合図です。8分が0.1秒になる瞬間で、初めて見たときは正直ちょっと感動しました。

まず動かす:pnpm workspaceにTurborepoを乗せる

説明より手を動かしたほうが早いです。pnpm workspaceの上にTurborepoを乗せる、最小のモノレポを作ります。apps/web と共有パッケージ packages/utils の2つだけ。

リポジトリの形はこうします。

acme-monorepo/
  apps/
    web/
      package.json
  packages/
    utils/
      package.json
  pnpm-workspace.yaml
  package.json
  turbo.json

pnpm-workspace.yaml は2行で十分です。

packages:
  - "apps/*"
  - "packages/*"

ルートの package.json。Turborepoを呼ぶスクリプトをそろえ、turbo 自体は devDependencies に入れます。2026年6月時点の安定版はv2系で、ここでは執筆時に確認した [email protected] を例にしていますが、実プロジェクトでは必ずロックファイルで固定してください。

{
  "name": "acme-monorepo",
  "private": true,
  "packageManager": "[email protected]",
  "scripts": {
    "build": "turbo run build",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "check": "turbo run lint test build"
  },
  "devDependencies": {
    "turbo": "^2.9.16"
  }
}

共有パッケージ packages/utilspackage.json。これを apps/web から workspace:* で参照します。workspace:* は「npmのレジストリじゃなく、このworkspace内のパッケージを使え」という指定です。

{
  "name": "@acme/utils",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "main": "./dist/index.js",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "lint": "eslint src",
    "test": "vitest run"
  }
}

ここまでで土台は完成。あとは turbo.json を書けば、Turborepoが依存グラフを理解して動き始めます。

turbo.jsonの正体:tasks・dependsOn・outputs

Turborepoの心臓は turbo.json です。長く見えても、覚える勘所は3つだけ。コピペで動く全体像を先に置きます。

{
  "$schema": "https://turborepo.dev/schema.json",
  "globalDependencies": ["pnpm-lock.yaml", ".env.example"],
  "globalEnv": ["NODE_ENV"],
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"],
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

勘所をひとつずつ。

tasks のキーは、各パッケージのスクリプト名。 "build" と書けば、Turborepoは各 package.jsonscripts.build を探して走らせます。だから新しいタスクを増やすときは、turbo.json とパッケージ側のスクリプト、両方の名前をそろえます。

dependsOn^ は「依存先を先に」。 "^build" は「このパッケージが依存しているパッケージのbuildを、先に終わらせてから自分をbuildする」という意味。apps/web@acme/utils に依存しているなら、utils → web の順で、Turborepoが勝手に並べてくれます。^ を付けない "build"(testの行)は「同じパッケージのbuildを先に」という意味になります。この記号一つで順序実行が決まるので、ここだけは丁寧に。

outputs は「キャッシュする成果物」。 ここに書いたファイルが作り置きとして保存され、キャッシュヒット時に復元されます。dist/** のように成果物だけを指定するのがコツ。!.next/cache/**! は除外で、「Next.jsの内部キャッシュは保存しなくていい」という指定です。

dev だけ毛色が違います。cache: false(開発サーバーの出力はキャッシュ不要)、persistent: true(起動しっぱなしの長寿命プロセスだと宣言)。これを書かないとTurborepoがdevの終了を待ち続けて固まります。

順序実行の全体像は、文章より図のほうが速いです。

flowchart LR
  utils["@acme/utils\nbuild"] --> web["apps/web\nbuild"]
  utils --> admin["apps/admin\nbuild"]
  web --> test_web["apps/web\ntest"]

dependsOn を宣言するだけで、Turborepoがこのグラフを自動で組み、依存のないタスクは並列で走らせます。順序を手書きのスクリプトで管理する苦行から、ここで解放されます。

ローカルとリモート、2段階のキャッシュ

キャッシュには2つのレイヤーがあります。ここを分けて理解すると、CI高速化の効きどころが見えます。

ローカルキャッシュリモートキャッシュ
保存先自分のPC(.turbo/cacheチーム共有のサーバー
効く相手自分の2回目以降チーム全員・CI
設定デフォルトでONturbo login + turbo link
嬉しい瞬間ブランチ往復が速い同僚やCIのビルド結果を丸ごと使い回せる

ローカルキャッシュは何もしなくても効きます。一度ビルドすれば、入力が変わらない限り次回は復元。ブランチを行き来してもサクサク。

本領はリモートキャッシュです。同僚がmainで一度ビルドした結果を、自分やCIがそのまま受け取れる。 自分のマシンで初めてビルドするタスクでも、誰かが同じ入力で既にビルドしていれば、ダウンロードして復元するだけ。CIが「他人の作り置き」を使うので、PRごとのフルビルドが消えます。仕組みの一次情報はTurborepo公式のCaching解説が正確で、ハッシュの中身(グローバルハッシュとパッケージハッシュ)まで踏み込んで読めます。

有効化はローカルで2コマンド。デフォルトの保存先はVercelのRemote Cacheです。

npx turbo login
npx turbo link

CIから使うときは、トークンとチーム名を環境変数で渡します。GitHub Actionsの例です。--affected で変更影響のある範囲だけに絞るので、fetch-depth: 0 で比較に必要なGit履歴を取っておくのを忘れずに。

name: turbo-ci
on:
  pull_request:
  push:
    branches: [main]
jobs:
  verify:
    runs-on: ubuntu-latest
    env:
      TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
      TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pnpm/action-setup@v4
        with:
          version: 11.5.1
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run lint test build --affected

ひとつ運用上の注意。ログもキャッシュ成果物として保存されます。 APIキーや顧客情報を console.log に出していると、それごとキャッシュに乗ってチームに共有されかねません。秘密情報をログに吐かない、を同時にルール化してください。

よく使うコマンドと、Claude Codeへの渡し方

日常で効くコマンドを並べます。--filter で対象パッケージを、--dry=json で「実行せずに計画だけ」を確認できます。

# 実行せず、何が走るか計画だけ見る(事故防止に最強)
pnpm turbo run build --dry=json

# @acme/web とその依存先だけビルド
pnpm turbo run build --filter=@acme/web...

# main から変更のあったパッケージのテストだけ
pnpm turbo run test --filter=...[origin/main]

# キャッシュを無視して強制実行(キャッシュ不具合の切り分けに)
pnpm turbo run build --force

このモノレポをClaude Codeに任せるときは、「Turborepoを設定して」だけだと事故ります。境界と禁止事項と検証コマンドを先に固定して渡すのがコツ。これはエージェントの足場(作業の前提を縛る枠)を作る作業で、詳しくはハーネスの作り方も参考になります。

このリポジトリは Turborepo v2 + pnpm workspace のモノレポです。

守る境界:
- apps/* はデプロイ対象のアプリ。
- packages/utils はフレームワーク非依存の関数だけを持つ。
- packages/* から apps/* へ依存してはいけない。
- turbo.json は pipeline ではなく tasks を使う。

依頼:
1. package.json と turbo.json を読み、タスク依存関係を説明して。
2. outputs が不足しているタスクを指摘して。
3. 直すなら最小差分で。
4. 最後に `pnpm turbo run build --dry=json` を実行して計画を報告して。

--dry=json を最後に踏ませると、Claude Codeが「設定を書いて終わり」ではなく「計画を見せてから止まる」動きになります。いきなり全部書き換えられる事故が、これで減ります。

僕がハマった落とし穴4つ

正直に書きます。スムーズには行きませんでした。

1つ目、pipeline をコピペした。 ネットの古い記事から turbo.json を持ってきたら、キーが pipeline のまま。v2系は tasks です。turbo が「pipeline は古い」と警告を出してようやく気づきました。v1からの移行なら、Claude Codeに「v1設定をv2のtasksへ変換して、差分を説明して」と頼むと安全です。

2つ目、outputs を欲張った。 速くしたい一心で .next/cache/** まで outputs に入れたら、キャッシュが数百MBに膨れ、復元のほうが遅くなりました。本末転倒です。outputs は成果物(dist.next 本体)だけ。フレームワークの内部キャッシュは ! で除外する、が正解でした。

3つ目、浅いcheckoutで --affected を使った。 CIで fetch-depth を指定し忘れたら、Git履歴が浅すぎてTurborepoが変更範囲を計算できず、毎回全パッケージが対象に。「絞ってるのに速くならない」と小一時間悩みました。fetch-depth: 0 を入れたら一発で直りました。

4つ目、共通化しすぎた。 packages/shared に便利そうな関数を何でも放り込んだ結果、そこを1行直すと全アプリのキャッシュが吹き飛ぶように。共通化の基準は「2つ以上のアプリで本当に同じ責務か」まで絞る。これでグラフもキャッシュもぐっと素直になりました。

よくある質問

Q. TurborepoとNx、どっちを選べばいい? A. 設定の薄さと学習コストの低さで選ぶならTurborepo。コード生成やプラグイン、より高度な依存解析まで欲しいならNxです。まず軽く始めて困ったら乗り換える、で十分。詳しい比較はNx workspace記事にまとめました。

Q. pnpmじゃなくnpm workspacesでも使える? A. 使えます。Turborepoはpnpm / npm / yarnのworkspacesに乗る設計です。pnpm-workspace.yaml の代わりに、npmならルート package.jsonworkspaces フィールドで管理します。詳細はpnpm workspace記事を。

Q. dependsOn^buildbuild、何が違う? A. ^build は「依存先パッケージのbuild」、^ なしの build は「同じパッケージ内のbuild」を先に走らせます。「テストの前に自分をビルド」なら build、「自分の前に部品をビルド」なら ^build です。

Q. キャッシュが古い結果を返してくる気がする。 A. まず --force でキャッシュを無視して実行し、切り分けます。多くは inputsglobalDependencies の宣言漏れで、ハッシュに入るべきファイルが入っていないのが原因。.env や設定ファイルを globalDependencies に足すと直ることが多いです。

Q. リモートキャッシュはVercel以外でも使える? A. デフォルトはVercelですが、独自のキャッシュサーバーを立てる選択肢もあります。まずは無料で始められるVercelで体験して、要件が固まってから自前運用を検討するのが現実的です。

実際に試した結果

冒頭の「8分のCI」を、この構成で測り直しました。ViteアプリのモノレポにTurborepoを乗せ、tasks と最小の outputs--affected を入れただけ。初回はもちろんフルで走りましたが、2回目以降は変更したパッケージ以外が >>> FULL TURBO で復元され、変更が1パッケージに収まるPRなら2分を切るようになりました。

一方で、最初に欲張って .next/cache/**outputs に入れたときは、キャッシュ転送のほうが重くて逆効果。速くするための設定が、雑だと遅くする——これがいちばんの学びでした。

入れる順番も大事です。いきなり全部やらず、まず turbo.jsontasks、次にルートのscripts、最後にCIのリモートキャッシュ。1段ずつ確かめると、どこで効いたかが見えて、壊れても戻せます。遅いビルドに毎日数十分溶かしているなら、最初の turbo.json 1枚を書くところから、ぜひ。チーム単位での導入やCI/CD設計に踏み込みたい場合は、研修・相談も使ってください。

#Turborepo #モノレポ #turbo.json #キャッシュ #Claude Code
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。