React / React Native の monorepo を Yarn 2 (berry) に移行したら快適になった話

接客現場のアナログな業務を効率化する デジちゃいむ というサービスを開発している、WASD Inc. の shinnoki です。

デジちゃいむでは Yarn Workspaces を使用した monorepo で React / React Native プロジェクトの開発をしているのですが、yarn install に時間がかかってしまうという問題がありました。

Yarn 2 (berry) に移行したところ yarn install の時間が大幅に短縮され、ローカルの開発環境や CI の実行時間が改善されたのでご紹介します。

移行の背景

Vercel 渋滞問題

Vercel の monorepo 機能を使って複数のフロントエンドをデプロイしています。

vercel.com

この機能はとても便利で、数クリックで連携が完了して GitHub のプルリクエストに対して自動でプレビュー環境をデプロイしてコメントをつけてくれます。

f:id:konoki_nannoki:20210530143951p:plain
GitHubコメントにVercelのプレビューが表示される

1つのプルリクエストに対してい紐付いているプロジェクトのデプロイが同時に走るのですが、それぞれ7分くらいかかってしまって渋滞してしまう問題が発生しました。

特にスプリントレビューの直前に駆け込みでマージした場合に、デプロイが渋滞してレビューできないため不要なキューをキャンセルするということが多発していました。

ログを確認すると yarn install が本来ならキャッシュが効いて短時間で終わるはずなのですが5分くらいかかってしまっていました。

Vercel の monorepo 機能は Root Directory という設定を使うのですが、おそらく Root Directory 外の node_modules をキャッシュしてくれていないことによって毎回 Link step が動いてしまっているという状況でした。

もちろん Vercel のビルドインスタンスを追加するという方法もあるのですが、まずは時間がかかっているのを解消しないと根本解決にはならないため対策を打つ必要がありました。

Nx への移行は懸念が残った

Nx という monorepo 用のフレームワークがあり、 React Native にも対応しているためこちらの採用することも案に上がりました。

nx.dev

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 の有効化

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

のような使い方をするといい感じにコンソール出力してくれます。

f:id:konoki_nannoki:20210530155647p:plain
yarn workspaces foreach -ipv のコンソール出力

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 で議論が進行中です。

github.com

パッチを当てたりして手元では何とか動くようになったものの不安が残りますが、今後アーキテクチャを含めて変更する可能性も十分にあるため深追いできていない状況です。

結果

yarn install が早くなった

yarn install は以下の4ステップで構成されます。

Architecture | Yarn - Package Manager

  • Resolution step: package.jsonyarn.lock の状態確認
  • Fetch step: キャッシュにないファイルの取得
  • Link step: node_modules にファイルを展開
  • Build step: ビルドが必要なライブラリのビルド

Yarn 2 移行前後で、キャッシュがある状態とない状態の yarn install の実行時間を計測してみました。

Yarn 1 は yarn install --verbose を使うと実行時間にも影響が出ていそうだったため通常実行した上でストップウォッチで計りましたが、 Yarn 2 は yarn install コマンド内で実行時間を出してくれるためその値を使っています。

f:id:konoki_nannoki:20210530211852p:plain
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 ではこの機能のコンソール表示がカッコいいと社内で評判です。

f:id:konoki_nannoki:20210530144452p:plain
yarn.lockのコンフリクト解消

まとめ

Yarn 2 のリリースから1年以上経ちましたが、デフォルトで有効になっていなかったり PnP の印象が強かったりするのでまだ使用していないプロジェクトも多いと思います。

React Native を含みプロジェクト間参照もある比較的複雑な monorepo プロジェクトですが、 node-modules プラグインを使うことで移行することができました。

開発スピードを保つためには定期的な開発環境のメンテナンスが欠かせません。

PnP や Zero-Install を使わないでも yarn install が高速化し開発スピードに貢献できる可能性があるので移行を検討してみてはいかがでしょうか。

WASD TECH BLOG では React, TypeScript, GraphQL, monorepo について発信していきますので、よろしくお願いいたします!

参考リンク