宇宙の中で一番重い「node_modules」

Naturalclar氏:よろしくお願いします。「node_modulesのブラックホールとの向き合い方」という資料で発表します。Naturalclarです。

はじめにちょっと自己紹介すると、本名はJesseと申します。現在、株式会社stand.fmというところで、音声配信アプリを作っています。React NativeでAndroid・iOS向けのアプリを作っています。また、OSS活動をいろいろとやっていて、「React Native Community」というGithub Organizationの中で、React Nativeの周辺モジュールの開発やReact Native自身のリリースに携わったりしています。

本日は、node_modules black holeにどう対処していくかというトピックです。この画像を見てもわかるとおり、node_modulesはブラックホールよりも重く、この宇宙の中で一番重いものとして知られています。

今回話すことは、けっこう軽めなトピックだと思っているんですが、ふだんnpmやyarnを使っていても解決しないことや、node_modulesを意識的に削減することで何がうれしいのか、そしてそれをどうやって行うのかについて話していこうと思います。

ご存じの方も多いとは思いますが、npm・yarnについて一言で説明すると、dependency managerです。「npm install」や「yarn install」というコマンドを叩くことで、Node.jsを開発するうえで使う外部モジュールをインストールできます。

ふだん開発するうえで「yarn install」「npm run-script」「yarn script」など、なにかしらpackage.jsonの中で作ったコマンドだけを意識すればNode.jsの開発はできると思います。

それらは、指定moduleをsemverに沿ってinstallしてれます。なので、Reactのversion 17をインストールしたい場合「yarn install react」とするとそのReactが降ってきてくれます。いろいろな人が「yarn install」を行った時に、それぞれバージョンが同じになるように、lockファイルを作ってくれます。それが「npm install」なり「yarn install」なりをした時にやってくれることです。

プロジェクトが進むと「node_modules」が重くなる原因

逆に「yarn install」だけだと、何をしてくれないのかというと、使用していないパッケージの削除だったり、例えばReactのversion 16とReactのversion 17が一緒に入っているなど、同じパッケージで重複バージョンがあったりする時に、それを勝手に削除してはくれません。また、publishされているパッケージの不要物の削除はしてくれません。

なので、JSを開発していくうえでプロジェクトを進めていくと、node_modulesは必然的に重くなっていきます。

重くなる原因は、さっきとほぼ重複しているのですが、使用していないパッケージが含まれていたり、複数バージョンが含まれていたり、パッケージの不必要なファイルを含めてpublishしてしまっていたりです。

あと、monorepoという1つのリポジトリの中に複数の子 パッケージを使用する構成にしている時に、共通で使えるパッケージを意図的にhoistするようにしないとhoistされなくて、node_modulesが重くなってしまいます。これについて一つひとつ詳しく説明していきます。

例えばpackage Aにlodashの4.17.20が依存しているとします。package Bがlodashの4.17.21に依存している状態で、package Aとpackage Bをインストールした時に、node_modulesの中は簡単に言うとこんなふうになります。これはバージョンが固定されている場合ですね。

node_modulesの中に、package Bが使っている4.17.21のlodashが入ってきて、package Aもnode_modulesに入ってきます。ただ、lodashのバージョンが違うので、package Aの中のnode_modulesの中に4.17.20のlodashが入ります。こんなふうに複数のlodashが入ってしまうケースがあります。lodash1つにつき4.7MBあるので、こういうケースがたくさんのパッケージで起こっていると、それだけ容量が増えてしまいます。

また、npmにpublishされているパッケージはあくまで人が作ったものなので、その中に不要なファイルが含まれているケースがけっこうあります。例えば、自分が作ったパッケージの使用例で、exampleフォルダみたいなものを作って、そこに使用例を記載して、そのままpublishしてしまった。または、docsがそのパッケージに含まれていて、それをpublishしてしまうと、そのパッケージをインストールした時に、node_modulesの中にexampleやdocsが含まれてしまいます。パッケージを作る人が、きちんと設定をすれば問題ないのですが、すべてのパッケージがそうではありません。こういうのもnode_modulesが重くなる原因です。

さっき言っていたmonorepoのケースですが、例えばTypeScriptは1つで57MBぐらいあります。例えば8個の子パッケージすべてでTypeScriptをインストールしていると、それだけで400MB以上の容量が使われてしまいます。これはmonorepoを使っている時限定になりますが、TypeScriptみたいな共通して使えるパッケージはrootに置いて、そこのTypeScriptをすべての子パッケージで使うようにしないと、node_modulesの容量がかなり膨れ上がってしまいます。

削減するメリット「インストール時間の削減」「CI時間の短縮」「コード負債の防止」

逆に、削減されていると何がうれしいのかについて説明します。簡単に言うと、インストール時間の削減、CI時間の短縮、monorepoのバージョンのずれによるエラー防止です。あと、コードの負債の防止などが挙げられます。

例えばインストール時間の削減。これは当たり前ですが、モジュールが増えれば、それだけインストール時間も増えます。一時期、僕がやっていたプロジェクトで、全体的にnode_modulesが6~7GBぐらいあって「yarn install」するだけで10分ぐらいかかっていました。それは後述するいろいろな削減を行ったうえで、2分ぐらいに削減できました。

また、CI時間の短縮もメリットの1つとして挙げられます。GitHub Actionsなどの従量課金制のCIサービスを使っている場合は、CI時間が延びれば延びるほど払うお金も増えてくるので、CIの時間はできるだけ抑えたいというのがあります。node_modulesの中を小さくすれば小さくするほどCIの時間も短くなって、より払うお金を減らせます。

あと、特定のパッケージに限った話なんですが、複数のバージョンが存在していると、それだけでエラーが起きてしまうパッケージがあります。僕がよく使っているReactのversion 16、React Native、webpackとかは、複数のバージョンがnode_modulesに含まれてしまうと、それだけでエラーが起きてしまうケースがあり、そういうバージョンも統一することでエラーを削減できます。

node_modulesをアップデートすることを常に意識することによって、コードの負債の防止になります。世の中、アップデートするだけでも大変なパッケージがいろいろとあります。React Nativeの場合、マイナーバージョンなどがアップデートした時に、それだけでは動かないケースが多々あるんですが、意識的にアップデートしていくことによって、あとでアップデートするのが大変ということがなくなります。

削減のための4ステップ

削減する方法は大きく分けて4つのステップがあります。まず、どの依存モジュールが容量を食っているかを把握する必要があります。そのうえで使用されていない依存modulesを削除したり、複数バージョンが入っているパッケージのバージョンを統一します。最後に、package上で必要ないファイルが含まれていたら、それを削除します。そのためのいろいろなツールや手法を紹介します。

どのmodulesが容量を食っているかを把握するツールはいろいろあるんですが、僕の場合は「ncdu」を使っています。画像で表示しているのは、僕がやっているとあるプロジェクトのmonorepoですが、これは確か12個ぐらいの子パッケージが入っている巨大なプロジェクトで、「yarn install」した時に最終的にnode_modulesを1.4GBに抑えられています。

また、複数バージョンのパッケージを把握するには、yarn.lockやpackage-lock.jsonを見ます。これはlodashの例ですが、lodashのsemverがいろいろと入っていて、ここにlodash 4.17.20が入っています。その次の行に、lodashの4.17.21や4.3.0が入っていて、こっちは4.17.21がインストールされているみたいなケースがあります。

ただ、これはsemverを見てみるとすべて同じバージョンを指しているので、削減できるはずです。なので、ここはyarn.lockやpackage-lock.jsonのこの部分を削除して、もう一度インストールすれば、この部分が削減されます。これだと、この部分のlodashが削減されるので、それだけで30MB削減できます。

まとめると、JavaScriptを開発するうえでnpm/yarn installすることは避けられないので、そこを最適化する必要があります。インストールの時間がかかりすぎるとそれだけでDXが悪くなるし、CIでの経費削減もできないので、ここらへんはぜひやってもらいたいと思います。

package-lock.jsonやyarn.lockは、一見人間には読めないような羅列になっているかもしれませんが、これはレビュー可能であると伝えたかったです。ちょっと駆け足になってしまいましたが、ご清聴ありがとうございました。