この記事は「はてなエンジニア Advent Calendar 2022」の11日目の記事です。
10日目は@NanimonoDaemonさんの「CSSのLiveReloadする!開発環境でのCSSのビルドをViteに任せてバックエンドとViteの開発サーバーを連携させる」でした。
はじめに
先日、CyberAgent さんが主催する CyberAgentHack/web-speed-hackathon-2022 に参加しましたので、その時にやったことなどの解説と振り返り記事になります。
Web Speed Hackathon 2022 とは?
"Web Speed Hackathon 2022" は、非常に重たい Web アプリをチューニングして、いかに高速にするかを競う競技です。
「Web Speed Hackathon」はリモート参加型のハッカソンです。 予め準備してある Web アプリケーションのパフォーマンスを改善することで競い合います。
というものになります。
Web Speed Hackathonそのものは今回が初参加ではなく、昨年の2021年は大会終了後に存在を知り、社内の大会で1日チームを組んで取り組んだのが始まりでした。当時は全く解くことができずに悔しい思いをしました。
このまま終わりたくないと勝手に思い、再挑戦という意味も込めて本大会に望み、嬉しいことに8位になることができました。
トップページに限ってはlocal上ですが、満点も出すことができ、そこそこ改善できたんじゃないかと思っています。
チューニングのためにやったこと
自分のコミット
実際のコミットはここから確認できます。VRTが落ちて戻したりしているのでそこまで見やすいものではありませんが、、、
Comparing CyberAgentHack:main...halkt:main · CyberAgentHack/web-speed-hackathon-2022
また、今回はherokuの無料枠でアプリをデプロイして、大会でもずっとこの環境でスコア計測も行いました。
analyzerの導入
まずはmain.jsがでかすぎるので中身をざっと確認するためにanalyzerを導入して実行したところ、
- index.jsxがめちゃくちゃ重いこと
- 次点でzengin.jsがヤバそうなこと
- lodash.jsやmoment.jsなどおなじみのやつらがいること
などがざっくり見てわかりました。
webpack.config.js
にfrontとserverの記述が混ざっていることでBuildができなくなってしまい、戻したりconfigをfrontとserverで分けたりと最終的に結構改造することになりました。
devtoolの変更
index.jsxがバカでかいことになっていましたが、これをすることでめちゃくちゃ小さくなり、main.js
のbundleサイズが減りました。(どれぐらい減ったかメモしておけばよかった)
webpackのmodeをprodに
webpackのmodeをproductionに変更しました。
他にも不要そうなところや怪しい箇所がないかはwebpackのドキュメントを見ながらチューニングをしていきました。
webpack.config.jsをclientとserverに分割
analyzeをdevelopmentでできるようにしたかったので実施。
prodとdevで共通の設定を読み込み際にはwebpack-merge
を使いました。
zengin.jsをchunk
次点でめちゃくちゃ重いzengin-code
はまだこの時点ではよくわかっていなかったので、一旦chunkしてみました。
こちらの根本解決は別途対応していますが、このときは「あとでdynamic import
すれば良いんじゃね?」と思ったので雑にやっています。
トップ画像をwebpに変更
他の画像も軒並みwebpにしたかったのでテスト的に実施しました。
brotli配信を試す
fastifyを使っていたので、fastifyCompress
で圧縮するようにしました。
また、webpackでbuildのタイミングでbrotliファイルを生成しておきそちらを参照するようにしたかったのですがうまく変える方法がわからず、上記に落ち着いたという背景です。
ただ、fastifyでのbrotliへの変換に時間がかかるようで、サーバーレスポンスが極端に遅くなる事態が発生してしまいました。
やっぱりgzip配信にする
上記を受けて、gzipに変更しました。時間があればbrotliにしたかったですがgzipでも十分効果があったのでこのままになっています
画像をすべてwebpにする
ここからようやくスコアが上がってきて90点/500点ぐらいをとれるようになりました。
※画像の生成はsharp
を使いましたがnodeで書いたscriptが雑すぎてcommitできておらず、、、
browserslistを指定
レギュレーションに従いchromeの最新バージョンを指定
zengin.js対応
ここが結構ハマったポイントでした。最初はdynamic importを試そうと思ったのですが、よくよく調べるとAPIがあることに気づきました。
zenigin-codeと違い、支店情報は含まれていなかったので別途APIを叩く必要があることに気づかず地味に時間を溶かしていました。 ただ、この形に変えたことでzengin.jsを削除でき、そこそこ効果的でした。
このあたりからスコアが300点台にのるようになってきました。
moment.jsの削除
おなじみのmoment.jsの削除も対応しました。今回はすべてdayjsで対応できそうだったのでこちらに変換しました。 (めちゃくちゃ今更ですがmoment-timezone.jsをなぜか消し忘れている...)
lodashの削除
lodashの削除も下記を参考に実施しました。流石に毎回調べて書いています。 moment.jsと含めてこれらを消すとほとんど残すところはReact以外はほとんど気にならなくなってきました。
swap対応
一部のページではfontの読み込みに時間がかかっていたのでfont-display
をswap
にすることで初期表示を早めるようにしました。
このあと、fontファイルをチューニングしたら逆にスコアが下がったりしたので色々ごちゃごちゃ変えたりして最終的にかなり小さいファイルを読み込むようになり、多少マシになりました。
preactに変更
前回の大会で知ったpreactに変えてみるというのも試してみました。結果、めちゃくちゃ一瞬で変更して軽量化に成功しました。
preactとreactで何が違うのかは下記などにまとまっていますが、コア部分の多くはほとんど一緒なこともあり、今回は全くReact側のコードはいじらずに切り替えができました。小さいプロジェクトならpreactでいいんでは?と思ってしまうぐらいサクッとできてコスパ良かったです。
CLS改善
CLSのスコアがこの時点で悪かったので、トップページは即座にメインビジュアルを表示するようにし、その他のページはLoading時のスペースを事前に確保する(かなり雑)ように対応しました。が、スコアには結構ききました。
改めて画像の最適化とlazy loadの修正
画像サイズが大きいものをトリミングしたりしていたので最適な画像を返すようにしたことと、lazy loadを適用しました。
最終結果
最終的には471.65点という結果になりました。
やれなかったことや反省点
1つは単純に試せなかった施策がいくつかありました。
パッと思いつくものだけでもdeploy先をheroku以外に変更してみる、CDNの導入、SSRの実施などです。
特にトップページ以外のページは最初のロード時にAPIのデータを取得を開始し、データが用意できたらレンダリングする挙動だったため、FCPやCLSが悪いままだったので、SSRなどすれば良くなったんじゃなかろうかと予想しております。
自分より上位の人達はこのあたりをうまく対応されていたんじゃないかなぁと思っています。
また、APIのデータ取得などもキャッシュするなりDBをチューニングするなりも効果はあった可能性もありそうでした。
開発環境の整備
また、開発環境の整備をもうちょっとちゃんとやっても良かったかもと思いました。
buildして立ち上げ直すという手順が結構多かったのでホットリロードできるようにするなどしても良かった気がしました。
また、VRTをローカルでも回せるようにすればよかったと思いました。
というのも、毎回VRTの実行はleaderboardからやっていたのでherokuにdeployしてVRTをの結果を見るのにめちゃくちゃ待ち時間がありました。
まとめてコミットしてからVRTが落ちていると原因特定がしづらくなってしまうので、1つ1つこまめに確認したいタイプの自分としては先に整備しておけばよかったと今は思っています
まとめ
まだできることはありそうでしたが、トップ10入りできると思っていなかったのでかなり嬉しかったです。
昨年何もできなかったときと比べると、速度改善にそれなりに自信をもつこともできました。
また、何よりこの1ヶ月夢中にコードと向き合い楽しい時間を過ごせました。
感謝
毎年楽しい課題の制作を提供してくれる運営の皆様に本当に感謝です、ありがとうございました。
参加者の解説記事
参加者の方の解説記事もいくつかありました。結構いろんなアプローチがあって勉強になります。