Web Speed Hackathon 2021やってみる
去年の解説
ローカルでスコアを差分取りながら、自分も実装してみる。WSH_SCORING_TARGET_PATHSを適当に設定しら、スコアは上振れした。差分取りたいだけなのでこのままで
WSH_SCORING_TARGET_PATHS='["/", "/posts/01EXS42Q9GYM0WGGKJBCS3DB05", "/posts/01EXRVDZC14NQ9ERH44CHV4RC3", "/posts/01EXRK14S8HSWSFX8X7AS0DCT4", "/users/mexicandraggle", "/terms"]' yarn run scoring --id nissy-dev --url https://polar-river-66365.herokuapp.com/
yarn run v1.22.10
$ yarn workspace @web-speed-hackathon/scoring start --id nissy-dev --url https://polar-river-66365.herokuapp.com/
$ node dist/index.js --id nissy-dev --url https://polar-river-66365.herokuapp.com/
INFO [1641376792083] (9495 on pc-002024-1): Competitor: nissy-dev
INFO [1641376862411] (9495 on pc-002024-1): Score: 254.06
::set-output name=export::{"competitor":{"id":"nissy-dev","url":"https://polar-river-66365.herokuapp.com/"},"buildInfo":{"commitHash":"21b837172b90cb6b83bf10a57b4a5f81ad0d970b","buildDate":"2021-12-23T15:27:28.256Z"},"result":{"score":254.06,"lighthouseScores":{"/":{"score":11,"firstContentfulPaint":0.88,"speedIndex":0.1,"largestContentfulPaint":0,"timeToInteractive":0.05,"totalBlockingTime":0,"cumulativeLayoutShift":0.04},"/posts/01EXS42Q9GYM0WGGKJBCS3DB05":{"score":44,"firstContentfulPaint":0.88,"speedIndex":0.44,"largestContentfulPaint":0.49,"timeToInteractive":0.43,"totalBlockingTime":0.01,"cumulativeLayoutShift":0.94},"/posts/01EXRVDZC14NQ9ERH44CHV4RC3":{"score":36,"firstContentfulPaint":0.88,"speedIndex":0.39,"largestContentfulPaint":0.35,"timeToInteractive":0.03,"totalBlockingTime":0,"cumulativeLayoutShift":0.92},"/posts/01EXRK14S8HSWSFX8X7AS0DCT4":{"score":26,"firstContentfulPaint":0.88,"speedIndex":0.36,"largestContentfulPaint":0.02,"timeToInteractive":0.38,"totalBlockingTime":0.01,"cumulativeLayoutShift":0.57},"/users/mexicandraggle":{"score":48,"firstContentfulPaint":0.87,"speedIndex":0.41,"largestContentfulPaint":0.82,"timeToInteractive":0.1,"totalBlockingTime":0,"cumulativeLayoutShift":0.88},"/terms":{"score":47,"firstContentfulPaint":0.76,"speedIndex":0.47,"largestContentfulPaint":0,"timeToInteractive":0.11,"totalBlockingTime":0.61,"cumulativeLayoutShift":1}}}}
INFO [1641376866186] (9495 on pc-002024-1): buildInfo: {"commitHash":"21b837172b90cb6b83bf10a57b4a5f81ad0d970b","buildDate":"2021-12-23T15:27:28.256Z"}
✨ Done in 75.71s.
「今回での変更 (650点ライン)」までできていたこと。時間かかり過ぎだけど
- webpackのmodeの設定
- lodashを消す
- なるべく素のJSで置き換えるのが良い
- polyfill周りの変更
- browserslist はpackage.jsonに書くほうが良い
- momentを消す
- dayjsもそれなりに大きいので使わなくていい場合は使わないほうが良い
- jQueryを消す
- CoverImageの書き換え (画像の高さ計算の削除)
- CSSに
object-fit
というプロパティがあるらしい
- CSSに
- tailwind cssのpurge
- CSSのminify
- 使っていないwebfontのcssの削除
- AudioContextのpolyfillの削除
- pakoの削除
- production用のJSX変換
- キャッシュ周り
- APIの方は、
private, no-cache
orno-store
の設定が良さそう
- APIの方は、
- GIFをWebMに変換
- ブラウザでの再生ならWebMのほうがmp4より有利っぽい
- JPEGをWebPに変換&リサイズ
- プログレッシブな画像のほうが実際のUXは良い場合もある
- WebP/AVIFだとだめ、WebPについてはプログレッシブなWebP2が開発されている
- いい記事:https://zenn.dev/gunta/articles/64de0540bafb3d
- プログレッシブな画像のほうが実際のUXは良い場合もある
- アイコンのバンドル
- 画像の遅延ロード
学び
object-fit: cover;
でアスペクト比を維持しつつ全体を覆うように要素に描画される。contain
は、画像が完全に入るように描画される
静的ファイルのCache-Controlには、immutableをつけたほうがパーフォーマンス的には良い
ブログの人は、momentやreact-helmetも素のJSに置き換えていたけど、現実的にはできないことが多そう。日付のフォーマットや計算は、dayjsでやったほうが安心感がある。react-helmetは、今回はtitleタグだけなので消せただけ。
「今回での変更 (650点ライン)」までで、できてなかったこと
- スクロールが重いのを修正
- ログイン状態のチェック前から画面を表示する
- inertのpolyfillの削除
- データ取得でのlimit/offsetの利用
- CSSのaspect-ratioを利用する
- CloudFlareの利用 (✋)
- 横幅を半分にリサイズした画像も用意するように
- normalize.cssの削除
- 波形画像のサーバーでの生成 (✋)
- 一方のデータが取得できたタイミングでできるだけ表示する
- スクロールバーによるレイアウトシフトへの対応
- font-display: swapの利用
- 規約ページの内容の部分表示化
- code splitting (✋)
- モバイルでは半分のサイズの画像を利用 (✋)
ちなみに、(✋)となっているやつは自分でも思いついていたけど、実装が大変そうだったからやっていないやつ。VRTが通らない可能性が高そうだったから手をつけていなかった。
スクロールが重いのを修正
まずこれができていなかった.... なんか重いと思っていたけど、原因特定と優先順位も挙げられなかった...
原因は、(2**18回確認する)怪しいスクリプトとイベントリスナーのpassive: falseの指定。スクロール周りの実装はMutationObserverを使う機会が多いからか、まったく知らなかった。
これを直すと 240 → 290 くらいスコア上がった
ログイン状態のチェック前から画面を表示する
そもそもCLS結構いいのであんまり影響なさそう
inertのpolyfillの削除
これもちゃんと調べれば消せたけど、そこまでサイズも大きくないので問題なさそう
データ取得でのlimit/offsetの利用
これはちゃんとやらなきゃいけなかった...
CSSのaspect-ratioを利用する
aspect-ratioと呼ばれるプロパティがある。知らなかった...
横幅を半分にリサイズした画像も用意するように (✋)
これは意外とサクッとできるのでやればよかった
normalize.cssの削除
Tailwindにはデフォルトで https://github.com/sindresorhus/modern-normalize が入っているらしい
ここまでで 310くらいのスコア
スクロールのやつとデータ取得でのlimit/offsetの利用をやれば、300点以上にはなっていた気がする
バイナリの扱いで結構わからなくなったのでメモ
ArrayBuffer と TypedArray
ES2015で標準化されたクラス。
ArrayBuffer自体は、バイナリデータを扱うための基底クラスで、バイナリデータを格納する箱 (領域) を作るイメージ。TypedArray (Unit8Array, Uint16Arrayなど) は、ArrayBufferを操作するためのクラス。TypedArrayからしかバイナリデータを追加、削除することはできない。
const buf = new ArrayBuffer(8);
console.log(buf); // ArrayBuffer { byteLength: 8 }
buf[0] = 0 // エラーは出ないけど、値として反映されない
const ua = new Uint8Array(buf);
console.log(ua); // Uint8Array [ 0, 0, 0, 0, 0, 0, 0, 0 ]
ua[0] = 1;
console.log(ua); // Uint8Array [ 1, 0, 0, 0, 0, 0, 0, 0 ]
Bufferは...?
Node.jsが独自に持っていた、バイナリデータを扱うためのクラス。 Bufferは、現在 Unit8Arrayを継承したクラスとなっており、TypedArrayに実装されているすべてのメソッドが利用可能である。slice()の挙動が違ったり、多少の互換性が無いこともある。
Buffer#sliceの実装は、既存のBufferのコピーなしで作成するのに対し、TypedArray#sliceの実装はコピーを作成するため、動作に違いが出ます。
波形データをサーバーで作るやつは、Node.js でaudioデータをデコードするライブラリが軒並みうまく使えなかった... おそらく音声をoggに変換したからな気がする....今回は波形データを作成するから、音声データはいじっちゃだめか...
以下を使ってデコードしたけど、波形データが変化してしまった... VRTでは落ちるけど、今はパフォーマンスを改善することに注力する
ちなみに、これでスコアは 310 → 450 くらいまでアップ。これは、リソースの取得も減るし、JSの実行時間も減るので結構効くな〜 実装的には一番面倒だけど、ちゃんとやったほうが良いやつ
一方のデータが取得できたタイミングでできるだけ表示する
スクロールバーによるレイアウトシフトへの対応
規約ページの内容の部分表示化
ここらへんはCLSのための作業が多いが、CLSはかなりいいのでスキップ。
font-display: swapの利用
font-display: block だと、フォントがダウンロードされるまでは何も画面に表示されないが、font-display: swapだとフォントがダウンロードされるまでの間は代替フォントで表示される
スコアが 450 から 600 まで一気に伸びた
code splitting
JSサイズを減らすことができる
モバイルでは半分のサイズの画像を利用
これもやればスコアは上がりそうだけど、半分のサイズの画像を用意したのとほぼ同じことをやるだけなので、割愛。
lazy loading については、一番の最初の投稿についてはやらないほうが良い。LCPのスコアが悪くなる。また、画面外で要素の描画をスキップできる content-visibility も存在する
「今回での変更」からは、ちょっと細かいところのチューニングになるので気になったところだけピックアップ。
fetchのpreload、サーバープッシュ
規約ページでfontのCSSをpreloadするように
preloadしていたfetchをサーバープッシュするように
TODO
preactへの移行
alias使うとかなり簡単に書き換えられるんだなー react-routerとも一緒に使えるのには驚き
ブログの人がやっていなくて個人的にやりたかったのは、ServiceWorkerのキャッシュやuseSWRのキャッシュかな〜
useSWRのところに関しては、APIのレスポンスをCDNでにキャッシュさせているのでやっているので、近いことはやっている
結構散らかって来たので、ブログに学びをまとめて終了にする
Cloudflareにも載せよう
まずは、個人ブログをCloudFlareに乗せてみた
nissy.dev のドメインのネームサーバーを、ドメインを買ったGoogle DomainからCloudflareに移行する。移行は指示通りやればできる。
ただ、移行したあとにリダイレクトループが起きたので、以下の方法で修正する必要があった。リダイレクトループが発生する流れは以下の通り。
blog.nissy.dev →(https) Cloudflareのエッジサーバー →(http) オリジンサーバー (Vercel) → (https) blog.nissy.dev ......
Flexible SSLに設定していると、CloudFlareがオリジンに問い合わせる時はHTTPで行うらしい。(こうしている理由って何かあるだろうか...?)オリジンサーバーの方で、HTTPをHTTPSへリダイレクトする設定が入っていると、リダイレクトループが発生する。
設定した記憶はないけど、確かに http://blog-nd-02110114.vercel.app/ にアクセスすると、自動的にhttpsにリダイレクトされる。ドキュメントにも308でリダイレクトすると書いてあった。
Furthermore, any HTTP requests to your Deployments are automatically forwarded to HTTPS using the 308 status code:
It is not possible to disable this redirection or prevent the Deployment from being served over HTTPS as it is considered an industry standard to serve web content over a sec
ちなみに今回の対応で、ドメイン全体でFull SSLとなるが、サブドメインごとに設定を変えることもできるらしい。
CloudFlareに乗せると HTTP3 でサーブされるようになった。
ブラウザとWebサーバーが利用可能な最高位のプロトコルを自動的にネゴシエートします。そのため、HTTP/3はHTTP/2よりも優先されます。CloudflareがHTTP/1.xを使うのは、オリジンWebサーバーとCloudflareの間だけです。
VercelとCloudflareのCDNを使うことはかなりおすすめされていないのでやめた... おすすめされていない理由としては、Vercel のDeploy のタイミングでCloudfalre CDNのキャッシュをパージできないから。そもそも、キャッシュ率を見たら全然キャッシュされてなかった... CloudflareはデフォルトでJSONがキャッシュされない & 読み込んでいるファイル数的にはJSON (ブログのコンテツが入っている) が結構多いからだと予想
herokuだと色々やりづらいし、あまり使うこともないので、GAEへデプロイしてみる
基本 stanrad 環境でデプロイするのがいいらしいが、automatic_scalingのmax_instancesやmax_idle_instancesの設定には注意する。
flexible 環境でやらないと、yarnコマンド実行できないらしい... 1日くらいはまった...
おかげでデプロイの仕方とログの見方はなんとなくわかってきた
デプロイの流れ
project作成 → app 作成 → deploy
デプロイには、Cloud BuildのAPIを有効にする
ログの見方
ビルドのログ : Cloud Buildの画面からログを確認
インスタンス起動のログ:AppEngine > Services からログを確認
CloudFlareでやろうとしたけど、すでに持っているドメインだとCloudFlareのCDNプロキシができなさそうなのがわかった...
カスタムドメインを買って、Cloudflareに乗せてみた
スコアは620くらいから650まで変化したから、キャッシュの効果はありそう