Turborepo完全ガイド - 高速モノレポビルドシステムで開発効率を最大化する
Turborepo完全ガイド - 高速モノレポビルドシステムで開発効率を最大化する
Turborepoとは
TurborepoはVercel製の高速モノレポビルドシステムとして、複数パッケージを持つ大規模プロジェクトのビルド時間を劇的に短縮します。
従来のモノレポの課題
# 従来のLerna/npm workspaces
npm run build # すべてのパッケージを直列ビルド
packages/ui → 30秒
packages/app → 45秒
packages/api → 20秒
合計: 95秒
# 問題点
- 直列実行による時間浪費
- 変更なしでも毎回フルビルド
- CI上でのキャッシュ再利用不可
- パッケージ間の依存関係を考慮しない実行順序
Turborepoの解決策
# Turborepo
turbo run build
# 初回
packages/ui → 30秒 \
packages/app → 45秒 } 並列実行(最大45秒)
packages/api → 20秒 /
# 2回目(変更なし)
packages/ui → FULL TURBO (0ms)
packages/app → FULL TURBO (0ms)
packages/api → FULL TURBO (0ms)
合計: 0秒(キャッシュヒット)
主要機能:
- 並列実行 - 依存関係を解決しながら最大並列化
- インクリメンタルビルド - 変更されたパッケージのみ再ビルド
- リモートキャッシュ - チーム全体でビルド結果を共有
- タスクパイプライン - 複雑な依存関係を宣言的に管理
インストールとセットアップ
既存プロジェクトへの導入
# 1. Turborepoをインストール
npm install turbo --save-dev
# 2. turbo.jsonを作成
npx turbo init
// turbo.json(自動生成)
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"]
},
"lint": {},
"dev": {
"cache": false
}
}
}
新規モノレポ作成
# Turborepoテンプレートから作成
npx create-turbo@latest
? Where would you like to create your turborepo? my-monorepo
? Which package manager do you want to use? pnpm
cd my-monorepo
pnpm install
pnpm dev
生成される構造:
my-monorepo/
├── apps/
│ ├── web/ # Next.jsアプリ
│ └── docs/ # ドキュメントサイト
├── packages/
│ ├── ui/ # 共有UIコンポーネント
│ ├── eslint-config/ # ESLint設定
│ └── typescript-config/ # TypeScript設定
├── turbo.json # Turborepo設定
├── package.json
└── pnpm-workspace.yaml
基本構造
ワークスペース設定
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
// package.json(ルート)
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "^1.13.0"
},
"packageManager": "pnpm@8.15.0"
}
パッケージ構造
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint ."
},
"devDependencies": {
"@types/react": "^18.2.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
}
// apps/web/package.json
{
"name": "web",
"version": "0.0.0",
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"@repo/ui": "workspace:*", // ローカルパッケージを参照
"next": "^14.1.0",
"react": "^18.2.0"
}
}
タスクパイプライン
基本的な依存関係
// turbo.json
{
"pipeline": {
"build": {
// "^build": 依存パッケージのbuildが完了してから実行
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "build/**"]
},
"test": {
// 自パッケージのbuildが完了してから実行
"dependsOn": ["build"]
},
"lint": {
// 並列実行可(依存なし)
}
}
}
実行順序:
turbo run test
1. packages/ui:build (依存なし、最初に実行)
2. apps/web:build (ui:buildに依存)
3. packages/ui:test (ui:buildに依存)
4. apps/web:test (web:buildに依存)
ui:build と api:build は並列実行
複雑な依存関係
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"test:e2e": {
// 複数の依存関係
"dependsOn": ["build", "test", "^build"]
},
"deploy": {
"dependsOn": ["build", "test", "lint"],
"outputs": []
}
}
}
タスクフィルタリング
# 特定パッケージのみ実行
turbo run build --filter=web
# 複数パッケージ
turbo run build --filter=web --filter=api
# 依存関係も含めて実行
turbo run build --filter=web...
# 特定パッケージの依存先を実行
turbo run build --filter=...ui
# パターンマッチ
turbo run build --filter=@repo/*
キャッシュ戦略
ローカルキャッシュ
// turbo.json
{
"pipeline": {
"build": {
"outputs": ["dist/**", ".next/**"],
"cache": true // デフォルトでtrue
},
"dev": {
"cache": false // 開発サーバーはキャッシュ不要
}
}
}
キャッシュの仕組み:
1. タスク実行前
- 入力ファイル(src/**)のハッシュ計算
- ハッシュが一致するキャッシュを検索
2. キャッシュヒット
- node_modules/.cache/turbo から復元
- outputs に指定したファイルを展開
3. キャッシュミス
- タスクを実行
- outputs を圧縮してキャッシュに保存
リモートキャッシュ(Vercel)
# Vercelにログイン
npx turbo login
# リモートキャッシュを有効化
npx turbo link
// turbo.json
{
"remoteCache": {
"enabled": true
}
}
利点:
- CI/CDでビルド時間を短縮
- チーム全体でキャッシュを共有
- ローカル環境でもCI結果を再利用
セルフホストリモートキャッシュ
# turborepo-remote-cacheをセットアップ
npm install -g turborepo-remote-cache
# サーバー起動
turborepo-remote-cache --token YOUR_SECRET_TOKEN
// turbo.json
{
"remoteCache": {
"signature": true,
"teamId": "team_xxx",
"apiUrl": "https://cache.example.com"
}
}
キャッシュ無効化
# キャッシュを無視して実行
turbo run build --force
# キャッシュをクリア
rm -rf node_modules/.cache/turbo
環境変数管理
自動検出
Turborepoは環境変数の変更を自動検出してキャッシュを無効化します。
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"env": [
"DATABASE_URL", // この環境変数が変わるとキャッシュ無効化
"API_KEY"
]
}
}
}
グローバル環境変数
// turbo.json
{
"globalEnv": [
"NODE_ENV",
"CI"
],
"pipeline": {
"build": {
"env": ["DATABASE_URL"] // パッケージ固有
}
}
}
.envファイル
# ルート .env
DATABASE_URL=postgres://localhost/db
API_KEY=secret
# apps/web/.env.local
NEXT_PUBLIC_API_URL=http://localhost:3000
// turbo.json
{
"pipeline": {
"build": {
"env": ["NEXT_PUBLIC_*"], // ワイルドカードサポート
"dependsOn": ["^build"]
}
}
}
並列実行の最適化
同時実行数の制御
# 最大2タスクを並列実行
turbo run build --concurrency=2
# CPU数に合わせて自動調整
turbo run build --concurrency=50%
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
}
},
"globalDependencies": ["**/.env.*"]
}
タスクのプロファイリング
# プロファイル情報を出力
turbo run build --profile=profile.json
# Chromeでプロファイルを表示
# chrome://tracing にドラッグ&ドロップ
プロファイルで確認できる情報:
- 各タスクの実行時間
- 並列実行のタイムライン
- ボトルネックの特定
- キャッシュヒット率
CI/CD統合
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm turbo run build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Test
run: pnpm turbo run test
- name: Lint
run: pnpm turbo run lint
Vercel
// vercel.json
{
"buildCommand": "turbo run build --filter=web",
"installCommand": "pnpm install",
"framework": "nextjs",
"ignoreCommand": "npx turbo-ignore"
}
turbo-ignoreの動作:
- 変更されたパッケージのみデプロイ
- 影響のないパッケージはスキップ
# apps/web に変更がある場合のみビルド
npx turbo-ignore web
# → 終了コード 0(ビルド実行)
# apps/web に変更がない場合
npx turbo-ignore web
# → 終了コード 1(ビルドスキップ)
CircleCI
# .circleci/config.yml
version: 2.1
jobs:
build:
docker:
- image: node:20
steps:
- checkout
- restore_cache:
keys:
- pnpm-{{ checksum "pnpm-lock.yaml" }}
- run:
name: Install dependencies
command: |
npm install -g pnpm
pnpm install
- save_cache:
key: pnpm-{{ checksum "pnpm-lock.yaml" }}
paths:
- node_modules
- ~/.pnpm-store
- run:
name: Build
command: pnpm turbo run build
environment:
TURBO_TOKEN: $TURBO_TOKEN
TURBO_TEAM: $TURBO_TEAM
workflows:
version: 2
build-and-test:
jobs:
- build
実践的なモノレポ構成
大規模アプリケーション
monorepo/
├── apps/
│ ├── web/ # Next.jsメインアプリ
│ ├── admin/ # 管理画面
│ ├── mobile/ # React Native
│ └── api/ # Express API
├── packages/
│ ├── ui/ # 共有UIコンポーネント
│ ├── utils/ # ユーティリティ関数
│ ├── config/ # 共通設定
│ ├── database/ # Prismaスキーマ
│ └── types/ # 共通型定義
├── tools/
│ ├── eslint-config/ # ESLint設定
│ ├── tsconfig/ # TypeScript設定
│ └── jest-config/ # Jest設定
├── turbo.json
└── package.json
// turbo.json(大規模構成)
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": [
"**/.env.*",
"tsconfig.json"
],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"dist/**",
".next/**",
"build/**",
"android/app/build/**"
]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"]
},
"test:e2e": {
"dependsOn": ["build", "^build"]
},
"lint": {
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
マイクロフロントエンド
microfrontends/
├── apps/
│ ├── shell/ # メインシェル
│ ├── product/ # 商品ページ
│ ├── cart/ # カート
│ └── checkout/ # チェックアウト
├── packages/
│ ├── shared-components/
│ ├── shared-state/ # Zustand/Redux
│ └── design-tokens/ # デザイントークン
└── turbo.json
// apps/shell/package.json
{
"name": "shell",
"dependencies": {
"@repo/shared-components": "workspace:*",
"@repo/design-tokens": "workspace:*",
"next": "^14.1.0",
"react": "^18.2.0"
},
"scripts": {
"build": "next build",
"dev": "next dev -p 3000"
}
}
// apps/shell/app/page.tsx
import { Button } from '@repo/shared-components';
import { colors } from '@repo/design-tokens';
export default function Home() {
return (
<div>
<h1 style={{ color: colors.primary }}>Shell App</h1>
<Button>Click me</Button>
<iframe src="http://localhost:3001/product" />
<iframe src="http://localhost:3002/cart" />
</div>
);
}
デザインシステム構築
UIライブラリパッケージ
packages/ui/
├── src/
│ ├── components/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ └── Button.stories.tsx
│ │ ├── Input/
│ │ └── Card/
│ ├── hooks/
│ └── index.tsx
├── package.json
└── tsconfig.json
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"exports": {
".": "./src/index.tsx",
"./button": "./src/components/Button/Button.tsx"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"test": "jest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@storybook/react": "^7.6.0",
"@types/react": "^18.2.0",
"jest": "^29.7.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
}
// packages/ui/src/components/Button/Button.tsx
export interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
children: React.ReactNode;
onClick?: () => void;
}
export const Button = ({
variant = 'primary',
size = 'medium',
children,
onClick
}: ButtonProps) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
>
{children}
</button>
);
};
Storybookの統合
// turbo.json
{
"pipeline": {
"storybook": {
"cache": false,
"persistent": true
},
"build-storybook": {
"dependsOn": ["^build"],
"outputs": ["storybook-static/**"]
}
}
}
# すべてのStorybookを起動
turbo run storybook
# 特定パッケージのみ
turbo run storybook --filter=@repo/ui
TypeScript設定の共有
共通TypeScript設定
packages/typescript-config/
├── base.json
├── nextjs.json
├── react-library.json
└── package.json
// packages/typescript-config/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"incremental": true
},
"exclude": ["node_modules"]
}
// packages/typescript-config/nextjs.json
{
"extends": "./base.json",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"module": "esnext",
"target": "es5",
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
// apps/web/tsconfig.json
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
ESLint/Prettier設定の共有
packages/eslint-config/
├── next.js
├── react-internal.js
└── package.json
// packages/eslint-config/next.js
module.exports = {
extends: ['next', 'turbo', 'prettier'],
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react/jsx-key': 'off'
},
parserOptions: {
babelOptions: {
presets: [require.resolve('next/babel')]
}
}
};
// apps/web/.eslintrc.json
{
"extends": ["@repo/eslint-config/next"]
}
# すべてのパッケージをLint
turbo run lint
# 自動修正
turbo run lint -- --fix
パフォーマンスベンチマーク
実測データ(中規模モノレポ)
構成:
- apps: 3個(web, admin, api)
- packages: 10個
【従来(Lerna)】
初回ビルド: 3分20秒
2回目(変更なし): 3分18秒
CI(クリーン環境): 3分45秒
【Turborepo導入後】
初回ビルド: 1分10秒(並列化)
2回目(変更なし): 0秒(ローカルキャッシュ)
CI(リモートキャッシュ): 15秒(キャッシュヒット)
改善率: 93%削減(CI環境)
大規模プロジェクト(Vercel公式データ)
Vercel社内モノレポ:
- 50以上のパッケージ
- 20万行以上のTypeScript
従来: 15分
Turborepo: 2分(87%削減)
キャッシュヒット時: 10秒(99%削減)
トラブルシューティング
キャッシュが効かない
# 問題: キャッシュヒットしない
# 原因1: outputs設定漏れ
# turbo.jsonでoutputsを正しく指定
# 原因2: 環境変数の影響
# 不要な環境変数をglobalEnvから除外
# 原因3: タイムスタンプの変動
# .gitignoreに一時ファイルを追加
依存関係のエラー
# 問題: "Cannot find module '@repo/ui'"
# 解決策1: ワークスペース再インストール
pnpm install
# 解決策2: ビルド順序の確認
turbo run build --filter=@repo/ui...
# 解決策3: package.jsonの確認
# "workspace:*" が正しく設定されているか
並列実行の競合
// 問題: 並列実行でファイル競合
// 解決策: 依存関係を明示
{
"pipeline": {
"db:migrate": {
"cache": false
},
"db:seed": {
"dependsOn": ["db:migrate"] // 直列実行を強制
}
}
}
ベストプラクティス
1. パッケージの粒度
良い例(適切な粒度)
packages/
├── ui/ # UIコンポーネント
├── utils/ # ユーティリティ
├── api-client/ # APIクライアント
└── types/ # 共通型定義
悪い例(細かすぎ)
packages/
├── button/ # 1コンポーネントで1パッケージ
├── input/
├── select/
└── ...
2. 共通設定の集約
良い例
tools/
├── eslint-config/
├── tsconfig/
├── jest-config/
└── prettier-config/
悪い例
各パッケージに個別の設定ファイル散在
3. ビルド出力の管理
// 良い例: outputs明示
{
"pipeline": {
"build": {
"outputs": ["dist/**", "build/**", ".next/**"]
}
}
}
// 悪い例: outputs未指定
{
"pipeline": {
"build": {} // キャッシュが効かない
}
}
まとめ
Turborepoは高速モノレポビルドシステムとして、以下の価値を提供します。
主要な利点
- 劇的な高速化 - 並列実行とキャッシュで最大99%のビルド時間削減
- インクリメンタルビルド - 変更されたパッケージのみ再ビルド
- リモートキャッシュ - チーム全体でビルド結果を共有
- 宣言的パイプライン - 複雑な依存関係を簡潔に管理
- 優れた開発者体験 - プロファイリング、フィルタリング、並列制御
採用判断基準
Turborepoを選ぶべき場合:
- 複数パッケージを持つモノレポ
- ビルド時間が課題(3分以上)
- CI/CDコスト削減が必要
- チーム開発での効率化
他の選択肢を検討すべき場合:
- 単一パッケージのプロジェクト
- ビルド時間が十分短い(1分未満)
- 既存Lernaで問題なし
Turborepoは現代的なモノレポ開発において、パフォーマンスと開発者体験の両立を実現する最良の選択肢です。