このエントリは以下のアドベントカレンダーの19日目の記事です。
- はてなエンジニア Advent Calendar 2019 - Qiita
- 前日は id:onk さんの Percona Toolkit を読む - id:onk のはてなブログ でした。
- KMC Advent Calendar 2019 - Adventar
- 前日は machida さんの (null) でした
突然ですが、先日行きつけのビアバーの店主が使っているPCが遅いという話があり、様子を聞くとどうやらHDDで且つメモリも4GBという雰囲気で、触ってみると、たしかにWindowsのホームメニューを出すのに数十秒とかかかっていて、これで暮らしているのは大変だろうと思い、HDDをSSDに、メモリは16GBに増強するということをしました。その結果見事に体験は改善されました。
一方、このときにふと思ったのは、速くなったねとは思うものの、遅かったものが普通くらいのパフォーマンスになっただけで、じゃあ他と比べて速いかというとそうではないよねということです。ウェブアプリケーションでもこういう話はあるなと思っていて、パフォーマンスチューニングと言っても、遅い状態から普通くらいにまず一旦持っていきたいよねと思うことがあります。
何の話かというとウェブアプリケーションを開発していると初期は素朴なので、HTMLもシンプルでJSも少なく、また画像なども必要最低限だけだったりして、阿部寛のウェブサイトのようにシュッとロードしてレンダリングが完了するのですが、ウェブアプリケーションの開発も進んでいくと、徐々にあれもこれもと徐々にHTMLも肥大化し、JSも大きくなり様々なモジュールがくっつき、場合によってはルーター的なのを内包し、ウェブページの中にも画像がドンドン登場したりして、いつの間にか雪だるま式にロード時間やレンダリングまでの時間が延びることになります。
今日は、この雪だるま式に膨らんでしまったロード時間やレンダリングまでの時間を短くしていくまでのことを紹介したいと思います。
つまるところ、First Contentful PaintやFirst Meaningful Paintまでを短くしようという話です。First Contentful PaintはいくつかあるPaint Timingの1つで、ユーザーがウェブページに遷移してきてからブラウザが DOM から受け取ったコンテンツの最初の部分をレンダリングするタイミングのことです。
初歩的なものを中心に紹介しますが、ウェブアプリケーションが大きくなっていく過程でこういったものは見落としがちで意外とシュッと出来る上に効果を感じられたりすると思うので、今一度皆さんのお手元のウェブアプリケーションにそういった箇所がないかチェックする際の指標になればと思っています。
スローガンのご紹介
この活動をやっていくときのスローガンを紹介します。復唱をお願いします。
- 読み込まない
- 実行しない
それぞれもう少し具体的なテクニックも踏まえて見ていきましょう。
読み込まない、そして実行しない
「読み込まない」は(そのときの表示に)必要なもの以外は読み込まないということです。例えば、ウェブページの上部を表示している時にウェブページ最下部に埋め込まれているYouTubeや、列んでいる画像を読み込まないということです。
Chromeのみですが、img要素に loading
という属性があり、loading=lazy
を指定することで遅延ロードをさせることが出来ます。非対応ブラウザでも IntersectionObserver
などを使ってpolyfillできるので、こういったことをすることで簡単に画像の読み込みを抑制することが出来ます。
意外とページ内を見渡してみると表示範囲外に要素が沢山あるもので、それらの読み込みを削ることで本当に表示に必要なものにネットワークリソースやレンダリングコストを集中させることが出来ます。PageSpeed InsightやLighthouseなどで簡単にチェックできるので一度やってみてください。画像やJSやCSSなど削減できるものが見つかると思います。
画像の遅延ロードを設定してまわることをもう少し大枠で捉えるとそもそもHTML自体も最初は削ってしまって、必要なタイミングで組み立てたり、fetchしてきて挿入するということも考えることが出来るかもしれません。
一方で、不要なものは読み込まない一方で必要なものは少しでも高速に読み込みたいものです。例えば必要な画像が決まっていたり、レンダリングに必要な情報をJSON APIで取得することが分かっている場合はpreloadやprefetchを使用することでリソースの先読みをすることが出来ます。
また、ここで読み込まない対象として一番しっかり考えたいものとしてJavaScriptのファイルがあります。とは言っても現代的なウェブアプリケーションにJavaScriptは切っても切れない関係ではあり、Reactなどを使ってSPAにしている場合にはいつの間にか巨大な1つのbundle済みファイルになっていることも少なくないと思います。読み込まないというわけにはいかないという前提で、どう削っていくかということを考える必要があります。
やはりここでもまずは最初のレンダリングに必要なJavaScript以外を読み込まないというのが鉄則になってきます。実行しなければ読み込むのは良いのでは?という方に向けて、ここでJavaScriptのファイルサイズに関する1つのグラフを紹介したいと思います。
(画像は JavaScript Start-up Optimization | Web Fundamentals より)
これによって分かるようにJavaScriptのファイルサイズを削減することは画像のサイズを削減すること以上の意味を持っています。JavaScriptは読み込まれるとそれがパースされ、コンパイルされ、実行されます。この間にかかる時間は画像のデコードよりも遥かに大きくウェブページの表示までのパフォーマンスに大きな影響を与えます。もちろんReactなどを使っていればJavaScriptを実行するまでが短くなればなるほどレンダリングが早くなるのは当然という感じでもあります。
このときWebpackのCode Splittingは1つの強力な選択肢になります。dynamic importを利用することで、JavaScriptを複数のChunkに分割してくれます。このことによりエントリポイントになるJavaScriptファイルは小さくなり、パースや実行の時間を短く出来ます。一方で、Chunkの分割を上手く戦略的にやったり、こちらもpreloadを設定しないと、メインの実行パス内でdynamic importをすることになってしまい結果的にJSの実行中にファイルの取得とパース等が起きてしまい終端のJavaScriptを実行するまでの時間が結果的に長くなり、レンダリングまでの時間が延びるということもあるので気をつけたいですね(この辺の上手いやり方があんまり掴めてないのでオススメ情報あったら教えて下さい)。
また現代的なウェブページの場合、ウェブページ内にSNSのシェアボタンや埋め込みのためのJavaScriptや場合によってはアナリティクスや広告のためのJavaScriptなど数多くのJavaScriptが読み込まれていると思いますが、これらも例えば必要な位置にユーザーがスクロールするまでJavaScript自体を挿入しないということも考えることが出来ると思います。
JavaScriptの実行は1つのフレームに対して1つのスレッドしかないので、これらの読み込みからの実行を抑制することでメインで提供・実行したいJavaScriptの実行時間をセーブすることにも繋がります。
また、読み込んだ際も、JavaScriptのそれぞれの関数などの実行を極力しないということも重要です。JavaScriptを書いているとメインのことをやりつつ、イベントをリッスンする形にしたり、Promiseなどの形にして、並列に複数のタスクをこなそうという風に記述したくなることがあると思いますが、こういったものを我慢して実行を遅らせるということもメインのレンダリングに必要なJavaScriptの実行時間を守るという観点で考えてみても良いかもしれません。色々なハンドラを初期化時に設定していることでいつの間にかメインの表示に必要なJavaScriptの実行を阻害している可能性があります。また、あるJavaScriptの(関数の)実行が長いことがPerformance計測などにより分かっている場合はrequestIdleCallback
のようなものの利用を検討してもいいでしょう。
JavaScriptの実行に関してはこれまた素朴にやっていると、ひとまずエントリポイントをdocument.addEventListener('DOMContentLoaded')
に引っ掛けていたりするかもしれませんが、script要素のasync
と組み合わせて読み込み完了後即実行させていてもこれだと効果薄になってしまうので、DOMがなくても準備できるもの、例えばglobalに何かをexportするとか、Sentryの初期化をするとかはそれまでにさっさと終わらせておくみたいなのも地味に効いたりするので見直してみてください。
また、JavaScriptの実行がウェブページのレンダリングに与える影響に関しては過去にまとめたりしているので、そちらも手前味噌ですが参考になるかと思います。
まとめ
ウェブページの表示の高速化の遅くなくすための道標として、考え方のヒントになればと思って書いてみましたが、いかがでしたか?若干本文中は具体例に欠けるような感じにはなってしまいましたが、こういったテクニックや考え方を持って今年一年ははてなで提供しているGigaViewer(はてなで提供しているマンガビューワです)でのマンガ表示部分が表示されるまでの高速化などに取り組んでいたので、ブログ記事の形でざっと考えていたことを書いてみました。
株式会社はてなでは一緒にウェブページ表示の高速化をやっていく人を募集しています。
また、京大マイコンクラブではこういったトークを僕が極稀に合宿などで行ったりしているので、興味がある人は入部してみてください
身近な人はお声掛けいただいたら一緒にPerformance計測などをしながらこの辺の話を伝授するみたいなことも多分出来ると思うのでお声掛けください。
アドベントカレンダーの明日の担当は id:dekokun さんと:tofu_on_fire:さんです。楽しみですね。