Open2

Web Speed Hackathon 過去問解いてみる

あんこあんこ

Web Speed Hackathon とは

公式ページによると

Web Speed Hackathonとは、予め準備してあるWebアプリケーションのパフォーマンスを改善することで競い合うハッカソンです。 主にWeb技術(フロントエンドおよびNode.js)に関するチューニングを出題いたします。 表示に非常に時間がかかるサービスをどこまで高速化できるかを競います。

2020 年から続いている大会で今年で 6 回目の開催に!
https://cyberagent.connpass.com/event/338797/

最近フロントエンドかじり虫なので過去問ちょっとだけ解いてみよ~っと

あんこあんこ

Web Speed Hackathon 2024

レギュとしては任意の記事を参照してもよいので満点を目指すって感じです!

公式ルール

  • 0.1vCPU / 512MB のサーバーを 1 つ使用
  • Chrome最新版での E2E テストと VRT をクリアすること
  • 漫画ビューアーの画像に難読化を施すこと

なにをどうやって計測する?

Web Speed Hackathon のバックエンド版 ISUCON で「推測するな、計測せよ」の精神をこちらにも適用しちゃいましょう!

今回チューニングで改善すべき数値とは Lighthouse v10 のスコアです。

指標 説明 推奨される数値
First Contentful Paint (FCP) 最初の DOM コンテンツを描画するまでの時間 1.8s 以内
Largest Contentful Paint (LCP) 最も大きな要素を描画するまでの時間 2.5s 以内
Speed Index (SI) 時間ごとの視覚的な変化量 3.4s 以内
Total Blocking Time (TBT) ユーザー操作がブロックされてる合計の時間 200ms 未満
Cumulative Layout Shift (CLS) 視覚的な安定性 0.1 未満
Interaction to Next Paint (INP) インタラクティブ性 200ms 未満

これらを改善していけばいいのですが Web Speed Hackathon、いつも凶悪なサイトを作り込んでいるため Lighthouse でも重すぎて正しく評価できません。あと単純に測るまで時間がかかります。それなので次のメトリクスを測ってそれを改善させていきます。

  • バンドルサイズ
  • bundle analyzer
  • devtools (Performance タブ・Network タブ・Lighthouse タブ)

フロントエンドにおいてバンドルサイズはネックになりやすいらしいのでそこをきちんと測って改善すると良いそう。そこでバンドルサイズを測るスクリプトをささっと書きました。bundle analyzer については ESBuild の bundle analyzer のサイト にメタファイルを投げ込むことで見れます。

profile.sh
#!/bin/bash

function convert_MB() {
  FILE=$(echo $1 | sed -e 's/.*\///')
  BYTE=$(wc -c $1 | awk '{print $1}')
  echo "scale=2; $BYTE / 1024 / 1024" | bc | xargs printf "$FILE: %.2fMB\n"
}

pnpm run build
convert_MB ./workspaces/client/dist/client.global.js
convert_MB ./workspaces/client/dist/serviceworker.global.js
convert_MB ./workspaces/server/dist/server.js
pnpm run start

バンドルサイズ

  • client: 119.89MB
  • serviceworker: 6.14MB
  • server: 36.89MB

bundle analyzer

スコア: 27.75 / 700.00

バンドルサイズを削る

バンドルとは JavaScript のコードを 1 ファイルにまとめることです。バンドルサイズを削減すると、通信量が減り、モバイル環境や低スペックサーバーでも快適に動作します。

削減方法

  • ソースマップはでかいので削ろう
  • ESM 版ライブラリを使おう
  • 不要なファイルをバンドルから追い出そう
  • minify や code splitting も適用してこう

ビルドコンフィグの最適化

今回は tsup でビルドしており、
モジュール形式は CommonJS (CJS) より treeshaking が効く ES Module (ESM) を選ぼう。
クライアントを ESM 形式に置き換えるとプロファイルと minify ができなくなってしまう為いったん iife のまま動かします。また tsup では ESM 形式以外については code splitting が未実装とのこと。

やること 効果 メモ
ソースマップを削除 119.89MB → 46.16MB ビルドファイルと元のソースコードの対応を示してデバッグ時のスタックトレースに役立ちますがとてもでかい
minify 46.16MB → 36.62MB
production モードでビルド 36.62MB → 36.38MB
ビルドターゲットを最新版の Chrome に絞る 36.38MB → 36.29MB
treeshaking 36.29MB → 36.30MB 設定するだけ大きくなっていそうですがあとで少し小さくなるので設定しておきます。

計 70% の削減に成功しました!

https://github.com/anko9801/web-speed-hackathon-2024/commit/24e38a433a02c9bc7a43739f9e80af46c4950694

ダイアログの内容とヒーロー画像を配信する

次は Bundle Analyzer でサイズが大きく不要なパッケージをバンドルから追い出します。

やること 効果
Dialog の内容をサーバー配信 -12.96MB
heroImage を画像化 -12.27MB

これで 36.30MB → 11.07MB と 25MB 削減できました!

不要なパッケージの削除

やること 効果 メモ
mui の treeshaking -4.03MB
magika -2.23MB ファイルからファイル拡張子を DeepLearning で特定するライブラリ、既に確定しているのでそれを活用します
unicode-collation-algorithm2 -1.77MB 文字の比較に使用していたのを Intl.Collator で代替
moment-timezone -0.81MB YOU MIGHT NOT NEED *
polyfill -0.65MB 最新版の Chrome のみに対応すればいいので polyfill は必要ありません。
three.js -0.47MB
jQuery -0.09MB 単に置換するとロードされなくて戸惑うけど DOMContentLoaded のイベントリスナーをセットするのがイベント発火に間に合ってないことが原因なので後ろに await を持ってくることで解決しました。YOU MIGHT NOT NEED *
lodash -0.07MB YOU MIGHT NOT NEED *
underscore -0.02MB
zustand -1.5kB
usehooks -0.1kB

これで 11.07MB → 0.92MB (1MB 未満) の削減に成功しました!今回ここまでやりましたがある程度小さくなると他の箇所にボトルネックに移るので臨機応変に対応していった方が良さそうです。

バンドルを admin と client に分ける

どのページでも client と admin どちらも配信していたので別々にバンドルして出し分けて上げます。

  • client 0.39MB
  • admin 0.70MB

https://github.com/anko9801/web-speed-hackathon-2024/commit/490b51f74c5dc70f95de591be49564740b5e55bd

これで bundle analyzer のお仕事は見納めです。120MB → 0.39MB と 99.7% も削減してくれました!ありがとう!

リクエストの最適化

devtools の Network タブを確認すると、651 リクエスト 100 MB の通信 187MB のリソース 1.1min の通信時間とめっちゃでかい通信してます。ここも最適化していきましょう!

フォント・画像を最適化

画像の形式を AVIF に変更し、画像のロードタイミング (preload や lazy loading) を適切に適用して高速に描画する。

計測してないのですが AVIF に変換したらとても速くなりました。これで 60 倍の高速化に成功しました!

インタラクティブ性 (INP) の改善

レイアウトシフト (CLS) の改善

stale-while-revalidate の活用
画像の width / height 指定
フォントの display: swap 適用

https://blog.jxck.io/entries/2016-04-16/stale-while-revalidate.html