Turborepo でモノレポ構成のプロジェクトを爆速でビルドする

2022.02.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Turborepo Demo and Walkthrough (High-Performance Monorepos)という動画で紹介されていた内容を試してみました。

Turborepo とは

Turborepoは Vercel が開発した JavaScript/TypeScript のモノレポ環境に特化したビルドツールです。yarn, npm, pnpm を利用している環境で利用可能です。

workspaces を利用してモノレポ環境で CI を実行する際、不要なリソースにまでコンパイル・ビルド・テストが実行されてしまいプロジェクトがスケールするにつれ CI にかかる時間が膨大になってしまいます。Turborepo はこの問題を解決するためのツールになります。

本エントリでご紹介する Turborepo の主要機能は以下です:

  • 変更に対して必要な部分のみビルドを行う
  • レポジトリ全体の依存をグラフで出力する
  • リモートキャッシュ機能で最新のビルドのキャッシュをクラウド環境(Vercel)へ保存しチームで利用できる(Beta)

Turborepo 自体は Go 言語 で作られています。

変更点に対して必要な部分のみビルドを行う

公式が用意しているサンプルプロジェクトを利用して Turborepo を試します。

サンプルには 2 つの Next.js アプリケーションと共通で利用する設定ファイルとコンポーネント集が含まれます。

npx create-turbo@latest
# ./my-turborepoにサンプルプロジェクトが作成されます
# - apps/web: Next.js with TypeScript
# - apps/docs: Next.js with TypeScript
# - packages/ui: Shared React component library
# - packages/config: Shared configuration (ESLint)
# - packages/tsconfig: Shared TypeScript `tsconfig.json`

サンプルプロジェクト構造

cd my-turborepo
tree -I node_modules
.
├── README.md
├── apps
│   ├── docs
│   │   ├── README.md
│   │   ├── next-env.d.ts
│   │   ├── next.config.js
│   │   ├── package.json
│   │   ├── pages
│   │   │   └── index.tsx
│   │   └── tsconfig.json
│   └── web
│       ├── README.md
│       ├── next-env.d.ts
│       ├── next.config.js
│       ├── package.json
│       ├── pages
│       │   └── index.tsx
│       └── tsconfig.json
├── package.json
├── packages
│   ├── config
│   │   ├── eslint-preset.js
│   │   └── package.json
│   ├── tsconfig
│   │   ├── README.md
│   │   ├── base.json
│   │   ├── nextjs.json
│   │   ├── package.json
│   │   └── react-library.json
│   └── ui
│       ├── Button.tsx
│       ├── index.tsx
│       ├── package.json
│       └── tsconfig.json
├── turbo.json
└── yarn.lock

ビルド設定

Turborepo の設定は Root にある package.json 内に記述されています。 workspacesで設定しているapp/packages/配下にあるプロジェクト(web, docs, config, ui, tsconfig)がスコープに含まれます。

package.json

{
  "name": "turborepo-basic-shared",
  "version": "0.0.0",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "lint": "turbo run lint",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "prettier": "^2.5.1",
    "turbo": "latest"
  },
  "engines": {
    "npm": ">=7.0.0",
    "node": ">=14.0.0"
  },
  "packageManager": "yarn@1.22.11"
}

ビルドを実行してみる

このままyarn turbo run buildを実行してみます。初回なので app, docs の両方のビルドが走ります。

my-turborepo git:(tutorial-1) ✗ yarn turbo run build
yarn run v1.22.11
$ /blogs/2022/Feb/Sandbox/turborepo/src/my-turborepo/node_modules/.bin/turbo run build
• Packages in scope: config, docs, tsconfig, ui, web
• Running build in 5 packages
docs:build: cache miss, executing 3dd856e65f14080c
web:build: cache miss, executing 03337e8e78ba011c
web:build: $ next build
docs:build: $ next build
docs:build: Attention: Next.js now collects completely anonymous telemetry regarding usage.
docs:build: This information is used to shape Next.js' roadmap and prioritize features.
docs:build: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
docs:build: https://nextjs.org/telemetry
docs:build:
web:build: Attention: Next.js now collects completely anonymous telemetry regarding usage.
web:build: This information is used to shape Next.js' roadmap and prioritize features.
web:build: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
web:build: https://nextjs.org/telemetry
web:build:
docs:build: info - Checking validity of types...
web:build: info - Checking validity of types...
web:build: info - Creating an optimized production build...
docs:build: info - Creating an optimized production build...
web:build: info - Compiled successfully
web:build: info - Collecting page data...
docs:build: info - Compiled successfully
docs:build: info - Collecting page data...
docs:build: info - Generating static pages (0/3)
web:build: info - Generating static pages (0/3)
docs:build: info - Generating static pages (3/3)
web:build: info - Generating static pages (3/3)
docs:build: info - Finalizing page optimization...
docs:build:
docs:build: Page Size First Load JS
docs:build: ┌ ○ / 305 B 71.3 kB
docs:build: └ ○ /404 193 B 71.2 kB
docs:build: + First Load JS shared by all 71 kB
docs:build: ├ chunks/framework-71b7ac9748a53568.js 42 kB
docs:build: ├ chunks/main-24e9726f06e44d56.js 26.9 kB
docs:build: ├ chunks/pages/\_app-2ad88a182aea1df3.js 1.37 kB
docs:build: └ chunks/webpack-45f9f9587e6c08e1.js 729 B
docs:build:
docs:build: ○ (Static) automatically rendered as static HTML (uses no initial props)
docs:build:
web:build: info - Finalizing page optimization...
web:build:
web:build: Page Size First Load JS
web:build: ┌ ○ / 304 B 71.3 kB
web:build: └ ○ /404 193 B 71.2 kB
web:build: + First Load JS shared by all 71 kB
web:build: ├ chunks/framework-71b7ac9748a53568.js 42 kB
web:build: ├ chunks/main-24e9726f06e44d56.js 26.9 kB
web:build: ├ chunks/pages/\_app-2ad88a182aea1df3.js 1.37 kB
web:build: └ chunks/webpack-45f9f9587e6c08e1.js 729 B
web:build:
web:build: ○ (Static) automatically rendered as static HTML (uses no initial props)
web:build:

Tasks: 2 successful, 2 total
Cached: 0 cached, 2 total
Time: 7.996s

✨ Done in 8.69s.

次に同じコマンドを再度実行してみます。

yarn turbo run build
yarn run v1.22.11
$ /blogs/2022/Feb/Sandbox/turborepo/src/my-turborepo/node_modules/.bin/turbo run build
• Packages in scope: config, docs, tsconfig, ui, web
• Running build in 5 packages
docs:build: cache hit, replaying output 3dd856e65f14080c
docs:build: $ next build
docs:build: Attention: Next.js now collects completely anonymous telemetry regarding usage.
docs:build: This information is used to shape Next.js' roadmap and prioritize features.
docs:build: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
docs:build: https://nextjs.org/telemetry
docs:build:
docs:build: info  - Checking validity of types...
docs:build: info  - Creating an optimized production build...
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build:
docs:build: Page                                       Size     First Load JS
docs:build: ┌ ○ /                                      305 B          71.3 kB
docs:build: └ ○ /404                                   193 B          71.2 kB
docs:build: + First Load JS shared by all              71 kB
docs:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
docs:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
docs:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
docs:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
docs:build:
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build:
web:build: cache hit, replaying output 03337e8e78ba011c
web:build: $ next build
web:build: Attention: Next.js now collects completely anonymous telemetry regarding usage.
web:build: This information is used to shape Next.js' roadmap and prioritize features.
web:build: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
web:build: https://nextjs.org/telemetry
web:build:
web:build: info  - Checking validity of types...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build:
web:build: Page                                       Size     First Load JS
web:build: ┌ ○ /                                      304 B          71.3 kB
web:build: └ ○ /404                                   193 B          71.2 kB
web:build: + First Load JS shared by all              71 kB
web:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
web:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
web:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
web:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
web:build:
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build:

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    163ms >>> FULL TURBO

✨  Done in 0.38s.

前回のキャッシュが参照され、ビルドがスキップされました。

アプリケーションの片方に変更を加えてみる

apps/配下のwebに変更を加え再度ビルドコマンドを実行してみます。

import {Button} from "ui";

export default function Web() {
  return (
    <div>
      <h1>Web APP</h1>/* h1の文字列をWEB -> WEB APPへ変更*/
      <Button />
    </div>
  );
}

結果:

✗ yarn turbo run build
yarn run v1.22.11
$ /blogs/2022/Feb/Sandbox/turborepo/src/my-turborepo/node_modules/.bin/turbo run build
• Packages in scope: config, docs, tsconfig, ui, web
• Running build in 5 packages
web:build: cache miss, executing 96f1e5e0ec51b434
docs:build: cache hit, replaying output 3dd856e65f14080c
docs:build: $ next build
docs:build: Attention: Next.js now collects completely anonymous telemetry regarding usage.
docs:build: This information is used to shape Next.js' roadmap and prioritize features.
docs:build: You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
docs:build: https://nextjs.org/telemetry
docs:build:
docs:build: info  - Checking validity of types...
docs:build: info  - Creating an optimized production build...
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build:
docs:build: Page                                       Size     First Load JS
docs:build: ┌ ○ /                                      305 B          71.3 kB
docs:build: └ ○ /404                                   193 B          71.2 kB
docs:build: + First Load JS shared by all              71 kB
docs:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
docs:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
docs:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
docs:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
docs:build:
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build:
web:build: $ next build
web:build: info  - Checking validity of types...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build:
web:build: Page                                       Size     First Load JS
web:build: ┌ ○ /                                      308 B          71.3 kB
web:build: └ ○ /404                                   193 B          71.2 kB
web:build: + First Load JS shared by all              71 kB
web:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
web:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
web:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
web:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
web:build:
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build:

 Tasks:    2 successful, 2 total
Cached:    1 cached, 2 total
  Time:    4.681s

✨  Done in 5.12s.

webのみがビルドされ変更を加えていないdocsのビルドはスキップされました。ビルド時間も全体の半分に短縮されました。

レポジトリ全体の依存をグラフで出力する

--graphオプションをつけることで依存関係の図の出力ができます。 イメージの生成にはgraphvizがインストールしてある必要があります。

# graphvizをダウンロード
brew install graphviz
# 依存関係のグラフを生成
yarn turbo run build --graph

✔ Generated task graph in graph-1645622506364370000.jpg
✨  Done in 3.66s.

graph-1645622506364370000.jpg

appdocsという2つの Next.js アプリケーションはどちらもui/Button.tsxのコンポーネントを参照しているのが図で確認できます。

リモートキャッシュ機能で最新のビルドのキャッシュをクラウド環境(Vercel)へ保存しチームで利用できる

ビルドアーティファクトを Vercel に保存しビルドキャッシュをチームで共有することができるリモートキャッシング機能(Beta)を利用することができます。

login コマンドで Vercel アカウントにログインします。

✗ npx turbo login
>>> Opening browser to https://vercel.com

    Waiting for your authorization...
>>> Success! Turborepo CLI authorized for xxxxxx@classmethod.jp

次にリモートキャッシュを利用するための設定を行います。

npx turbo link
>>> Remote Caching (beta)

  Remote Caching shares your cached Turborepo task outputs and logs across
  all your team’s Vercel projects. It also can share outputs
  with other services that enable Remote Caching, like CI/CD systems.
  This results in faster build times and deployments for your team.
  For more info, see https://turborepo.org/docs/features/remote-caching

? Would you like to enable Remote Caching for "~/blogs/2022/Feb/Sandbox/turborepo/src/my-turborepo"? Yes
? Which Vercel scope (and Remote Cache) do you want associate with this Turborepo? your_vercel_scope

>>> Success! Turborepo CLI authorized for xxxxx

To disable Remote Caching, run `npx turbo unlink`

上の設定でリモートキャッシング機能が有効化されたのでローカルにキャッシュされているビルド履歴(node_modules/.cache/turbo)を削除して再度ビルドコマンドを実行してみます。

# ローカル環境のキャッシュを削除
rm -rf ./node_modules/.cache/turbo

# ビルドコマンドを実行
yarn turbo run build
yarn run v1.22.11
$ /blogs/2022/Feb/Sandbox/turborepo/src/my-turborepo/node_modules/.bin/turbo run build
• Packages in scope: config, docs, tsconfig, ui, web
• Remote computation caching enabled (experimental)
• Running build in 5 packages
docs:build: cache hit, replaying output 3dd856e65f14080c
docs:build: $ next build
docs:build: info  - Checking validity of types...
docs:build: info  - Creating an optimized production build...
docs:build: info  - Compiled successfully
docs:build: info  - Collecting page data...
docs:build: info  - Generating static pages (0/3)
docs:build: info  - Generating static pages (3/3)
docs:build: info  - Finalizing page optimization...
docs:build:
docs:build: Page                                       Size     First Load JS
docs:build: ┌ ○ /                                      305 B          71.3 kB
docs:build: └ ○ /404                                   193 B          71.2 kB
docs:build: + First Load JS shared by all              71 kB
docs:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
docs:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
docs:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
docs:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
docs:build:
docs:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
docs:build:
web:build: cache hit, replaying output 96f1e5e0ec51b434
web:build: $ next build
web:build: info  - Checking validity of types...
web:build: info  - Creating an optimized production build...
web:build: info  - Compiled successfully
web:build: info  - Collecting page data...
web:build: info  - Generating static pages (0/3)
web:build: info  - Generating static pages (3/3)
web:build: info  - Finalizing page optimization...
web:build:
web:build: Page                                       Size     First Load JS
web:build: ┌ ○ /                                      308 B          71.3 kB
web:build: └ ○ /404                                   193 B          71.2 kB
web:build: + First Load JS shared by all              71 kB
web:build:   ├ chunks/framework-71b7ac9748a53568.js   42 kB
web:build:   ├ chunks/main-24e9726f06e44d56.js        26.9 kB
web:build:   ├ chunks/pages/_app-2ad88a182aea1df3.js  1.37 kB
web:build:   └ chunks/webpack-45f9f9587e6c08e1.js     729 B
web:build:
web:build: ○  (Static)  automatically rendered as static HTML (uses no initial props)
web:build:

 Tasks:    2 successful, 2 total
Cached:    2 cached, 2 total
  Time:    2.215s >>> FULL TURBO

Remote computation caching enabled (experimental)と表示されリモート環境のキャッシュを参照しているのが確認できました。ビルドの内容は変わらないのでそのままキャッシュの内容が反映されビルド自体はスキップされました。

リモートキャッシング機能自体は無料で利用できるのでチーム開発にすでに Vercel を利用している場合は嬉しいですね。

References