接客現場のアナログな業務を効率化する デジちゃいむ というサービスを開発している、WASD Inc. の shinnoki です。
デジちゃいむでは Yarn Workspaces を使用した monorepo で React / React Native プロジェクトの開発をしているのですが、yarn install
に時間がかかってしまうという問題がありました。
Yarn 2 (berry) に移行したところ yarn install
の時間が大幅に短縮され、ローカルの開発環境や CI の実行時間が改善されたのでご紹介します。
移行の背景
Vercel 渋滞問題
Vercel の monorepo 機能を使って複数のフロントエンドをデプロイしています。
この機能はとても便利で、数クリックで連携が完了して GitHub のプルリクエストに対して自動でプレビュー環境をデプロイしてコメントをつけてくれます。
1つのプルリクエストに対してい紐付いているプロジェクトのデプロイが同時に走るのですが、それぞれ7分くらいかかってしまって渋滞してしまう問題が発生しました。
特にスプリントレビューの直前に駆け込みでマージした場合に、デプロイが渋滞してレビューできないため不要なキューをキャンセルするということが多発していました。
ログを確認すると yarn install
が本来ならキャッシュが効いて短時間で終わるはずなのですが5分くらいかかってしまっていました。
Vercel の monorepo 機能は Root Directory という設定を使うのですが、おそらく Root Directory 外の node_modules
をキャッシュしてくれていないことによって毎回 Link step が動いてしまっているという状況でした。
もちろん Vercel のビルドインスタンスを追加するという方法もあるのですが、まずは時間がかかっているのを解消しないと根本解決にはならないため対策を打つ必要がありました。
Nx への移行は懸念が残った
Nx という monorepo 用のフレームワークがあり、 React Native にも対応しているためこちらの採用することも案に上がりました。
Nx では各プロジェクト内に package.json を作らず一つの node_modules で全ての依存関係を管理し、 Vercel にデプロイするときにも Root Directory を使用しないため発生していたキャッシュの問題は解決します。
一方で各プロジェクトで個別にライブラリのバージョンを管理できないので「影響の少ないプロジェクトだけ先にアップデートして段階的に移行したい」ということができなくなるというのが大きな懸念でした。
これに対する Issue も複数上がっていますが、 Nx は single-version policy という思想を採用しているので、これは向き不向きの問題なのかなと思います。
Using different versions of a lib or a package · Issue #309 · nrwl/nx · GitHub package.json per app · Issue #1777 · nrwl/nx · GitHub
移行コストの少ない方法でどうにか yarn install
を高速化できないかと模索した結果 Yarn 2 への移行に行き着きました。
Yarn 2 の使い方
Yarn 2 の有効化
Installation | Yarn - Package Manager
実はすでに yarn
コマンドが入っているだいたいの環境ではすぐに Yarn 2 を使うことができます。
Yarn 2 を有効化したいプロジェクト内で
$ yarn set version berry
を実行すると .yarnrc.yml
と .yarn
が作成され Yarn 2 が有効になります。
このファイルをコミットすることで、他の環境でも自動的に Yarn 2 が有効化されます。
Plug'n'Play (PnP) vs node-modules
Plug'n'Play | Yarn - Package Manager
Yarn 2 には .pnp.cjs
というファイルを使用することで node_modules が不要になる PnP という機能があります。
PnP の対応は徐々に進んでいるものの、 React Native をはじめとして node_modules のパスに依存した仕組みの場合どうしても使用が難しくなります。
その場合 .yarnrc.yml
に
nodeLinker: node-modules
を追加することで PnP を使用せず従来のように node_modules ディレクトリが作成されます。
今回は React Native のプロジェクトを含んでいたため node-modules を使用しましたが、機会があれば PnP も使ってみたいです。
Zero-Installs
Zero-Installs | Yarn - Package Manager
キャッシュを Git の管理下に含めることで yarn install
を高速化する機能です。
PnP とは独立した別の概念で、機能というよりかは .yarn/cache
の中にある大量の zip ファイルを gitignore に含めるかどうかの違いです。
今回は300MB程度であったため問題ないと判断し試しに Git の管理下にいれてみていますが yarn install
がとても早くなり快適な一方、プルリクエストの Files Changed が多くなってしまいレビューのノイズになってしまうのはデメリットだと感じています。
Zero-Installs を有効にするかどうかは意見が分かれると思っていて、必要に応じて設定を見直したいと思います。
対応が必要だったもの
wsrun を yarn workspaces foreach
に移行
開発時にバックエンドとフロントエンドを同時に立ち上げる必要があるのですが、手間を減らすために wsrun を利用して yarn start
で一通り必要なプロジェクトが立ち上がるような設定をしていました。
Yarn 2 では wsrun と同じことができる yarn workspaces foreach
というコマンドが提供されています。
`yarn workspaces foreach` | Yarn - Package Manager
利用するにはまず
$ yarn plugin import workspace-tools
で workspace-tools
プラグインを有効化する必要があります。
今回の用途の場合は --interactive
--parallel
--verbose
オプションを追加して
$ yarn workspaces foreach -ipv --include <projectの正規表現> run start
のような使い方をするといい感じにコンソール出力してくれます。
patch-package を patch:
に移行
ライブラリに不具合があったり中身を少し書き換えたい場合、今まで patch-package というツールを使ってパッチを当てていました。
patch-package はとても重宝していたのですが、 Yarn 2 では dependencies のバージョンに patch:
プロトコルを指定できるようになりライブラリにパッチを当てる機能が標準で用意されるようになりました。
Protocols | Yarn - Package Manager
patch-package ではパッチ内のパスが node_modules/<パッケージ名>/
から始まっていたのが patch:
ではパッケージ内のパスから始まるようになったというところだけ書き換えれば、今までのパッチをそのまま利用可能でした。
プロジェクト間参照を workspace:*
に変更
今まで yarn workspace 内のプロジェクト間参照も通常のライブラリと同じように dependencies のバージョンを指定する必要がありましたが、 workspace:*
という専用のプロトコルができたのでそちらに移行しました。
今まであまり意味がなくても package.json に version を指定する必要がありましたがその必要がなくなりました。
Serverless Webpack が動かない(未解決)
monorepo 内のバックエンドのコードは Serverless Framework を使って AWS Lambda にデプロイしているのですが、 TypeScript をトランスパイルするために利用している Serverless Webpack プラグインが動かなくなりました。
以下の Issue で議論が進行中です。
パッチを当てたりして手元では何とか動くようになったものの不安が残りますが、今後アーキテクチャを含めて変更する可能性も十分にあるため深追いできていない状況です。
結果
yarn install
が早くなった
yarn install
は以下の4ステップで構成されます。
Architecture | Yarn - Package Manager
- Resolution step:
package.json
とyarn.lock
の状態確認 - Fetch step: キャッシュにないファイルの取得
- Link step: node_modules にファイルを展開
- Build step: ビルドが必要なライブラリのビルド
Yarn 2 移行前後で、キャッシュがある状態とない状態の yarn install
の実行時間を計測してみました。
Yarn 1 は yarn install --verbose
を使うと実行時間にも影響が出ていそうだったため通常実行した上でストップウォッチで計りましたが、 Yarn 2 は yarn install
コマンド内で実行時間を出してくれるためその値を使っています。
それぞれサンプル数1で特に Link step と Buid step は IO に左右されるためでバラつきがありますが、 Yarn 2 では概ね60%程度時間が削減されています。
Buid step に時間がかかってしまっているのは node-sass のビルドが走ってしまっていたという別の問題があり、現在は更に見直して yarn install
全体を通して1分程度に収まるようになっています。
Vercel の渋滞で困らなくなった
yarn install
が高速化したことによりもともとのきっかけだった Vercel のビルド時間も改善されましたが、やはり Link step が動いてしまっているため node_modules のキャッシュには失敗してしまっているようです。
monorepo 機能のキャッシュが改善されるのが一番嬉しいですが、現状でも yarn workspaces focus
コマンドを活用したり Build Command を工夫することによってさらなるチューニングができそうです。
十分にビルド時間が短くなったことにより今のところ渋滞は解消されたので、また実害が出たら調査しようと思います。
コンソールの表示がかっこいい
Gitのマージ時に yarn.lock
にコンフリクトが発生した場合 yarn install
を行うと自動的にコンフリクトを解消してくれます。
この機能自体は Yarn 1 からあったのですが Yarn 2 ではこの機能のコンソール表示がカッコいいと社内で評判です。
まとめ
Yarn 2 のリリースから1年以上経ちましたが、デフォルトで有効になっていなかったり PnP の印象が強かったりするのでまだ使用していないプロジェクトも多いと思います。
React Native を含みプロジェクト間参照もある比較的複雑な monorepo プロジェクトですが、 node-modules プラグインを使うことで移行することができました。
開発スピードを保つためには定期的な開発環境のメンテナンスが欠かせません。
PnP や Zero-Install を使わないでも yarn install
が高速化し開発スピードに貢献できる可能性があるので移行を検討してみてはいかがでしょうか。
WASD TECH BLOG では React, TypeScript, GraphQL, monorepo について発信していきますので、よろしくお願いいたします!
参考リンク
- Home | Yarn - Package Manager
- 公式ページ
- yarnをv1からv2(Berry)へ移行する
- 移行の際に大変参考になりました
- YarnをYarn 2(berry)にアップグレードした話|holly(ホリー)|note
- 同じく React Native を使っている stand.fm さんも最近 yarn 2 に移行したようです