Web Speed Hackathon 過去問解いてみる
Web Speed Hackathon とは
公式ページによると
Web Speed Hackathonとは、予め準備してあるWebアプリケーションのパフォーマンスを改善することで競い合うハッカソンです。 主にWeb技術(フロントエンドおよびNode.js)に関するチューニングを出題いたします。 表示に非常に時間がかかるサービスをどこまで高速化できるかを競います。
2020 年から続いている大会で今年で 6 回目の開催に!
最近フロントエンドかじり虫なので過去問ちょっとだけ解いてみよ~っと
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 のサイト にメタファイルを投げ込むことで見れます。
#!/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% の削減に成功しました!
ダイアログの内容とヒーロー画像を配信する
次は 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
これで bundle analyzer のお仕事は見納めです。120MB → 0.39MB と 99.7% も削減してくれました!ありがとう!
リクエストの最適化
devtools の Network タブを確認すると、651 リクエスト 100 MB の通信 187MB のリソース 1.1min の通信時間とめっちゃでかい通信してます。ここも最適化していきましょう!
- 既に取得したデータを再活用 (651 → 154 reqs)
- Cache-Control で静的ファイルをキャッシュ: スコアには影響しないが
- ServiceWorker の並列数制限を解除 a jitter をなくしました
- 圧縮データを ServiceWorker ではなくクライアントで解凍
- いらないデータを送らない
フォント・画像を最適化
画像の形式を AVIF に変更し、画像のロードタイミング (preload や lazy loading) を適切に適用して高速に描画する。
- 画像の不要な preload を止める 他の描画がブロックされるのでやめます。
- loading='lazy' 画面外の画像は遅延ロード
-
不要なフォントを削除
- 一見 NotoSansJP が使われているように見えますがデフォルトの NotoSansJP を使っているので要りません。紛らわしい~
- cyber-toon.svg の不要な部分を削る
-
AVIF 形式への変換
- 画像形式 tier は BMP < PNG < JPEG < WebP < AVIF・JPEG XL で JPEG XL は Chrome で対応していないので AVIF で殴れば良さそう。
- ヒーロー画像を解像度によって srcset sizes picture で出し分け
- fetchpriority="high"
- シンプルな
計測してないのですが AVIF に変換したらとても速くなりました。これで 60 倍の高速化に成功しました!
インタラクティブ性 (INP) の改善
- ReDoS の防止: 正規表現の最適化
- 検索の debounce 適用: 不要な API 呼び出し削減
- イベントリスナーの passive: true 設定
レイアウトシフト (CLS) の改善
stale-while-revalidate の活用
画像の width / height 指定
フォントの display: swap 適用