Asa's Website

Web Speed Hackathon 2024 で 3 位になりました

participation logcompetitive programmingweb speed hackathon

作成日 / 最終更新日

Table of Contents

今年も、株式会社サイバーエージェントさん 主催の Web Speed Hackathon 2024 に参加させていただきました!
結果は題名の通りですが、試合中にログを残した参加記です。殴り書きです。

参加までの経緯

去年

正直なところ、これまで私は Web Speed Hackathon について聞いたことがありませんでした。
Twitter で、相互フォローしている方が参加するというツイートを見て、私も参加してみようと思ったのがきっかけです。

あくまでいろいろ経験をして勉強になればと思って参加することにしました。
正直、パフォーマンスガチ勢には勝てないので...。

とか書いていたんですが、なんだかんだ惜しいところまで来たので、今年も参加することにしました。
ふつうにたのしいもん。

当日まで

Maximum からでも何人か参加するっぽいので、みんなで練習。
saitamau-maximum/web-speed-hackathon-2022-scoring-tool を作って、 Maximum 内で 2 月は模擬戦みたいなことをしてました。

それ以外は特に何も...。

今は書いてるのは 3/22 (前日)。
そうだ、やることを挙げておこう。

  • 準備
    • 開いておく: 2022 練習まとめ, 2023 本番
    • ルールをチェックする
      • VRT ありますか
      • ラスト 1 時間はちゃんとレギュレーションに従ってるかチェックしよう
  • 設定
    • Webpack なら webpack-bundle-analyzer, Vite なら rollup-plugin-visualizer を導入する (2022, 2023 参照する)
    • dev → prod にする
    • target を最新版のみにする chrome124
    • Docker ビルド設定速くできない?
  • 見る
    • 実際に開いてみて DevTools のネットワークでどこがボトルネックになってるか見る
    • HTTP/2 になってるか確認
    • 圧縮が有効になってるか確認
  • 開発
    • polyfill 消し + ライブラリ減らす
      • めんどくさいのはとりあえず後回し!
      • 置き換えに失敗してたらやばいよ! (去年があるので...)
      • 無理せず tree shaking に任せるのもあり
      • lodash 置換メモ: https://youmightnotneed.com/
      • momentjs は dayjs に置き換える
    • ファイル分割
    • 非同期にできませんか
    • 画像の縮小 + WebP + loading=lazy (← 今回は架空のマンガサイトらしいので、めちゃくちゃ効きそう)
      • react-lazy-load-image-component も使える
    • canvas なくす
    • キャッシュ
      • ETag も使えるとよさそう
    • フォント woff + サブセット化
    • DB に index 貼る
    • CSS-in-JS やめたいよね
    • Link タグ使ってますか
    • @apollo/client は重いので urql に置き換える
    • ReDoS に注意
  • 大事
    • ちゃんと食べよう!
    • ちゃんと寝よう!

ほとんどが 2023 年の WSH でやったことなんですが、やること多くなってしまった...
全部できるかわからんけど参考にはなりそう。

開幕

去年みたいに Zoom 一番乗り!とかしようとしてたんですが (嘘)、待合室みたいなのがあって、対策 (?) されてました。

過去最高の問題だそうです。たのしみ~

レギュレーション、VRT あるらしいです。しかもページ画像を難読化しろとのこと。これは難しそうだけど、どうせ難読化処理が重いんだろうな~とか言ってみる。

いざ本番です。ぞい!

(以下、対応するコミットのリンクを載せておきます。コミット名は雑です)

Day 1

デプロイ + コード読み

[10:30]
Fork + Clone したらまずはデプロイして、スコアを見てみます。また、この間にざっくりコードを読んでみます。

おすすめデプロイ先が fly.io じゃなくて草
前日に fly.io の準備してたのに...
ということでおすすめの koyeb にデプロイすることにしました。

設定変更

[10:46]
デプロイの間にコードをざっくり読みます。Webpack でも Vite でもなく tsup らしいです。なにそれ。
でも明らかに minify: false とか treeshake: false とか、 target: Chrome の古い版 とかになってるので、直しておきます。
tsup 設定変更 @e3b1d60

見てみる

[10:53]
実際にアプリをローカルで見てみます。
ネットワークタブを見てみると、 HTTP/1.1 で、また NotoSansJP がローカルから読み込まれてるっぽい。
でもざっくり見た感じ使われてないっぽいので消しておきます。
NotoSansJP 削除 @4b58ca5

favicon も base64 で埋め込まれていたので、 ico として読み込むように。
favicon を作成 @49e8795

また、ロゴ画像も 14MB あったので Figma で 10KB に最適化しました。
D&D して export するだけ。べんり~
cyber-toon.svg 軽量化 @9282cdd

polyfill 消し

[11:07]
とにかく重すぎる... polyfill を消しましょう。
ちなみにスコアリングはなんかトラブってるっぽいので保留。

まず lodash を消します。 done [11:11]
lodash 消し @a65cab1

次に moment-timezone を消します。これは dayjs にすればいいですね。 done [11:17]
moment 消し @5fa9161

そうこうしているうちに、スコアリングが復活したみたいです。

その間に、 polyfill を消していきます。
今回も side-effects.ts にまとめられていたけど、なんかコメントアウトされていたので安心して削除。
いつも side-effects だろ!って人には罠になっただろうな~ [11:22]
(もしかして運営がそのままコミットしちゃってた...?)
side-effects 削除 @290b9b3

jQuery。ひっさびさにみた。消します。簡単に消せた。 [11:24]
jquery 削除 @e62cde7

なんかまだ bundle サイズ重くね? client に node の polyfill があったわ。
でも消したらエラーが出たのでだめなんかなぁ

遅延読み込み (できてない)

[11:33]
っていうか metafile オプションってそのまま bundle analyzer に突っ込めるんだ、そのままにしといてよかった。
こっちで解析しよっと。

ん、 constants 重くね? なにこれ? footer で使われてるっぽい。
これ遅延させてよさそうなので、遅延させます。
えー、バンドルサイズ減らない... あきらめて戻します。 tsup わからん。

そうこうしてる間に、スコアリングが終わったみたい。
56.75 点、暫定 8 位。まあ何もしてないからまあよし。

Hero 画像軽量化

ImageSrc もなんかめちゃくちゃ重い。 え、png 画像をそのまま base64 してるやん。
しかも解像度もクソデカ。 解像度を落として、 WebP にしておきました。
hero を webp にした @4cd6a34

[11:51]
そろそろ 2 度目の計測行きますか。 えー、 Service Worker インストールに失敗してる... 謎すぎる...

とりあえずほかのところやってみるか...

JS 軽量化

[12:28]
あ~~ mui icon 全部入ってるやん~~ 見逃してた~~
なおした。多少軽くなった
svgIcon を mui から直接 @9a46b9f

利用規約とか

[12:38]
constant が重そう。 fetch で読み込むようにしてみる? できた。
Client も Server も結構軽くなった。
constants を fetch で動的読み込み @5ef1242

バグ直し 1

[12:52]
動かないんじゃどうしようもないから、 Service Worker を直したい。
いろいろ試してみた結果、 treeshake を true にしてるとよくないっぽい。
treeshake true にするとダメなんてそんなことある??
serviceWorker が動かなかったの直した @1e98b85

Unicode Collation Algorithm

[13:22]
unicode-collation-algorithm2 のデータ遅いのでどうにかしたい。
Intl.Collator が使えるっぽいので、置き換え。
Push して Retry してる間にもぐもぐタイム行きます。
unicode-collation-algorithm2 削除 @f16abda
[13:30]

バグ直し 2

[13:56]
帰ってきた。
317.95 点。暫定 1 位!?

でもちゃんと見てみるとちゃんと動いてなさそう。 header, main が読み込まれない。謎すぎる...
git reset しながらデバッグしてみると、 jQuery 消したことが原因っぽい。
変更点を見てみると、 $(document).ready の置き換えがよくなかったっぽい。
具体的には、すでに読み込まれてたら即実行してほしいのに、 DOMContentLoaded イベントリスナに登録するだけになってた。
You Might Not Need jQuery にはちゃんと書いてあったけど、どうせこうだろ!ってやったらやらかした。思い込みよくない。
何もかも表示されない問題修正 @5da948d

バグ直し 3

[15:42]
もう一回ローカルで見てみると、 Hero が読み込まれない...
どうやら Canvas でやってるっぽいけど、 CSS で置き換えられたので消しておく。
Hero 読み込まれてない修正 @147a627

Windows 特有のアレ

[16:03]
なんかマンガの画像が表示されてない。
あーこれ前にもあった Windows のせいだ。
/\ になってるやつ。
直した。
Windows 特有の /\ になってるやつ @7309ba2

バグ直し 4

[16:13]
なんか Hero の Aspect-ratio 変わってね? img じゃなくて div に設定されていたので直した。
Hero の Aspect ratio おかしい @1d4670c

Axios 消し (できなかった)

[16:18]
そういや axios 使ってるけど、 fetch に置き換えてなかった。
ファイルを 1 つずつ見ながらやってたけど、なんか ky っていうのがあったのでそれでよかったらしい。
が、置き換えてみたらめちゃくちゃエラーが出たのでめんどくさくなってやめた。
タイムロスした...。

E2E で初回テスト & 再計測

[17:10]
今更 E2E テスト環境があることに気づいた。動かしてみる。
とりあえずスコアリングも再計測。まあちゃんと動作するようになった代わりにスコアは激減することはわかってます。
sor4chi に 1 位を譲りました (?)

index.html I/O 消し

[17:36]
なんか毎回 fs で I/O 発生してるっぽい。遅そう。埋め込んじゃう。
index.html の I/O 処理消し @f53696f

a タグ置き換え

[17:42]
すっかり忘れてた。 a タグを Link タグに変えた。
react-router-dom の Link を使う @5ab9005

WebP で読み込ませる

[17:53]
getImageUrl、 WebP で読み込んだらいいんじゃね? ということで変更。
マンガデータはちょっと怖いのでそのまま。
getImageUrl を webp にする @73dbc83

useImage 消し

[18:01]
useImage 変えたいな~
ちょっとまって、 Image component の loading が eager になってた。修正。
loading eager を lazy に直した @f157f01

devicePixelRatio なくてもよくね?だめなのかなぁ
もういいや、けしちゃえ! useImage も使わないようにした。
useImage を使わないようにする @08f68ca

今度こそ Axios 消し

[18:33]
いや~やっぱり axios 消したいよな~ ky だとエラー出るから普通に fetch 使うかぁ
なおした。 axios 嫌い! @9247f84

DB index 貼り (できてない?)

[18:47]
DB 重そうだな~ index とか貼れないかな~ たぶん貼った。 Drizzle わからん。
sql index 貼り @413e1de

トップページの N+1

[19:13]
CLS やばすぎるので直す。 あれ、最初に呼ばれるリクエストに全部情報入ってるから毎回本の情報とか取得する必要なくね? 実装おわり。
最初のリクエストに全部入ってるので再度読み込む必要はない @e6c9e96

[19:35]
book とか author とかも同じっぽいな~
めんどくさいので後回し。

ReDoS 修正 (できてない)

[19:57]
ReDoS 見つけた! (Copilot に任せて) たぶん直せたので、再計測。
結果を見たら風呂入ってご飯食べる。
ReDoS 解消? @31b40fd

計測のメモ (風呂入ったら忘れるので):

  • ホームとエピソードがスコア低め。原因を Page Speed Insights で見てみよう
  • マンガ遅いっぽい... 画像の暗号化・復号を高速にしないとなぁ
  • 利用規約開くの満点で草、いいね
  • admin 系、全部タイムアウトしてる なんで?

[20:32]
ごはんへ

shims 消し & gzip

[21:39]
かえってきた。 Page Speed Insights で見てみる。
うーんやっぱり CLS, LCP かなぁ
ん、圧縮されてない?直しておこう

ちょっとまて、 shims: true になってて草
いらないので消しておく
shims: false @96aa174

Hono の Compress を追加した。
ついでに jitter 処理いらなくね?になったので消した。
gzip @48f159c
jitter いらなくね? @fe75d8c

admin, client で js 分割

[22:09]
tsup 全然わからん!
えー、全然思いつかない...
なんだかんだ 1 時間経過...
全然思いつかなかったので admin, client で js を分けてみる。
admin ファイル分割 @3ad4923

Service Worker を避ける

[23:23]
ふつうに考えて、 Service Worker インストールの時間無駄じゃね?
ふつうのデータはそのまま読み込んでいいよね
ってことでマンガデータ以外は Service Worker 使わないようにした。
(マンガデータは怖かったので手を付けられなかった)
jxl のときだけ service worker 使う @11d050f

wasm を fetch

[23:30]
wasm JS から分割できないかな...
いやふつうに node_modules から assets にコピーして fetch すればいっか
wasm を assets に持ってくる @a9e02f0

assets のキャッシュ

[23:53]
assets ディレクトリ、クライアント側でキャッシュさせてよくね?
cache assets dir @8fa28bc

今日はここまで。

Day2

defer → async

[7:38]
起きてご飯食べてきた。
今日も始めるぞ~

react-router-dom のリファレンス見てみて、 loader とか使えないかな~と考える。
っていうか react-router って Remix 製なんだ、知らなかった
SSR エラー出たのでちょっと撤退。

なんもわからんのでとりあえず defer にしてたのを async に変えた (?)
defer → async @7d6974c

suspense しない

[8:03]
去年そういえば suspense: true になってるの false にしたら多少高速になったっけ...
ついでに CLS とか対策をちょっとだけした。あとデータの使いまわしもなおした。
api からのデータを変えた @92e1778

とりあえず計測。

マンガページ対策

[9:41]
マンガデータをどうにかしたいよなぁ
なんか「世界は我々の想像する以上に変化するため、2 ** 12 回繰り返し観測する」とかあって草
そんないらんやろ
ということで一応 10 回くらいにしてみる。

検索画面もデータ使いまわしされてなかったしバグ生んでたので修正。
bug fix @53732f1

マンガページの若干の CLS 対策として max-height じゃなくて height にした。どうせ高さ変わらないので。
マンガページまわり @77212cf

もう一度計測。

検索画面 バグ修正

[10:03]
ローカルで一応テストを回してみたところ、ようやく全部通った!
(これまではどっかでタイムアウト起きてた)

テストしててわかりづらいかなと思ったので検索画面に loading を追加。
search に loading indicator 追加 @baab35b
bug fix 2 @dd38de2

underscore 消し

[10:18]
admin ログインできないの謎だなぁ
ん、underscore ってなに!? lodash みたいなやつか??消さなきゃじゃん
バンドルサイズが小っちゃくて見逃してた...
underscore 消し @9364907

マンガデータの圧縮

[10:24]
そろそろ画像の暗号化・復号をどうにかしないといけない
WASM 使いたくないもんなぁ...
「内容を明らかに推察できる状態」ってどこまで含まれるんだろ...
適当に共通鍵とか使ってもいいのかな...
とりあえずやるだけやってみるかぁ

API どうすればいいんだろ...
ちょっとまて、 jxl の effort 0 だし圧縮してないじゃん! 直した
jpegXl 圧縮してないじゃん! @4a2e20b

安定した E2E テストに通す

[10:42]
VRT・E2E テストが安定する PR ができたっぽいのでマージする。
なんか初期 VRT の画像がおかしかったせいでエラー出たけど、添付されていた画像の名前を darwin から win32 にして直した。
でもフォントが違うせいで VRT 落ちるんですが... あの...

ん、 admin ページがなんかおかしい?
なんか ReDoS 直したときにおかしくなったっぽい、 Copilot を過信しすぎたのがよくない...
ふつうに @ を含むかは includes すればいいし、記号があるかは /[^a-zA-Z0-9]/.test すればいいのに...
あーだから admin ログインできなかったのか...? (後々違いそうということがわかる)
maybe fix redos @29a7ddc

[11:22]
そうこうしてるうちになんか採点サーバーが更新されてたっぽい。
admin ログインできないのこれのせいじゃね?
すかさず再計測。

にしても admin ページの VRT 通らない謎...
なんか画像のサイズが違うよ!って言われてるけど、これってデバイス依存だったりしないのかな...
max-width に ch を指定してて、これはフォント依存っぽい...
あの... Windows 勢はどうすればいいんですか...? (後々思ったけどちゃんと clar 投げればよかった)

マンガデータの暗号化をしようと思いました

[11:36]
とりあえず暗号化周りをどうにかする。
/api/v1/images に POST してるのってふつうの画像じゃないの?なんで暗号化してるの?
とりあえずロジックがわからんので後回し。

[12:01]
admin ページが遅いので、直したい。
うーんこれはめんどくさそう。
画像の暗号化先にやるか...

[12:40]
暗号化に着手。
とりあえずレギュレーションには「漫画ページ画像を取得したときに」と書いてあるのでアップロードするときにはそのままでも問題なさそう。
暗号化は取得時に適当に鍵作って暗号化して鍵指定してとかやればいいかな...
でやったらファイルサイズクソデカすぎて遅い...困った...
いやでもほかのところ改善すべきだよな...

admin 周り

[13:32]
もうなんだかんだで残り 4 時間... どうすればいいんだ...
でも明らかに admin が重いのでそっちを直さないと。
1 つずつ見ていく。
作者の本取得するときに全件取得しちゃってる、よくないので修正
作者詳細で本全件取得よくないね @b8819cc

なんか作者データ削除重くね? え、各件について消してるのか...
relation 生やして消すようにした。
DB に column がないよ!って怒られたので initialize 時に追加するようにした。
削除の N+1 @dcd39f7

[14:27]
ん、 DB に column がないよって怒られたってことは勝手に drizzle orm 側で追加してくれないのか...
ってことはもしかして index ちゃんと貼れてなかったりするのかな?
initialize 時に sqlite 側から貼るようにしてみる。
Copilot 任せ。おわり。
index 貼り @ea8c60f

[14:38]
マンガって sequential データだから別に一度に全部取得する必要はなさそうだな
でもとりあえず後で

[14:43]
作品編集周りが重いので直したい。
book の再取得とかをいい感じに直した。
作品一覧回り @c98ec8d
再計測。

[14:56]
ごはんまだたべてなかった。食べてこよっと
食べながらやるぞい

[15:16]
なんか落ちてる、確かめてみると更新処理が動いてないっぽい
なんもわからんからとりあえず Revert しとこ...
Revert "作品一覧回り" @a2254f7

wasm 大量発生を修正

[15:26]
せっかく API で offset, limit が設定されてるんだし無限スクロールできるんじゃないかな?
いやテスト落ちそうだな...
とりあえずもう一回 Page Speed Insights で計測してみる。
CLS は改善できてるっぽいのであとは TBT かな

なんか wasm 大量発生してるのいい加減直すか... cache storage 使ってある程度はなおした。
(非同期なので一気にリクエスト来られるとキャッシュヒットしないことがある)
fix wasm 大量発生 @a94fa75

エピソード追加修正

[15:51]
新しいエピソード追加に時間が結構かかるっぽい。
ん、いや、 SQLite 側でエラー出てるわ
あー、 author_id とか追加しちゃったときに null になってるのが原因だった...
多分直せたので再計測。早くなっててくれ~
fix null 制約 @4f1be8b

マンガデータ復元を非同期に

[16:08]
え、もう 1 時間半切ってるの??
うーんあと何しよう...
暗号化・復号が絶対重いから、とりあえず非同期にしてみるか
encrypt, decrypt を promise @f402390

ほげ~とした時間

[16:21]
そういえばマンガデータの画像小さくしてなくね?
復号しながら再計測。
えー、なんかまだエピソード追加できてないんですが...
実際に試してみると、なんか更新したときにボタンがリセットされないっぽいな...
デバッグしてみると、 formik.isSubmittingfalse になってないっぽい...
えでもここ変えた覚えないんですが...

[16:51]
運営側のミスと信じて E2E テストを回してみる。
VRT は明らかに変な diff じゃなければもう Windows のせいにしてそのまま続行。
うーん E2E テストは通ってそう...
これで通ってるんだったら仕様なのか...

[17:10]
E2E テスト回したらもうこんな時間なので、最後に計測して終わりにするか。
ちょうど終わった...

終了

お疲れさまでした~

解説メモ

ビルドツールが tsup 、 target を esm に設定しないとダメ、それはそうだけどやってみたらエラー出たのがなぁ...
えー横線 canvas だったのか気づかなかった...ちゃんとサイト見ないとだめね
スクロールスナップ CSS 置き換えできるの知らなかったな... 実力不足
マンガデータ画質落とすと線が入るらしく、危なかった
Magika 消してよかったのか... 怖くて消せなかった...

順位発表遅くてめっちゃ会話を引き延ばしてる... ライブコーディングで高速化するの!? ヤバすぎ
CSS で clamp とか @container とかあるの知らなかったな...
CSS だけでマンガビュアー作れるのすごいな...

順位発表

[20:22]
順位発表開始!どうなるかな~
最終 3 位!? え、やった~!!
まさか名前が呼ばれるとは思ってなかったのでめちゃくちゃびっくり
Zoom でコメント求められたんですが、びっくりして全然うまく話せなかった...

上位勢ほとんどレギュ落ち...
前回みたいにレギュ落ちで悲しい...みたいなことにならなくてよかった!
(上位勢の人なんかごめんなさい)

そして Maximum がもう大活躍なのよこれが

自分が総合 3 位、 sor4chi がスコア 1 位 (レギュ落ち...)、 Daaaai0809seiei-n も好成績でした~!!
(ツイートだと seiei-n が学生 3 位って書かれてるけど、本人談レギュ落ちかも?ってことなので本当に学生 3 位かは怪しい)

ありがとうございました~来年も参加します!

やることメモの結果

最初のほうにやることメモ書いたんですが、どれくらい役立った/できたか?というのをまとめてみる。

  • 準備
    • ✅ 2022 練習・2023 のリポジトリを開いておく
    • ✅ ルールをチェックする
  • 設定
    • ❌ ビルドアナライザを導入する
    • ✅ dev → prod にする
    • ✅ target を最新版のみにする chrome124
    • ❌ Docker ビルド設定速くできない?
  • 見る
    • ✅ 実際に開いてみて DevTools のネットワークでどこがボトルネックになってるか見る
    • ✅ HTTP/2 になってるか確認
    • ✅ 圧縮が有効になってるか確認
  • 開発
    • ✅ polyfill 消し + ライブラリ減らす
    • ❌ ファイル分割
    • 🤔 非同期にできませんか
    • 🤔 画像の縮小 + WebP + loading=lazy
    • 🤔 canvas なくす
    • 🤔 キャッシュ
    • ❌ フォント woff + サブセット化
    • ✅ DB に index 貼る
    • ❌ CSS-in-JS やめたいよね
    • ✅ Link タグ使ってますか
    • @apollo/client は重いので urql に置き換える
    • ✅ ReDoS に注意
  • 大事
    • ✅ ちゃんと食べよう!
    • ✅ ちゃんと寝よう!

うーん、まあまあ役立ったかな?
ということで次回やることメモ!!

  • ルールは最初のデプロイ中にちゃんと読む
  • chrome 最新版をメモしておく
  • dev → prod
  • gzip や zstd を有効化しよう
  • ReDoS 注意、 Copilot は過信しない
  • ビルドツールがよくわからなかったら Vite に即置換する勢いで
  • base64 grep する
  • パスがあったら replaceAll('\\', '/') する
  • それ、 CSS で置換できませんか?

賞品獲得

入賞ということで、賞品をいただけることになりました!ありがとうございます!
楽しみです!

4/10 届きました!
1 つが CyberAgent のキャラクター、もう 1 つがトロフィー!
トロフィーには中にクラウンが輝いていてすげ~~!になりました
本当にありがとうございます!!!