🍣

Web Speed Hackathon 2021 mini 参加記

10 min read

概要

Web Speed Hackathon 2021 miniに参加させていただきました!

最終結果は
約 706点で4位です

githubリポジトリ

今回このようなフロントエンド(がメインな?)チューニングコンテストが初めてなので、
行なった戦略と記録を残して次回のチューニングコンテストに向けた備忘録と対策を考えていきます。

行ったこと

(のちの記事を書くことなど考えず、チューニングしてしまったので)
雑なコミットログと、Github issueに残ってるスコアを参考に記述していきます。

スクロール改善

2021/12/05

まず計測などをせず、アプリを触ってみて明らかに
スクロールがおかしかったので、

Webフロントエンド ハイパフォーマンス チューニングを参考に、

document.addEventListener('event名', { passive: true, capture: true })

等とした上で、

// 念の為 2の18乗 回、最下部かどうかを確認する

と書かれた関数を

const is Bottom = document.body.getBoundingClientRect().bottom <= window.innerHeight
if (isBottom && !prevReachedRef.current) {
...

等とすることで、軽くする試みをしました。
(※この処理では後々バグがあり、上手くいかなかったので、条件式を後々変更しました。)

この時点で、
スコアは91.48でした。

use_inifinite_fetch の修正 (windowメモ化)

2021/12/05

スクロール処理時、毎回バックエンドから900ものデータを
受け取っていたので、初回だけ通信し、window.allPostsに保存し、
以降そこから参照する形に変えました。

この時点で
スコアは108.09でした。

http/2化

2021/12/06 ~ 2021/12/08

http/2における恩恵は、http/1でやる必要があるチューニングの手間を省略してしまいます。(ex: HTTPのリクエスト数を減らしたり、同時接続数の制限を擬似的に増やしたり...)
なので、http/2化の優先度はかなり高いと判断しています。

今まで、herokuでデプロイしてましたが、
herokuはhttp/2を無料では対応しておりませんでした。(有料で行おうとすると、かなり高額)
いろいろ試行錯誤しても、無料で(期限付き無料枠とかではなく)http/2を行う方法が見つかりませんでした。
誰か知っている方がいれば教えてください。(herokuが対応してくれるのが一番楽だ...)

結局gcpを登録し、無料枠で行うことにしました。

この時点で
スコアは59.08でした。
(これなんで下がったんですかね...)

また、gcpがyarn build後でないと、変更が反映されないことにしばらく気付かないので、
これより下のスコアの時系列は信用できなかったりします。

スクロール改善(setTimeout),defer、async

2021/12/08

スクロール改善(setTimeout)

Webフロントエンド ハイパフォーマンス チューニングに倣い、スクロールが毎回発火するのが少し嫌でしたので、setTimeoutで対応することにしました。

defer、async

手当たり次第、
(imgタグなどに)async,deferを付与しました。

この時点でスコアは49.31でした。
(isuconなどでも一時的に下がることはあったので、まあそういうことなのかなとも思いますが、
今考えるとCLSとかsetTimeoutの間隔が短すぎたとかかなと思います。)

運営対応,画像、動画の最適化

2021/12/10
運営からのお知らせがあったので、それに対応しました。

画像、動画の最適化

このあたりから、lighthouseを軸にちゃんと考えて気がします。
(それまではスコアが低すぎて、lighthouseがまともに使えなかった、gcpでデプロイしていたものがlighthouseで落ちるなどがありました。)

画像、動画をサイズ変更などは考えずできる範囲で最適化しました。

この時点でスコアは53.96でした。

use_inifinite_fetchのpaging,画像、動画、音声の最適化(サイズ変更含む)

2021/12/12

use_inifinite_fetchのpaging

api通信を行っているのに、フロントだけにしか責務がないような、修正に反省し、
バックエンドからoffsetを指定して、取得する形に変えました。

画像、動画、音声の最適化(サイズ変更含む)

画像、動画をアプリ上で表示されうる最大サイズでリサイズしました。(それより小さいものは無視)
また、前に行った最適化と同様音声の最適化も行いました。

この時点でスコアは49.16でした。
また、isBottomの条件式の修正も行いました。

画像をCoverdImageで計算するのをやめる。

2021/12/13

複数枚画像があると、縦長になったりするのですが、
その計算が、domを参照して行われてました。
明らかに、ImageArea(1つの投稿における、image全ての情報が存在する)で、
自明なことですので、height,widthをImageAreaで計算し、CoverdImageに渡しました。

この時点でスコアは40.91でした。

音声のworker化,tailwindcssのpurge,webpack

2021/12/15

音声のworker化

SoundWaveSVGの計算が明らかに遅かったので、
worker化しました。

tailwindcssのpurge

  purge: ['./src/**/*.html','./src/**/*.jsx']

これですね。

webpack

このあたり、からwebpackを意識してきました。
BundleAnalyzerを導入し、
momentのlocalesが気になったので、
MomentLocalesPluginを導入しました。

また、core-jsが古いブラウザ対応のために存在しているらしいので、削除し、
standardized-audio-contextも、標準APIに存在しているので削除しました。

この時点でスコアは76.89でした。

apectBoxの修正, blobを使わないようにする, profile画像のリサイズ,webp化

2021/12/16 ~ 2021/12/17

gcpでyarn buildしないといけないことを確認しました

apectBoxの修正

   <div ref={ref} className="relative w-full h-1" style={{ height: clientHeight }}>

ここが、styleがheightに依存しているのが、
どうも気に食わないし、明らかにボトルネックになりえるので、

    <div className="relative w-full h-1" style={{ height: 'auto', paddingTop: (aspectHeight/aspectWidth)*100+'%' }}>

とし、styleでaspect比の維持を試みます。

blobを使わないようにする

blobを使わないでも計算できるように、なっているので

    const img = new Image();
    img.onload = function () {
      setRatioBool((height/width) > (img.height/img.width))
    };
    img.src = src;

等とし、blobによるコストを減らしました。

profileの画像のリサイズ

profileの最大サイズは他のimgと比べて明らかに小さいので、
最適化しました。

webp化

jpegよりもwebpの方が軽量なので、
jpegの画像を全てwebpにしました。

この時点でスコアは139.23でした

gifの最適化,purgeCSSPluginの適用,使っていないfontを省く,soundWaveSVGをworkerではなく、バックエンドの責務に

2021/12/18 ~ 2021/12/20

gifの最適化

imgと同様gifのサイズも、できる限り小さくしたいため、
358×358にしました。

purgeCSSPluginの適用

webpackに適用しました。

使っていないfontを省く

tailwindcssでは、default -> 400,
bold -> 700
と決まっていて、実際アプリ上で使われているのは、
この二つのみですが、500,600,800も読み込まれる可能性があったので、コメントアウトなりして
省きました.

soundWaveSVGをworkerではなく、バックエンドの責務に

元々,soundWaveSVGはdomの操作にも一切関係なく、
また、ユーザーの操作にも関係ないのでバックエンドの責務にすることで、
解決します。
またそれに伴いそこそこ重かった lodash も削除できます

この時点でスコアは、 153.45でした。

画像周辺の最適化, CoveredImageで計算しない。

2021/12/21 ~ 2021/12/23

画像周辺の最適化

lighthouseはデフォルトでは、モバイル画面で計算されます。
もしその場合、pcとモバイルのサイズは全然違いますし、
可能な限りいろいろなサイズを用意した方が良いです。

本来の開発であれば、どこかに
(document.width, document.height) => (return 最適な画像にアクセスできるパラメーター)
や、@2.x,@3.xなどのデバイスピクセル比などできちんと管理した方が良いとは思いますが、
get_path.jsという関数が用意されていましたので、
そこに条件式を追加する形にしました。
if文の一つで悩むほど、高速なチューニングはできておりませんので。

CoveredImageで計算しない。

また、CoverdImage.jsxで、domを参照して、
それに伴いclassを考えているコードになってましたが、
domを参照して、サイズを取得するだけでもボトルネックになることは、Webフロントエンド ハイパフォーマンス チューニングでわかっていますし、

なにより、アプリを触ってみると、APIから受け取るimagesのlengthによってCoverdImage.jsxが計算するwidth,heightがわかるので、このコンポーネントの上位にあたるImageAreaに計算の責務を渡しました。

この時点でスコアは、 195.71 でした。

fast-average-colorをバックエンドの責務に

ユーザーのプロフィールのヘッダーがuserのiconの画像に依存されてきめられていたので、
バックエンドに責務を渡すことで、前処理し、フロントエンドのfast-average-colorを消すこともできました。

この時点でスコアは、212.58でした。

画像の最適化,svgファイルの最適化、woff->woff2,block -> swap,jquery.axaxではなくfetchに変更する

2021/12/24 ~ 2021/12/26

画像の最適化

画像は1枚のみ,2枚以上3枚以下でheightの方が高い画像,3枚以上4枚以下で横長の画像
でサイズが違いますので、それによる最適化を行いました。

svgファイルの最適化

svgがネットワークでみると著しく重たかったので、
中身をみるとアプリに使われていないアイコンなども同梱されて、単一ファイルになってました。
svgはdomで描画することもできますので、必要なもののみをjsonでくり抜いて、
描画するようにしました。

woff->woff2,block -> swap

fontのfont-displayとformatを変更しました。

jquery.axaxではなくfetchに変更する

reactのアプリなのにjqueryのバンドルが入っていたのが、明らかに不自然できたので、
jquery.ajaxしていたものをfetchに変更しました。

この時点でスコアは 420.91でした。

fontを文字単位で最適化,bundle周りの最適化, loading='lazy'

2021/12/27

fontを文字単位で最適化

アプリで使われているfontの割に、
woffのサイズが大きかったので、レギュレーションを確認して、
新しい投稿のフォントに関しては、VRTの確認がなかったので、
バックエンドに登録されているjsonと、アプリ上で使われている文言で使われているものをサブセットフォントメーカーで抜き取りました。

また、これで抜き取るだけではアルファベットがVRTで引っ掛かったので、
FontForgeを扱い、000.woff2がアルファベットも同梱されているのを確認したので、
(FontForgeは確認できるだけなので、woffファイルをttfファイルにまとめるアプリがないかなーと考えています。なければ作らなければ...)
サブセットフォントメーカーで抜き取ったもの(利用規約用とそれ以外で分けました)+000.woff2にすることで読み込みサイズの高速化を試みました。

bundle周りの最適化

BundleAnalyzerで目についたものを最適化することで、
確実に高速化していたことはわかっていたので、
例えば、gzipやBufferなどの削除やmoment.jsで使っている機能を自家製にかえるなどして高速化を行うなどしました。

loading='lazy'

imageにlazyを付与しました。
(手当たり次第つけすぎたので、にちにLCPの画像はeagerにしたりしました。)

この時点で571.31でした。

React.lazy化

2021/12/27

ここらへんで利用規約が重いことに疑問が湧いてきてましたので、
最初は利用規約のみ静的なページで配信できないかを模索していましたが、
コード分割
というのがあったので、React.lazy化(?)を行いました。

この時点で598.73でした。

chunk分け,webp->avif, preact化

2021/12/28 ~ 2021/12/31

chunk分け

Webpackでチャンクを分割する100%正しい方法
http/2ですしこの頃ではnpmの数も減ってきていたので、まあ大丈夫だろうと思い、これを行いました。

webp->avif

WebPよりも軽いAVIFとは?最強の次世代画像フォーマットの解説と作り方

画像が主なボトルネックなのでは?という考えでしたので探している時に見つけました。

preact化

BundleAnalyzerでみると、
react-domが明らかにでかくて気になっていました。
レギュレーションの縛りにReactを扱うことというのがなかったので、
より軽いもの、もしかしたら、素のhtml/css/jsで書いてやろうとも考えてましたが、
preactを見つけましたので、適応しました。

必要なものはimportする形のようなので、flask,djangoの関係のようなものなのかなと考えてました。

この時点でスコアは653.02でした。

movieの責務をworkerに渡す

チューニングしていると、
ユーザー詳細,ホーム,動画の投稿詳細のスコアが悪いことが、気になります。

PausableMovie.jsxにperfomance.markをちりばめて、
計測してみると、
performance.measure('frame', 'frame-start', 'frame-end')
で明らかに100ms ~ 300msかかっておりボトルネックとなっておりましたので、
このframeの部分をworkerに渡してみました。
(workerの受け渡し自体もボトルネックとなりうるので気を付けないといけない...)

この時点でスコアは699.55でした。

div->p, browserslist,optimize-css,videoタグ化

div->p

useMemoのコストを心配する前に余計なdivを減らせ!
これが頭の片隅にあったので、不要なdivを消しつつ、divでなくても良いところはpに変えました。

browserslist

babel周りのチューニングを漁っている時に見つかったので、
package.jsonに記述しました。

optimize-css(&& cssnano-preset-advanced)

僕のアプリ、
cssのコメントが消えてないわ、インライン化されてないわ...
だったので、適用しました。

videoタグ化

700点以上にどうしてもあげたかったので、ボトルネックを調べていたのですが、
どう調べてもgifが存在する時に1~5点他と比べて下がってしまっていました。
これはworkerの受け渡し自体のボトルネックもあるかと思い、どうにか前処理やらなんやらできないか調べていた時に、
「アニGIF使ってる人は、意識低すぎ」グーグルのエンジニアがダメ出し
[ファイルサイズが大きく読み込みが遅いGIFは避けるべき?GIFのパフォーマンス問題を解決する方法]
(https://www.seleqt.net/design/optimisinggifsfortheweb/)
これらを見つけたので、webm化してvideoタグにしました。

これにて最終結果706.53となりました。

反省点, 感想

場当たり的なことをしすぎてしまった...
それともっている知識が古すぎましたね
Webフロントエンド ハイパフォーマンス チューニングこれだけでまともに戦えると思っていました。現実は厳しかったです...
他の人のチューニング記事をみると、SSRなどしても上がりそうですので、次回に向けてここらへんの知識も入れときたいですね。
正直今回のコンテスト、参加者の中で一番時間を溶かした自負があるぐらいかなりハマってしまっていたので次回がどこかであれば参加したいですし、僕個人も開催できたらなーとか考えました。
あと前準備はいくらでもできますので、
font周りのshやらなんやらは作ろうと思います。

Discussion

ログインするとコメントできます