toshi00.log

いろいろ試したい

Web Speed Hackathon 2021 mini に参加しました

Web Speed Hackathon 2021 mini が2021年12月4日〜2022年1月3日にかけて開催され,これに参加してきました.重たい短文投稿サイトをチューニングし高速化することを目指すコンテストです.

自分が参加できたのは終了直前の数日だけなのですが,多くのことを学ぶことができました.

f:id:toshi00p:20220111171901p:plain

スコアはLighthouse を用いて計測されます(→詳細).満点は720点です.
初期計測で79.02点,最終計測で649.21点です.最終9位でした.振り返りつつ,復習のために行ったことを記録しておきます.

対応するコミットをそれぞれの項目に記載しましたが,一つのコミットに複数の変更が入っている場合があります.また,各項目はジャンルごとに並んでおり,順番と効果は関係しません.

github.com

やったこと

NODE_ENV

初期実装ではトランスパイル後のCSSが6.5MB,JavaScriptが12.5MBもあります.NODE_ENV=developmentが適用されていたので修正しました.modeとsource-map設定についても変更します.このあたりは,2020年に開催されたWeb Speed Hackathon Online Vol.1 の解説を参考にしつつ作業しました.

当該コミット: 938b2d8a25ac1e

webPack

Tailwind CSSのpurge

Purgeオプションを設定し,未使用のスタイルを除去します.
当該コミット: 5d53909

cssnanoの導入

cssの最適化を行います.

当該コミット:8a9c1fc

Babel

今回のターゲットブラウザは Chrome 最新版だけなので,必要な分だけトランスパイルしました.

当該コミット:b9017f45cc5693

assetsの軽量化

WOFF2を使うようにする

WOFF2はWOFFよりも圧縮率が高いとされています.今では多くのブラウザが対応しているようです.Can I use WOFF2

当該コミット:81031e8

Font Awesomeの必要なアイコンだけ読み込み

使わないアイコンを削除して読み込むファイルサイズを縮小しました.アイコンごとに別のファイルにしても良かったかもしれません.

当該コミット:586d17b b698d5b

投稿画像のリサイズとwebPへの変換

画像が表示サイズに比べてかなり大きかったのでリサイズしました.また,webPへの置き換えを行いました.圧縮率の観点から,AVIFへ置き換えたほうが効果が高いかもしれません.

当該コミット:ec719ec 661266d 273c014

アイコン用画像のリサイズとwebPへの変換

投稿画像と同様に,リサイズとwebPへの置き換えを行いました.

当該コミット: 2c0ac1f e6ef437

GIFのリサイズとwebMへの置き換え

GIFをリサイズしつつ,より軽量なwebMに置き換えました. 動画圧縮コーデックはVP9を利用しましたが,圧縮率の観点から考えるとAV1のほうが良いかもしれません.

当該コミット:1af1610ce180014fc2430

依存パッケージの最適化

moment

必要な部分だけ読み込み

webpack-bundle-analyzer を見つつJavaScriptファイルを小さくします.moment(71.9kB)がデカかったのでmoment-locales-webpack-pluginを使って必要な部分だけ読むようにしました.

※ 最終的にmomentを使わずに実装できました(次項参照).

当該コミット:6252ff6

momentの削除

moment(71.9kB)は一部でしか使っておらず,自前で実装できそうです.結局,実装を置き換えて使用を取りやめました.

当該コミット:2b762c0

Preactへの置き換え

webpack-bundle-analyzer 上でReact(2.8kB)とReact DOM(39.4kB)がかなり大きく表示されています.より軽量なPreact(4kB)を使用しました.

当該コミット:2fc58d9

bluebird(gifler,omggifを使わない)

GIFからwebMに変更した結果,gifler,omggifが不要になりました.giflerに必要だったbluebird(21.7kB)も読み込まれなくなりました.

当該コミット:6841907

lodash

lodash(24.5kB)を使わずに実装できたので除去しました.

当該コミット:f47c83d

jQuery

jQuery(30.3kB)のajaxによる実装を,fetch APIに置き換えました.

当該コミット:4b7bb29

pako

クライアントからサーバーへの送信ではgzip圧縮を諦め,pako(13.5kB)を取り除きました.

当該コミット:4b7bb29

AudioContextの読み込みを削除

音の波形をバックエンド側で計算するので,不要になったAudioContextを消しました.

当該コミット:a624800

image-size

画像のトリミングをCSSobject-coverで行うようにしたので,image-size(4.8kB)は使わなくなりました.

normalize.cssを読み込まない

Tailwind CSSにリセットCSSが含まれているので,normalize.cssは(おそらく)不要です.

当該コミット: 6cc52a8

webfont.css

使用していないフォントをwebfont.cssから削除

font-weightを見ると,400と700しか使っていないことがわかるので,それ以外を削除しました.

当該コミット:904fc05

font-displayをblockからswapに

font-displayがblockだとフォントの読み込み完了まで文字が表示されません.swapと設定し,ローカルにある代替フォントを表示しました.

当該コミット:a67b493

scriptの非同期読み込み

deferで非同期読み込みしつつ,loadイベントを削除しました.

当該コミット:c62ae6654f9847

JavaScript

投稿データのキャッシュ

毎回全データをリクエストしていたので,データのキャッシュとlimitの指定をしました.

当該コミット:29f733a

ページ最下部かのチェックを1回だけに

devtoolのパフォーマンスを確認しつつ重たいコードを修正します.念の為 2の18乗回チェックするというすごいコードだったので,1回だけに変更しました.

当該コミット:8eaceef

画像のトリミングをCSSに置き換え

JavaScriptによる大きさ指定ではなく,object-coverを使ってトリミングしました. 当該コミット: 347d94f

コンポーネントの大きさ指定をCSSで行う

初期実装では,AspectRatioBoxの大きさ指定がJavaScriptで行われています.CSSaspectRatioを使って実装するよう修正しました.

当該コミット:54f9847

音の波形を事前計算しておく

波形計算にかなりCPUを使っていました.描画まで時間がかかる点でも厳しいので,バックエンドで事前に計算しておくようにしました.

当該コミット:7e4a7d7 f69ab0c bda79f7

規約ページの後半を遅延ロード

本当はReact.lazyを使うつもりだったのですが,うまくいかなかったのでとりあえずフォントだけ遅延ロードするようにしました.

当該コミット:744dd56

バックエンド

babel-nodeではなくnodeを使う

初期実装では,実行時に動的に変換して動作する babel-node を使っていました.事前にトランスパイルしておき,実行には通常のNodeを使うよう修正しました.

当該コミット:1c65c57

brotli圧縮

後にCDN側で解決できるのですが,brotli圧縮をしてファイルを配信しました.詳しくは理解していないのですが,brotli圧縮のほうがgzipより圧縮率が高いようです.
静的ファイルを毎回圧縮するのは非効率なので,brotli-webpack-pluginを使って事前に圧縮しています.

当該コミット: 5e24f5e

CDN(Cloudflare)の導入

Cloudflareを導入して,http2配信やbrotli圧縮,CDNキャッシュ,クライアントキャッシュ設定などを行いました.Expressによる静的ファイル配信がかなり遅いので,効果は大きかったです.

もっとできそうなこと

効果のほどはわかりませんが,できることはまだまだある気がしています.面白いけど,なかなかに難しいですね……

  • 依存パッケージの最適化
    • react-helmetを使用しない
    • preact-routerに置き換える
    • Preact/compatからPreactへの移行
  • 画像・動画
    • AVIF形式にする
    • 動画圧縮コーデックをVP9からAV1へ置き換える
    • スマホ・縮小画像用により小さな画像を用意する
    • lazyloadする
  • webfont.cssの遅延読み込み
  • React.lazyなどを使ったChunk Splitting
  • クエリ付きリクエストのキャッシュ
  • バックエンドでExpressからFastifyへの移行