こんにちは、 Web フロントエンドエンジニアの @progfay です。 今回は PR に紐づいたプレビュー環境のビルドに 10 分半かかっていたところを 3 分半ほどまでに短縮した改善活動についてお話しします。
技術改善 Day
私の所属するスタディサプリ中学講座の開発プロジェクト (通称: tara) では「技術改善 Day」を 2 週間に 1 回実施しています。 「技術改善 Day」とは、案件開発を進めていく中で出てきた技術的負債の解消に丸 1 日チームで集中して取り組む日です。
tara 内の Web フロントエンドメンバーで解消したい技術的負債を考えたところ、その中の一つに Web フロントエンドアプリケーションのビルドに時間がかかっている問題 が挙がりました。
tara プロジェクトではデバッグや QA を効率的に行うために PR ごとに紐づいたプレビュー環境を用意しています。 私たちは Next.js を使った Single Page Application を開発しており、 Web フロントエンド単体でビルドが可能です。 しかしながら、このビルド処理に 10 分以上かかっていたため、待ち時間が長すぎて作業が滞りがちになっていました。
そこで、我々は PR 環境のビルドにかかる時間を短縮しようと試みました。
現状を把握しよう
まずはビルドの各ステップにかかる時間を眺めてみることにしました。 ビルドは PR 環境への push を契機に GitHub Actions 上で以下の処理が実行されています。
docker build
で Web フロントエンドアプリケーションを Docker Image としてビルドする- ビルドした Image を ECR に push する
この一連の流れが 10 分 30 秒ほどかかる中、 10 分間は docker build で Image を作成するステップに費やされていました。
Docker Build のログを眺めてみる
GitHub Actions の log を、時間を測りながら見ていくといくつかのことに気付きました。
taking snapshot of full filesystem...
という log と共に処理が 1 分ほど止まってる- さらにこれが 4 回もある
yarn install
が 2 回実行されていて、それぞれ 1 分ほどかかっていた
Dockerfile を確認してみる
どうして yarn install
を 2 回も実行する必要があるのかを調べるために、ビルドに使われる Dockerfile
を確認することにしました。
# 当時のコードと全く同じではないですが、大まかな流れは同じになっています FROM node:16.9.0 AS base FROM base AS builder COPY package.json yarn.lock tsconfig.json ./ RUN yarn install --frozen-lockfile --non-interactive COPY . RUN yarn build FROM base AS prod-server WORKDIR /app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile --non-interactive --production COPY --from=builder /app/.next ./.next COPY --from=builder /app/next.config.js ./ FROM nginx:1.19 WORKDIR /app COPY --from=builder /app/build . COPY --from=builder /app/nginx /etc/nginx
ビルドには Multi-stage Build が用いられています。
yarn install
は builder
と prod-server
の中でそれぞれ呼ばれていました。
前者の builder
では、 yarn build
のために必要な packages を取得するために yarn install
を実行しているようです。
後者の prod-server
では、 Next.js の Server を Production 相当で起動するための stage のようです。
というのも、 tara プロジェクトでは開発初期の段階では Next.js の Server を使う予定でした。
それが途中から Static HTML Export の機能を使う路線に切り替えた際に、 Dockerfile
を正しく整理しきれていないせいで prod-server
Stage が残ってしまっていたようです。 (実際にこの処理を書いたのは過去の自分でした...)
要らない処理は削除してしまおう
prod-server
Stage は現在では利用されていないため、完全に削除することにしました。
これによりビルドにかかる処理は 3 分ほど短縮されました。
その他にも、 Visual Regression Test に使用している snapshot 画像を COPY
しないように .dockerignore
に追記したり、 Next.js 12.3 の SWC Minifier を試すなどして、ビルドに係る処理を 6 分前後に短縮できました。
まだまだ速くしたい
当初のビルド時間から 4 分ほど短縮されましたが、 push するたびに 6 分も待たされるのはまだまだ早いとは言えません。 そこで、更なるビルド時間短縮を目指して追加の高速化に着手しました。
これにあたり、作業は技術改善 Day ではなく、日々の業務の中で進めることにしました。 理由としては、技術改善 Day を契機に負債を解消することへの意識を高めて日常的に改善していく文化を生み出したいからです。 技術改善 Day は負債を解消するいい機会ではあるものの、普段の開発の中で継続して改善したほうが持続可能性が高く理想的だと考えています。 そのための第一歩として、率先して技術改善 Day 以外で細々と負債を解消していく活動をしていくことにしました。
個人的な感想ですが案件とは関係のない改善系は優先度が低いため、作業へのモチベーションもメンバーからの認知度も下がりがちです。 せっかくなら楽しみながら改善に取り組みたいという思いから、自分は改善タスクに馴染みやすいタイトルをつけるなどしています。 今回は高速化を目指していたため、大げさに "Beyond the Lightning..." という副題をつけて改善に取り組みました。
次のボトルネックは?
さて、 6 分前後のビルドの中で一番のボトルネックとなっている部分はどこになっているでしょうか。
時間を計測してみると next build && next export
に 3 分ほど掛かっていることがわかりました。
そこで Next.js の CI Build Caching を試してみましたが、実行時間には差が出ませんでした。 なぜ速くならないのかを探るために複数回実行して log を眺めてみると、ビルド step 前に以下の message が表示されていました。
Cache not found for input keys: Linux-nextjs-0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
また、ビルド step 後には以下の message が表示されていました。
Warning: Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.
next build
を実行しているのだから .next/cache
は作られているはずですが、 GitHub Actions 側からは見つからないと言われています。
そんなはずはないと思考を巡らせた結果、 next build
は Docker 内で実行されているため .next/cache
も Docker の中で生成され、 GitHub Actions からはアクセスできないことに気が付きました。
next build
を効かせるためには Docker の外側に .next/cache
を吐き出すか next build
自体を Docker 外で行うかのいずれかが必要です。
Dockerfile
は local 環境でデバッグする際にも使われていたため、 CI 上でのビルドには不要な処理も含まれてしまっていました。
これらを省くことで更なる速度改善が行えると見込み、 CI 上でビルドする際に使用する Dockerfile
を分けました。
またビルドに必要な環境は Docker を使わずとも揃っていたことから、 Docker 外で next build
を行うように修正をしました。
結果として Next.js の CI Build Caching が効き、 next build && next export
にかかる時間は 1 分未満になりました。
全体的なビルド時間は 3 分半ほどで終わるようになりました。
今後の展望
今の全体の処理とそれに掛かる時間は以下の通りです。
- setup-node, yarn や Next.js の cache 読み込み: 65 秒前後
yarn install
: 65 秒前後next build && next export
: 50 秒前後- image build & push: 30 秒前後
主に yarn cache dir
や yarn install
に時間がかかっているため、ここを改善することで更なる高速化が見込めそうです。
また、 Turbopack など Next.js の build performance 最適化にも期待していきたいところです。
おわりに
当初は PR 環境のビルドに 10 分半かかっていたのが、今では 3 分半まで短縮されました。
この改善活動について自分の上司に報告したら「全然 Lightning じゃないじゃん」ツッコまれてしまいましたが、進行の邪魔にならない限りはタスクに自分達が楽しめる名前を付けたりしながらワイワイするのは改善活動を文化として根付かせる活動として有益なのかもしれません。
みなさんにも是非、 "Beyond the Lightning..." などと銘打って余計な時間を削っていただければ幸いです。