POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Vitaly Friedman

本記事は、原著者の許諾のもとに翻訳・掲載しております。

目次

前編

  • 準備段階:計画と指標
    パフォーマンスを重視する文化、Core Web Vitals、パフォーマンスのプロファイル、CrUX、Lighthouse、FID、TTI、CLS、端末。
  • 現実的な目標の設定
    パフォーマンスバジェット、パフォーマンス目標、RAILフレームワーク、170KB/30KBバジェット。
  • 環境の定義
    フレームワークの選択、パフォーマンスコストの基準設定、Webpack、依存関係、CDN、フロントエンドアーキテクチャ、CSR、SSR、CSR + SSR、静的レンダリング、プリレンダリング、PRPLパターン。

中編

  • アセットの最適化
    Brotli、AVIF、WebP、レスポンシブ画像、AV1、アダプティブメディア読み込み、動画圧縮、Webフォント、Googleフォント。
  • ビルドの最適化
    JavaScriptモジュール、モジュール/ノーモジュールのパターン、ツリーシェイキング、コード分割、スコープホイスティング、Webpack、デファレンシャルサービング、Webワーカー、WebAssembly、JavaScriptバンドル、React、SPA、パーシャルハイドレーション、インポート・オン・インタラクション、サードパーティ、キャッシュ

後編

チェックリストのPDFファイル(166KB)、編集可能なApple Pagesファイル(275KB)、.docxファイル(151KB)も自由にダウンロードできます。皆さんが最適化を楽しめますように!)

アセットの最適化 #

20. プレーンテキストの圧縮にBrotliを使用する。

2015年、Googleはオープンソースのロスレスな新しいデータ形式であるBrotli導入しました。 現在、Brotliは全てのモダンなブラウザでサポートされています。 オープンソースのBrotliライブラリは、Brotli用のエンコーダとデコーダを実装するものです。 エンコーダには11の品質レベルがあらかじめ定められており、品質レベルが高いほど、CPUの要求水準が高い代わりに圧縮率も良くなります。 圧縮が遅いほど最終的な圧縮率は高くなりますが、それでもBrotliの解凍は高速です。 Brotliの圧縮レベル4は、gzipよりも容量が小さく、圧縮が速いことは注目に値します。

実際、Brotliはgzipよりもはるかに有効なようです。 Brotliに関する意見や体験はさまざまですが、サイトをgzipで最適化する場合に比べて、 ファイルの容量とFCPのタイミングを最低でも1桁台、 最高なら2桁台の割合で改善できるでしょう。 Brotliによってサイトの圧縮率がどれだけ改善するかを推定することも可能です。

ブラウザがBrotliを受け入れるのは、ユーザがWebサイトにHTTPSでアクセスする場合に限られます。 Brotliは広くサポートされており、多くのCDN( AkamaiNetlify EdgeAWSKeyCDNFastly(現在はパススルーのみ)、 CloudflareCDN77)のサポート対象となっています。 さらに、BrotliをまだサポートしていないCDNでさえ使用することが可能です(サービスワーカーを利用する場合)。

問題は、全てのアセットをBrotliの高いレベルで圧縮するのはコストが大きく、膨大なオーバーヘッドコストを生み出すため、多くのホスティングプロバイダがBrotliを全面的に使用できないことです。 実際、最高の圧縮レベルでは、Brotliはとても遅くなります。 その結果、ファイル容量を削減できるとしても、サーバが応答を送信し始めるまでにかかる時間によって相殺される可能性があります。 これはサーバが、アセットが動的に圧縮されるのを待つ必要があるためです。 (ただし、ビルドタイムにおいて静的圧縮を行う時間があるなら、当然ながら、圧縮率を高く設定するほうが望ましいでしょう)。

さまざまな圧縮手法のバックエンドタイムの比較。驚くことではありませんが、Brotliは(今のところ)gzipよりも低速です。(プレビューを拡大

しかし、このような状況は変わりつつあるのかもしれません。 Brotliファイル形式にはビルトインの静的ディクショナリが組み込まれています。 複数の言語のさまざまな文字列に加え、これらの文字列に複数の変換を適用するという選択肢もサポートしており、汎用性が高くなっています。 Felix Hanauは、調査によって、 レベル5~9でコンテンツを圧縮した場合のパフォーマンスを改善する方法を発見しました。 その方法とは、「デフォルトと比べて絞り込まれたディクショナリのサブセット」を使用し、 Content-Typeヘッダを通じて、コンプレッサがHTML、JavaScript、CSSのどのディクショナリを使うべきかを指定することです。 その結果、「Webコンテンツを高いレベルで圧縮する場合でも、限られたディクショナリを使用するアプローチによって、パフォーマンスへの影響をごくわずか(CPUへの影響は通常の12%に対して1~3%)とすることができました」。

改善後のディクショナリアプローチによって、CPU使用時間の増加をわずか1~3%に抑えつつ、アセットを高い圧縮率で高速に圧縮できます。通常、圧縮レベルが5から6になると、CPU使用時間は最大で12%増加します。(プレビューを拡大

さらに、Elena Kirilenkoの調査によれば、 過去の圧縮アーティファクトを使用することで、高速で効率的なBrotli再圧縮を実現できます。 Elenaによると、「Brotliでアセットを圧縮した後、動的なコンテンツをオンザフライで圧縮しようとする場合において、 コンテンツが事前に利用可能なコンテンツと類似しているときは、圧縮時間を大幅に短縮できます」。

このようなケースはどれほどあるのでしょうか。 例えば、**を配信するケース(コードの一部がすでにクライアント側でキャッシュされている場合や、 WebBundlesで動的なバンドルを提供する場合など)が考えられます。 あるいは、事前に分かっているテンプレートに基づく動的なHTMLや、WOFF2フォントの動的なサブセットを使用する場合もあります。 Elenaによれば、コンテンツを10%削減すると、圧縮率が5.3%、 圧縮速度が39%改善します。コンテンツを50%削減した場合、さらに圧縮率が3.2%、圧縮速度が26%改善します。

Brotli圧縮の品質は向上しています。そのため、静的アセットを動的に圧縮するコストを回避できるなら、間違いなく労力を費やすに値するでしょう。 もちろん、Brotliは、HTML、CSS、SVG、JavaScript、JSONなど、あらゆるプレーンテキストのペイロードに使用できます。

注記:2021年初めの時点で、HTTP応答の約60%はテキストベースの圧縮をすることなく配信されており、 30.82%はgzip、9.1%はBrotliで圧縮されています(いずれもモバイルとデスクトップ)。 例えば、Angularページの23.4%は(gzipやBrotliによって)圧縮されていません。 それでも、圧縮は、スイッチをオンにするように、とても簡単にパフォーマンスを改善する手段となることがよくあります。

圧縮の戦略はどうすべきでしょうかBrotliとgzipの最高レベルで静的アセットを事前に圧縮し、 Brotliのレベル4~6で(動的)HTMLをオンザフライで圧縮しましょう。 サーバがBrotliやgzipのためのコンテンツネゴシエーションを適切に処理できるようにしてください。

2020年に圧縮された状態で提供されたリソースのうち、22.59%はBrotliで圧縮されています。約77.39%はgzipで圧縮されています。(画像の出典:Web Almanac: Compression)(プレビューを拡大

21. アダプティブなメディア読み込みとクライアントヒントを使用するか?

昔から言われていることですが、 srcsetsizes<picture>要素によってレスポンシブな画像を使用することは、 いつも忘れないようにしてください。 特にメディアのフットプリントが大きいサイトの場合、 アダプティブなメディア読み込み(この例ではReactとNext.js)によって一歩踏み込んだ対策を取ることができます。 これは、ネットワークが遅くメモリが小さい端末にはコストが軽いユーザ体験を、ネットワークが高速でメモリが大きい端末には完全なユーザ体験を提供するものです。 Reactの場合、サーバ側のクライアントヒントや、クライアント側のreact-adaptive-hooksによって実現できます。

レスポンシブ画像の未来は、クライアントヒントの導入が拡大することで劇的に変化するかもしれません。 クライアントヒントはHTTPリクエストのヘッダフィールドで、例えばDPRViewport-WidthWidthSave-DataAccept(画像の形式の設定を指定する場合)などがあります。 これらは、ユーザのブラウザ、画面、接続などの詳細をサーバに伝達することとなっています。

これらの情報を踏まえ、サーバは適切なサイズの画像によってレイアウトを埋める方法を決定し、画像を望ましい形式でのみ提供できます。 クライアントヒントによって、一連のリソースは、HTMLマークアップから、クライアントとサーバ間のリクエストと応答のネゴシエーションへ移行されます。

実際のアダプティブなメディア提供。オフラインのユーザにはテキスト付きのプレースホルダ、2Gのユーザには低解像度の画像、3Gのユーザには高解像度の画像、4GのユーザにはHD動画を送信します。「Loading Web Pages Fast On A $20 Feature Phone」より。(プレビューを拡大

Ilya Grigorikが少し前の記事で述べているとおり、 クライアントヒントは穴を埋める役割を果たすものであって、レスポンシブな画像に代わるものではありません。 「<picture>要素は、HTMLマークアップにおいて必要な『演出』のコントロールを実現します。クライアントヒントは、出力される画像リクエストに注釈を付与し、リソース選択の自動化を可能にします。サービスワーカーは、クライアント側の完全なリクエスト・応答管理機能を提供します」。

サービスワーカーは、例えば、新たなクライアントヒントのヘッダ値をリクエストに追加する、 URLを上書きしてCDNに画像リクエストを送信する、接続性とユーザ設定に基づいて応答を調整するなどが可能です。 これは画像アセットだけではなく、それ以外のほとんどのリクエストにも当てはまります。

クライアントヒントをサポートするクライアントでは、画像のバイト数を42%削減し、 上位3割弱のケースでは1MB超のバイト数を削減できました。Smashing Magazineでも19~32%の改善を記録しました。 クライアントヒントはChromiumベースのブラウザでサポートされていますが、 Firefoxでのサポートも検討中です。

ただし、通常のレスポンシブ画像マークアップと、クライアントヒント向けの<meta>タグの両方を提供した場合、 これらをサポートするブラウザは、レスポンシブ画像マークアップを評価し、クライアントヒントのHTTPヘッダを使用して適切な画像ソースをリクエストします。

22. 背景の画像にレスポンシブ画像を使用するか?

間違いなく使用すべきです。image-set(現在はSafari 14、 およびFirefox以外のほとんどのモダンなブラウザでサポートされています)を使用すれば、レスポンシブな背景画像を提供できます。

background-image: url("fallback.jpg");
background-image:
  image-set( "photo-small.jpg" 1x,
   "photo-large.jpg" 2x,
   "photo-print.jpg" 600dpi);

基本的に、背景画像は条件付きで提供することができ、1x記述子では低解像度、2x記述子では高解像度、600dpi記述子では印刷物と同等の品質さえ実現可能です。 ただし、ブラウザは背景画像に関する特別な情報をアシスティブテクノロジー(支援技術)ツールに提供しないので、こうした写真は単なる装飾とするのが理想です。

23. WebPを使用するか?

画像の圧縮は、通常は手軽に成果を上げる手段と考えられていますが、それでも実際はまだまだ利用されていません。 もちろん、画像はレンダリングをブロックしませんが、LCPのスコアが低くなる大きな要因の1つです。 画像を表示する端末にとって、画像のコストや容量が大き過ぎるのは非常によくあることです。

ですから、画像にWebP形式を使用することは、少なくとも検討に値するでしょう。 実際、2020年にAppleがSafari 14にWebPのサポートを追加したことで、 WebPの物語はゴールを迎えつつあります。 長年にわたる議論を経て、現在、WebPは全てのモダンなブラウザでサポートされています。 WebP画像は、<picture>要素に加えて必要に応じてJPEGフォールバックを使用するか(Andreas Bovensのコードスニペットを参照)、 あるいはコンテンツネゴシエーションを使用することで(Acceptヘッダを使用)提供できます。

しかし、WebPに欠点がないわけではありません。 WebP画像のファイル容量はGuetzliやZopfliと同等ですが、 WebPはJPEGのようなプログレッシブレンダリング機能をサポートしていません。 そのため、WebP画像のほうがネットワーク転送速度は速いとしても、ユーザにとっては、画像が完成するのは昔ながらのJPEGのほうが速く感じられるかもしれません。 JPEGでは、データが半分や4分の1だけでも「まとも」なユーザ体験を提供し、残りは後で読み込むことができます。 一方、WebPの場合は、半分が空白の画像を提供することとなります。

どちらを選ぶべきか、それはあなたが何を求めているかによって変わります。 WebPではペイロードを削減し、JPEGでは体感的なパフォーマンスを改善することが可能です。 WebPについてもっと詳しく知りたい方は、GoogleのPascal MassiminoによるWebP Rewindという講演をご覧ください。

WebPへの変換ツールとしてはWebP Convertercwebplibwebpがあります。 Ire Aderinokunは画像をWebPに変換するためのとても詳細なチュートリアルを公表しています。 Josh ComeauのEmbracing modern image formatsという記事も同様の手引きとなっています。

Pascal MassiminoのWebP Rewindという講演では、WebPについて徹底的に語っています。(プレビューを拡大

SketchはネイティブでWebPをサポートしており、WebP画像はWebPのPhotoshop用プラグインを使用することでPhotoshopからエクスポートできます。 しかし、他の選択肢を利用することも可能です

WordPressかJoomlaを使用している場合は、WebPのサポートを簡単に導入するのに役立つ拡張機能があります。 例えば、WordPress向けのOptimusCache Enablerや、 Joomlaの自前のサポート拡張機能などです(Cody Arsenaultの記事より)。 また、React、styled components、gatsby-imageによって<picture>要素を抽象化することもできます。

宣伝になってしまい恐縮ですが、Jeremy WagnerはWebPについてのSmashing Bookを発行しています。 WebPに関する全てに興味があるなら、チェックしてみると良いかもしれません。

24. AVIFを使用するか?

あなたも聞いたことがあるかもしれませんが、AVIFの登場は大ニュースです。 AVIFはAV1動画のキーフレームから派生した新たな画像形式です。 オープンでロイヤルティフリーのフォーマットで、ロスありとロスなしの圧縮、アニメーション、 ロスありのアルファチャンネルをサポートしています。 (JPEGでは問題だった)シャープな線とベタ一色を処理することができ、 どちらについても他の形式より良い結果を生み出します

実際、WebPやJPEGと比較すると、AVIFのパフォーマンスははるかに優れています。 AVIFでは、DSSIM(人間の視覚を近似したアルゴリズムに基づいて測定した、2つ以上の画像の(非)類似度)を同一に維持しつつ、 ファイル容量を平均で50%も削減できます。 Malte Ublは、画像読み込みの最適化に関して徹底的に解説した記事で、 「(AVIFの)パフォーマンスは常に一貫してJPEGを大幅に上回っています。これはWebPとは異なる点です。WebPは、常にJPEGより容量が小さい画像を生成するとは限らず、段階的な読み込みをサポートしていないため、実際は全体でマイナスとなっている可能性があります」と述べています。

AVIFをプログレッシブエンハンスメントとして使用し、古いブラウザにはWebP、JPEGやPNGを配信できます。(プレビューを拡大)。以下のプレーンテキストビューをご覧ください。

皮肉なことに、AVIFのパフォーマンスは大規模なSVGさえ上回っています。 ただし、当然ながら、SVGの代わりとみなすべきではありません。 AVIFはHDRカラーをサポートする初めての画像形式の1つでもあり、明度、色深度、色域が向上しています。 唯一のデメリットは、AVIFは現在、段階的な画像デコードを(今のところは?)サポートしておらず、 Brotliと同様に高い圧縮率でのエンコードが現時点では非常に遅いことです(ただし、デコードは高速です)。

AVIFは現在、Chrome、Firefox、Operaでサポートされており、Safariでも間もなくサポートされる見込みです(AppleはAV1を開発したグループの一員であるため)。

それでは現在、画像を提供するための最高の方法は何なのでしょうか? イラストとベクター画像については、(圧縮した)SVGが間違いなく最高の選択肢です。 写真については、私たちはpicture要素によるコンテンツネゴシエーションの手法を使用しています。 AVIFがサポートされていればAVIF画像を送信します。 そうでない場合は、まずはWebPにフォールバックし、WebPもサポートされていない場合は、 フォールバックとしてJPEGかPNGに切り替えます(必要に応じて@mediaの条件を適用します)。

<picture>
  <source srcset="image.avif" type="image/avif">
  <source srcset="image.webp" type="image/webp">
  <img src="image.jpg" alt="Photo" width="450" height="350">
</picture>

ただし、正直に言うと、picture要素内の何らかの条件を使用する可能性のほうが高いでしょう。

<picture>
<source
  sizes="(max-width: 608px) 100vw, 608px"
  srcset="
    /img/Z1s3TKV-1920w.avif 1920w,
    /img/Z1s3TKV-1280w.avif 1280w,
    /img/Z1s3TKV-640w.avif   640w,
    /img/Z1s3TKV-320w.avif   320w"
  type="image/avif"
/>
<source
  sizes="(max-width: 608px) 100vw, 608px"
  srcset="
    /img/Z1s3TKV-1920w.webp 1920w,
    /img/Z1s3TKV-1280w.webp 1280w,
    /img/Z1s3TKV-640w.webp   640w,
    /img/Z1s3TKV-320w.webp   320w"
  type="image/webp"
/>
<source
  sizes="(max-width: 608px) 100vw, 608px"
  srcset="
    /img/Z1s3TKV-1920w.jpg 1920w,
    /img/Z1s3TKV-1280w.jpg 1280w,
    /img/Z1s3TKV-640w.jpg   640w,
    /img/Z1s3TKV-320w.jpg   320w"
  type="image/jpeg"
/>
  <img src="fallback-image.jpg" alt="Photo" width="450" height="350">
</picture>

prefers-reduced-motionを使用すれば、モーションが少ないほうを好む顧客向けに、 アニメーション付きの画像を静止画と交換することもできます。

<picture>
  <source media="(prefers-reduced-motion: reduce)" srcset="no-motion.avif" type="image/avif"></source>
  <source media="(prefers-reduced-motion: reduce)" srcset="no-motion.jpg" type="image/jpeg"></source>
  <source srcset="motion.avif" type="image/avif"></source>
  <img src="motion.jpg" alt="Animated AVIF">
</picture>

ここ数カ月でAVIFは勢いを大きく増しています。

それでは、未来を担うのはAVIFなのでしょうか。 Jon Sneyersは、そうは考えていません。AVIFのパフォーマンスはJPEG XLを60%下回っていますJPEG XLとは、GoogleとCloudinaryが開発した別の無料のオープンフォーマットです。 実際、JPEG XLは全体的にはるかに優れたパフォーマンスをあげているように思われます。 しかし、JPEG XLは依然として標準化の最終段階にとどまっており、どのブラウザでもまだ機能しません。 (昔ながらのInternet Explorer 9の時代から存在するMicrosoftのJPEG-XRと混同しないようにしましょう)。

Responsive Image Breakpoints Generator画像とマークアップの生成を自動化します

25. JPEG/PNG/SVGは適切に最適化されているか?

ランディングページを開発していて、ヒーロー画像をとにかく高速に読み込むことが必要不可欠である場合、JPEGをプログレッシブにし、 mozJPEG(スキャンレベルを操作することでレンダリング開始時間を改善する)かGuetzliで圧縮するようにしましょう。 Guetzliは、GoogleがZopfliとWebPの教訓を踏まえて開発した、体感的なパフォーマンスに重点を置いたオープンソースのエンコーダです。 唯一の欠点は処理時間が遅いことです(メガピクセル当たりCPU使用時間は1分)。

PNGにはPingo、SVGにはSVGOやSVGOMGを使用できます。 Webサイトの全てのSVGアセットを素早くプレビューし、コピーかダウンロードをする必要がある場合は、 svg-grabberが役立ちます。

画像最適化に関するあらゆる記事で言われていることですが、ベクターアセットを整理しておくことにも触れなければなりません。 使用しないアセットを一掃し、不必要なメタデータを削除し、アートワーク内(とSVGコード)のパスポイントの数を減らしましょう(Jeremyに感謝します)。

他にも便利なオンラインツールを利用できます。

  • Squooshは、(ロスありかロスなしの)最適な圧縮レベルで、画像を圧縮、サイズ変更、操作することができます。
  • Guetzli.itは、GuetzliでJPEG画像を圧縮・最適化するのに使えます。輪郭がシャープな画像や、ベタ一色の画像に有効です(ただし、かなり低速な可能性があります)。
  • Responsive Image Breakpoints Generatorや、CloudinaryImgixなどのサービスを使えば、画像の最適化を自動化することができます。また、多くの場合はsrcsetsizesを使うだけでも大きな効果が得られます。
  • レスポンシブマークアップの効率性のチェックには、imaging-heapを使用できます。これはさまざまなビューポートのサイズや端末のピクセル比率について、効率性を測定することが可能なコマンドラインツールです。
  • 画像が圧縮されていない状態で本番環境に導入されないように、GitHubワークフローに自動的な画像圧縮を追加することも可能です。このアクションはPNGとJPGに対応したmozjpegとlibvipsを使用します。
  • 内部でのストレージの最適化には、Dropboxが開発した新たなLepton形式を使用できるかもしれません。LeptonはJPEGを平均22%の割合でロスレス圧縮するものです。
  • プレースホルダ画像を早く表示したい場合は、BlurHashを使用しましょう。BlurHashは画像を取り込み、その画像のプレースホルダとなる短い文字列(わずか20~30字)を提供します。文字列は十分に短いため、JSONオブジェクトにフィールドとして簡単に追加できます。

BlurHashは、画像のプレースホルダを小さくコンパクトに表示します。(プレビューを拡大

ときには、画像の最適化だけではうまくいかないこともあります。 クリティカルな画像のレンダリング開始までに必要な時間を短縮するには、 あまり重要でない画像の読み込みを遅らせ、スクリプトの読み込みはクリティカルな画像のレンダリング後まで先送りしましょう。 最も安全な方法は、ネイティブな遅延読み込みとlazyloadを併用する ハイブリッド遅延読み込みです。 lazyloadは、IntersectionObserverを使用して、ユーザインタラクションによって発生したあらゆる表示の変化を検知するライブラリです(IntersectionObserverについては後ほど詳しく説明します)。 さらに、以下のような手法もあります。

  • クリティカルな画像を先読みして、 ブラウザによる画像の発見が遅くなり過ぎないようにできないか検討してみましょう。 背景画像については、もっと積極的な最適化を目指すのであれば、<img src>で通常の画像として追加した後に画面から隠すという方法もあります。
  • メディアクエリに応じて、画像を表示する際に異なる寸法を指定し、sizes属性を利用して画像を切り替えることを検討しましょう。 これにより、例えばsizesを操作して、ルーペコンポーネントのソースを切り替えられるようになります。
  • 画像のダウンロードが一貫しているかをチェックし、 前景や背景の画像の予期しないダウンロードを防ぎましょう。 注意すべきなのは、カルーセル、アコーディオン、画像ギャラリーなど、デフォルトで読み込まれるが、まったく表示されない可能性がある画像です。
  • 画像には必ずwidthheightを設定してくださいCSSのaspect-ratioプロパティintrinsicsize属性に注意しましょう。 これにより、画像のアスペクト比と寸法を設定することで、ブラウザが所定のレイアウト枠を早いうちに確保し、 ページ読み込み中のレイアウトのジャンプを回避できます。

おそらく今後わずか数週間か数カ月で、各ブラウザはaspect-ratioに対応するようになるでしょう。Safari Technical Preview 118にはすでに搭載されており、FirefoxとChromeにもflags(試験運用機能)として実装されています。プレビューを拡大

思い切ってエッジワーカーを使用し、HTTP/2ストリームを分割・再編することで、ネットワークを通じた画像の送信を高速化することもできます。 エッジワーカーとは、簡単に言えば、CDN上で実行されるリアルタイムフィルタです。 エッジワーカーは、コントロール可能なチャンクを利用したJavaScriptストリームを使用しているため(これらは基本的に、ストリーミングの応答を変更できるCDNエッジ上で実行されるJavaScriptです)、 画像の配信をコントロールすることが可能です。

サービスワーカーでは通信中の内容をコントロールできないため、上記の仕組みでは間に合いませんが、エッジワーカーでは機能します。 そのため、特定のランディングページ向けに段階的に保存された静的JPEGとともに使用できます。

imaging-heapのサンプル出力。imaging-heapは、さまざまなビューポートのサイズや端末のピクセル比率について、効率性を測定することが可能なコマンドラインツールです。(画像の出典)(プレビューを拡大

まだ最適化が足りない場合は、背景画像に関する複数のテクニックによっても画像の体感的なパフォーマンスを改善できます。 コントラストを調整し、 不必要な細部をぼかす(または色を削除する)ことで、ファイルの容量を縮小できることも忘れないでください。 品質を低下させることなく、小さい写真を拡大する必要がある場合は、Letsenhance.ioの使用を検討しましょう。

これまでの最適化は基本をカバーしているに過ぎません。 Addy Osmaniは、重要な画像の最適化に関するとても詳細なガイドを公表しています。 このガイドでは、画像の圧縮とカラーマネジメントについて非常に深く解説しています。 例えば、(ガウスぼかしフィルタを適用して)画像の不要な部分をぼかすことでファイル容量を削減できます。 最終的には、色を削除したり、写真を白黒にしたりすることで、容量をさらに削減することも可能です。 背景画像については、Photoshopから0~10%の品質で写真を書き出せば、完全に許容できるレベルになります。

Smashing Magazineでは、画像名の末尾に-optをつけるようにしています(例えばbrotli-compression-opt.png)。 画像にこの接尾辞が付いていれば、チームの誰もが、その画像がすでに最適化されていることが分かります。

それと、JPEG-XRをWebで使用しないようにしましょう。 「仮にバイト容量の削減によるプラスの影響が生じていたとしても、CPU上のJPEG-XRソフトウエア側のデコード処理は、特にSPAの場合、プラスの影響を相殺するか、あるいは上回ってしまいます」 (CloudinaryとGoogleのJPEG XLと混同しないようにしてください)。

Addy Osmaniは、アニメーション付きのGIFを、ループ再生されるインライン動画で代替することを推奨しています。両者のファイル容量には大きな差があります(80%の削減)。(プレビューを拡大

26. 動画は適切に最適化されているか?

これまでは画像について扱ってきましたが、昔ながらのGIFに関する議論は避けてきました。 私たちはGIFを愛していますが、もはやGIFは(少なくともWebサイトやアプリケーションからは)永久に追放すべきです。 アニメーション付きの重いGIFを読み込むと、レンダリングのパフォーマンスと帯域幅の両方に影響を与えます。 アニメーション付きのWebPに切り替える(GIFをフォールバックとする)か、 ループ再生されるHTML5動画とGIFを完全に入れ替えると良いでしょう。

画像とは異なり、ブラウザは<video>コンテンツを先読みしません。 しかし、HTML5動画は通常、GIFよりもはるかに軽く、容量も小さくなります。 HTML5動画が選択肢に入らない場合は、Lossy GIFgifsicleあるいはgiflossyを使用すれば、 少なくともGIFをロスありで圧縮することができます。

Colin Bendellのテストによれば、Safari Technology Previewにおけるimgタグ内のインライン動画は、 同等のGIFに比べて20倍以上速く表示され、7倍以上速くデコードされます。 さらに、ファイルの容量もGIFに比べてごくわずかです。ただし、他のブラウザではサポートされていません。

うれしいことに、動画の形式は、長年の間にとても大きな進歩を遂げてきました。 長い間、私たちはWebMが支配的な形式になることを望んでいました。 そして、WebP(本質的にはWebM動画コンテナ内部の静止画の1つ)は時代遅れの画像形式に代わる存在になろうとしています。 実際、Safariは現在、WebPをサポートしています。 しかし、最近はWebPWebMのサポートが増えてきたにもかかわらず、真のブレークスルーは起きていませんでした。

それでも、WebMはほとんどのモダンなブラウザで使用することができました。

<!-- By Houssein Djirdeh. https://web.dev/replace-gifs-with-videos/ -->
<!-- A common scenartio: MP4 with a WEBM fallback. -->
<video autoplay loop muted playsinline>
  <source src="my-animation.webm" type="video/webm">
  <source src="my-animation.mp4" type="video/mp4">
</video>

しかし、もしかすると、状況は完全に変わるかもしれません。 2018年、Alliance of Open Mediaは、AV1という新しい有力な動画形式を発表しました。 AV1はH.265コーデック(H.264の進化版)と同様の圧縮を実現しますが、H.265とは違って無料です。 ブラウザベンダは、H.265ライセンスの価格設定を受けて、同等のパフォーマンスを有するAV1を代わりに採用しました。 AV1は(H.265と同様に)WebMの2倍の圧縮性能を持っています

AV1はWeb上の動画の最終的なスタンダードになる可能性が十分にあります。(画像の出典:Wikimedia.org)(プレビューを拡大)

実際、Appleは現在、HEIF形式とHEVC(H.265)を使用しており、最新のiOSでは全ての写真と動画をJPEGではなくHEIFとHEVCで保存しています。 HEIFHEVC(H.265)は、(今のところは?)Webにあまり普及していませんが、AV1はそうではありません。 さらに、AV1をサポートするブラウザは増えています。全てのブラウザベンダがAV1に対応するとみられるため、<video>タグにAV1ソースを追加するのは理にかなっています。

今のところ、最も広く使用・サポートされているエンコード形式はH.264で、MP4ファイル形式で提供されています。 ですから、ファイルを提供する前に、MP4がマルチパスエンコードで処理されていること、 frei0r iirblur effectによるぼかしが行われていること(該当する場合)、moov atomメタデータがファイルの先頭に移動していること、 サーバがバイトサービングを許可していることを確認しましょう。 Boris Schapiraは、動画を最大限まで最適化するためのFFmpegの正確な手引きを提供しています。 もちろん、WebM形式を代わりとして提供するのも良いでしょう。

動画のレンダリングの高速化を始める必要があるが、 動画ファイルの容量が依然として大き過ぎる場合はどうすべきでしょうか。 例えば、ランディングページに大容量の背景動画がある場合などです。 よくあるテクニックは、まずは1番目のフレームを静止画として表示するか、 または大幅に最適化した短いループのセグメント(動画の一部に見える)を表示し、 次に動画のバッファが十分にたまったら実際の動画を再生するというものです。 Doug Sillarsが執筆した背景動画のパフォーマンスに関する詳しいガイドは、 このようなケースで役立つかもしれません。(Guy Podjarnyに感謝します)。

上記のシナリオでは、レスポンシブなポスター画像を提供したい場合もあるかもしれません。 デフォルトでは、video要素は1つの画像のみをポスターとして受け入れますが、それが必ずしも最適とは限りません。 このような場合はResponsive Video Posterが役立ちます。 このJavaScriptライブラリは、画面ごとに異なるポスター画像を使用することを可能にする一方、トランジションオーバーレイを追加することや、 動画のプレースホルダのスタイルを完全にコントロールすることもできます。

調査によれば、動画のストリームの品質は視聴者の行動に影響します。 実際、開始の遅れが約2秒を超えると、視聴者は動画から離脱し始めます。 この点を過ぎると、遅延が1秒増えるごとに、離脱率は約5.8%上昇します。 平均的な動画の開始時間が12.8秒で、 40%の動画が1回以上停止し、20%の動画の再生が2秒以上停止することは驚きではありません。 実際、動画はネットワークがコンテンツを供給するよりも速く再生されるため、3Gでは動画が停止することは避けられません。

それでは、どうすれば解決できるのでしょうか。 通常、小型画面の端末は、デスクトップに提供される720pや1080pの解像度に対応できません。 Doug Sillarsによれば、 小さいバージョンの動画を作成することや、JavaScriptを使用して小型画面向けのソースを検知することによって、 こうした端末でも確実に高速でスムーズな再生を実現できます。 別の選択肢として、ストリーミング動画を使用することも可能です。 HLS動画ストリームは、適切なサイズの動画を端末に配信し、画面ごとに異なる動画を作成する必要がなくなります。 また、ネットワーク速度をネゴシエートし、使用しているネットワークの速度に合わせて動画のビットレートを調整します。

帯域幅のムダを回避するには、動画を実際にしっかりと再生できる端末のみに向けて、動画のソースを追加するという方法があります。 別の方法としては、videoタグからautoplay属性を完全に削除し、JavaScriptを使用して大画面向けにautoplayを挿入するという手もあります。 さらに、動画ファイルが実際に必要となるまで、ブラウザがそのファイルをまったくダウンロードしないようにするには、videopreload="none"を追加しなければなりません。

<!-- Based on Doug Sillars's post. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ -->
<video id="hero-video"
          preload="none"
          playsinline
          muted
          loop
          width="1920"
          height="1080"
          poster="poster.jpg">
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>

それから、AV1を実際にサポートしているブラウザに狙いを絞ることができます。

<!-- Based on Doug Sillars's post. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ -->
<video id="hero-video"
          preload="none"
          playsinline
          muted
          loop
          width="1920"
          height="1080"
          poster="poster.jpg">
<source src="video.av1.mp4" type="video/mp4; codecs=av01.0.05M.08">
<source src="video.hevc.mp4" type="video/mp4; codecs=hevc">
<source src="video.webm" type="video/webm">
<source src="video.mp4" type="video/mp4">
</video>

そのうえ、特定の基準値(1000pxなど)を超える場合は、autoplayを再び追加することも可能です。

/* By Doug Sillars. https://dougsillars.com/2020/01/06/hiding-videos-on-the-mbile-web/ */
<script>
    window.onload = addAutoplay();
    var videoLocation  = document.getElementById("hero-video");
    function addAutoplay() {
        if(window.innerWidth > 1000){
            videoLocation.setAttribute("autoplay","");
      };
    }
</script>

端末とネットワーク速度ごとの動画の停止回数。高速なネットワークに接続している高速な端末は、ほとんど停止していません。Doug Sillarsの調査より。(プレビューを拡大

動画再生のパフォーマンスは1つの独立した大きなテーマです。 詳しく知りたい場合は、Doug SillarsのThe Current State of VideoVideo Delivery Best Practicesという別のシリーズ記事をご覧ください。 これらの記事では、動画配信の指標、動画の先読み、圧縮、ストリーミングについて詳しく扱っています。 最後に、Stream or Notでは自社サイトの動画ストリーミングがどれだけ速いか(あるいは遅いか)を確認することが可能です。

Zach LeathermanのComprehensive Guide to Font-Loading Strategiesは、Webフォントの配信を改善するための数多くの選択肢を紹介しています。

27. Webフォントの配信は最適化されているか?

最初に問うべき疑問は、そもそもUIシステムフォントを使用すれば良いのではないか、ということです。 UIシステムフォントなら、さまざまなプラットフォームで正確に表示されることをダブルチェックするだけで済みます。 UIシステムフォントを使用できない場合、あなたが使用するWebフォントには、使われていないグリフや追加的な機能・太さが含まれている可能性が高いでしょう。 フォント開発会社に依頼すれば、Webフォントをサブセット化することが可能です。 オープンソースのフォントを使用する場合であれば、GlyphhangerFontsquirrelを使えば自分でサブセット化できます。 Peter Müllerのsubfontでは、ワークフロー全体を自動化することさえ可能です。 このコマンドラインツールは、ページを静的に分析することで、Webフォントの最適なサブセットを生成し、ページに挿入します。

WOFF2のサポートはとても有用です。 ブラウザがWOFF2をサポートしていない場合は、WOFFをフォールバックとして使用できます。 あるいは、古いブラウザにはシステムフォントを提供しても良いでしょう。 Webフォント読み込みの選択肢は本当に数多く存在しており、 Zach LeathermanのComprehensive Guide to Font-Loading Strategiesという記事ではそういった戦略の1つを選ぶことができます (Web font loading recipesではコードのスニペットも提供されています)。

現在、検討に値する選択肢は、preloadによるCritical FOFTと、 「Compromise」法でしょう。 いずれの選択肢も、Webフォントを段階的に配信する2段階レンダリングを使用しています。 まず、ページを高速かつ正確にレンダリングするために必要なWebフォントのごく一部を読み込み、次に残りのファミリーを非同期で読み込みます。 両者の違いは、「Compromise」法では、フォントの読み込みイベントがサポートされていない場合に限り、 ポリフィルを非同期で読み込むということです。 これにより、ポリフィルをデフォルトで読み込む必要がなくなります。 手軽に成果を上げる必要があるときは、Zach Leathermanのわずか23分でフォントを整理するためのチュートリアルとケーススタディを参考にしましょう。

一般に、フォントの先読みのためにpreloadのリソースヒントを使用するのは良いアイデアでしょう。 ただし、マークアップの中では、クリティカルなCSSとJavaScriptへのリンクの後にヒントを設置するようにしてください。 preloadを使用すると優先順位が複雑になります。 そこで、DOMにおいて、外部のブロッキングスクリプトの直前にrel="preload"要素を挿入することを検討しましょう。 Andy Daviesによれば、「スクリプトを使用して挿入されたリソースは、スクリプトが実行されるまでブラウザからは隠れています。 この挙動を利用して、ブラウザがpreloadヒントを検知するタイミングを遅らせることができます」。 さもなければ、フォントの読み込みによって初期のレンダリング時間が延びるでしょう。

全てが重要だということは、何も重要でないということと同じです。各ファミリーのうち、1つだけ、あるいは最大でも2つのフォントを先読みするようにしましょう。(画像の出典:Zach Leatherman – スライド93)(プレビューを拡大

対象を絞り込み、最も重要なファイルを選択するのは良いアイデアです。 例えば、レンダリングに必要不可欠なファイルや、目に見える邪魔なテキストのリフローを回避するのに役立つファイルなどです。 一般論として、Zachは各ファミリーにつき1つか2つのフォントを先読みすることを推奨しています。 一部のフォントがあまり重要でない場合は、読み込みを遅らせるのも理にかなっています。

@font-face規則においてfont-familyを定義するときに、local()バリュー(ローカルフォントを名前によって参照する)を使用することはとても一般的になりました。

/* Warning! Not a good idea! */
@font-face {
  font-family: Open Sans;
  src: local('Open Sans Regular'),
       local('OpenSans-Regular'),
       url('opensans.woff2') format ('woff2'),
       url('opensans.woff') format('woff');
}

このアイデアは合理的です。 Open Sansなど、一部の有名なオープンソースフォントは、ドライバやアプリケーションによっては事前にインストールされていることもあります。 この場合、フォントがローカル環境で利用できれば、ブラウザはWebフォントをダウンロードする必要がなく、ローカルフォントをすぐに表示できます。 Bram Steinが指摘しているとおり、 「ローカルフォントとWebフォントの名前が同じであっても、同じフォントではない可能性が高いと言えます。 多くのWebフォントは『デスクトップ』版とは異なっています。テキストが異なる形でレンダリングされたり、 一部の文字が別のフォントにフォールバックされたりする可能性があります。 OpenType機能が完全に欠けていることや、行の高さが変わることも考えられます」。

また、書体は時とともに変化するため、ローカル環境にインストールしたバージョンがWebフォントとまったく異なっており、 文字の見た目が大きく変わる可能性もあります。 そのため、Bramは、@font-face規則において、ローカル環境にインストールしたフォントとWebフォントを決して一緒にしないことを推奨しています。 Google Fontsでは、RobotoのAndroidリクエストを除き、全てのユーザのCSSの出力結果におけるlocal()を無効化することで、同様の措置を講じています。

コンテンツの表示を待つのが好きな人はいません。 CSSのfont-display記述子を使用すれば、フォント読み込みの挙動をコントロールすることが可能です。 font-display: optionalなら、コンテンツが即座に読める状態になります。 font-display: swapを使用する場合でも、フォントのダウンロードが完了するまで3秒のタイムアウトがあるものの、 ほとんど即座に読むことができます(ただし、実際はもう少し複雑です)。

ただし、テキストのリフローの影響を最小限に抑えたい場合は、 Font Loading APIが便利です(全てのモダンなブラウザでサポートされています)。 これは、具体的には、あらゆるフォントについてFontFaceオブジェクトを作成し、 その全てを取得した後にのみ、それらをページに適用することを意味します。 このように、全てのフォントを非同期で読み込み、フォールバックフォントからWebフォントへ一斉に切り替えることによって、全ての再描画をグループ化しますZachの説明(32:15から始まります)とコードのスニペットをご覧ください。

/* Load two web fonts using JavaScript */
/* Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */

// Remove existing @font-face blocks
// Create two
let font = new FontFace("Noto Serif", /* ... */);
let fontBold = new FontFace("Noto Serif, /* ... */);

// Load two fonts
let fonts = await Promise.all([
  font.load(),
  fontBold.load()
])

// Group repaints and render both fonts at the same time!
fonts.forEach(font => documents.fonts.add(font));

Adrian Beceは、Font Loading APIを使用した最初のフォント取得の方法として、bodyの最上部にノーブレークスペースのnbsp;を追加し、 それをaria-visibility: hidden.hiddenクラスで非表示にすることを提案しています。

<body class="no-js">
  <!-- ... Website content ... -->
  <div aria-visibility="hidden" class="hidden" style="font-family: '[web-font-name]'">
      <!-- There is a non-breaking space here -->
  </div>
  <script>
    document.getElementsByTagName("body")[0].classList.remove("no-js");
  </script>
</body>

これは、異なる読み込み状態に対して異なるフォントファミリーを宣言するCSSと相性が良く、フォントの読み込みが完了するとFont Loading APIによって変更がトリガーされます。

body:not(.wf-merriweather--loaded):not(.no-js) {
  font-family: [fallback-system-font];
  /* Fallback font styles */
}

.wf-merriweather--loaded,
.no-js {
  font-family: "[web-font-name]";
  /* Webfont styles */
}

/* Accessible hiding */
.hidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
}

あらゆる最適化を試したにもかかわらず、依然としてLighthouseがレンダリングをブロックするリソース(フォント)の削除を提案してくることを不思議に思った経験はありませんか。 Adrian Beceは、上記と同じ記事で、Lighthouseを満足させるためのいくつかのテクニックや、 Gatsby Omni Font Loaderを紹介しています。 Gatsby Omni Font Loaderは、高パフォーマンスな非同期のフォント読み込みと、フォント切り替えに伴うちらつき(FOUT)の処理に対応したGatsby向けのプラグインです。

現在、多くの人は、Webフォントの読み込み先としてCDNやサードパーティのホストを使用しているかもしれません。 一般に、可能ならば常に、全ての静的アセットを自分でホストするほうが望ましいと言えます。 Google Fontsを簡単に自分でホストする方法として、google-webfonts-helperを使用することを検討してみましょう。 それが不可能である場合は、ページのオリジンを通じて、Google Fontファイルをプロキシすることができるかもしれません。

ただし、注目に値する点として、Googleは初期設定の時点でとても多くの仕事を済ませているため、 微修正さえすれば、サーバの遅延を回避できる可能性があります(Barryに感謝します)。

これは特にChrome v86(2020年10月リリース)以降は非常に重要となります。 なぜなら、ブラウザのキャッシュがサイトごとに分割されるため、 フォントなどの複数のサイトをまたぐリソースは同一のCDNで共有できなくなるからです。 この挙動は、長年にわたってSafariのデフォルトでした。

しかし、上記の手法がまったく使えない場合は、Harry Robertsのスニペットを利用すれば、 可能な限り速くGoogle Fontsを取得できます。

<!-- By Harry Roberts.
https://csswizardry.com/2020/05/the-fastest-google-fonts/

- 1. Preemptively warm up the fonts’ origin.
- 2. Initiate a high-priority, asynchronous fetch for the CSS file. Works in
-    most modern browsers.
- 3. Initiate a low-priority, asynchronous fetch that gets applied to the page
-    only after it’s arrived. Works in all browsers with JavaScript enabled.
- 4. In the unlikely event that a visitor has intentionally disabled
-    JavaScript, fall back to the original method. The good news is that,
-    although this is a render-blocking request, it can still make use of the
-    preconnect which makes it marginally faster than the default.
-->

<!-- [1] -->
<link rel="preconnect"
      href="https://fonts.gstatic.com"
      crossorigin />

<!-- [2] -->
<link rel="preload"
      as="style"
      href="$CSS&display=swap" />

<!-- [3] -->
<link rel="stylesheet"
      href="$CSS&display=swap"
      media="print" onload="this.media='all'" />

<!-- [4] -->
<noscript>
  <link rel="stylesheet"
        href="$CSS&display=swap" />
</noscript>

Harryの戦略では、まずフォントのオリジンを事前にアップしておきます。 次に、優先度が高いCSSファイルを非同期で取得し始めます。 その後、優先度が低いファイルを非同期で取得し、完了後にのみページに適用します (スタイルシートのprint属性に関するテクニックを利用します)。 最後に、JavaScriptがサポートされていない場合、当初の手法にフォールバックします。

Google Fontsと言えば、&textを使用して、必要な文字だけを宣言することで、 Google Fontsリクエストの容量を最大で90%削減できます。 さらに、Google Fontsには最近、font-displayのサポートも追加されたため、すぐに使用することができます。

ただし、注意すべき点もあります。font-display: optionalを使用する場合、 preloadを併用するのは最適ではない可能性があります。 なぜなら、preloadはWebフォントのリクエストを早い段階でトリガーするからです(取得する必要があるクリティカルパスのリソースが他に存在する場合、 ネットワークの輻輳を引き起こします)。オリジンをまたぐフォントリクエストを高速化するにはpreconnectを使用しましょう。 ただし、異なるオリジンからフォントを先読みすると、ネットワーク上で競合が起きるため、preloadには注意しなければなりません。 これらのテクニックは全て、ZachのWeb font loading recipesで取り上げられています。

一方、ユーザがアクセシビリティ設定においてReduce Motionを有効にしている場合、 省データモードを選択している場合(Save-Dataヘッダを参照)、 あるいはユーザの接続が遅い場合(Network Information APIを通じて判定)は、 Webフォントを使用しない (または、少なくとも2段階目のレンダリングの対象とする)ことが賢明かもしれません。

また、CSSメディアクエリのprefers-reduced-dataを使用して、 ユーザが省データモードを選択している場合はフォント宣言を定義しないようにすることもできます (他の使用方法もあります)。 このメディアクエリは基本的に、CSSが使用できるようにするために、クライアントヒントHTTP拡張機能からのSave-Dataリクエストヘッダがオン/オフのどちらになっているかを明らかにするものです。 現在はChromeとEdgeのみでflagsとしてサポートされています

指標にはどのようなものがあるでしょうか。Webフォント読み込みのパフォーマンスを測定したい場合は、 All Text Visible (全てのフォントが読み込まれ、全てのコンテンツがWebフォントで表示された時点)、 Time to Real Italics、初回のレンダリング後のWeb Font Reflow Countの導入を検討してみましょう。 当然ながら、どの指標も小さいほどパフォーマンスが優れています。

可変フォントはどうでしょうか。重要な点として、可変フォントは、 パフォーマンスに大きな影響を与える可能性があります。 可変フォントによって、幅広いタイポグラフィックのデザインを選択することが可能になりますが、 複数の個別ファイルのリクエストではなく、1つの連続リクエストを送信しなければならないというコストがかかります。

可変フォントはフォントファイル全体の合計容量を劇的に削減しますが、 上記の1つのリクエストの処理が遅くなり、ページの全てのコンテンツのレンダリングをブロックする可能性があります。 したがって、フォントをサブセット化して、いくつかの文字セットに分割することは、依然として重要です。 ただし、メリットとして、可変フォントを導入するとデフォルトのリフローはきっかり1回となるため、再描画のグループ化にJavaScriptが必要ありません。

それでは、盤石のWebフォント読み込み戦略とはどのようなものでしょうか。 それは、フォントをサブセットに分割し、2段階のレンダリングを可能にすること、font-display記述子でそれを宣言すること、 Font Loading APIを使用して再描画をグループ化すること、持続的なサービスワーカーのキャッシュでフォントを保存することです。 初回アクセス時には、外部のスクリプトにブロックされる直前にスクリプトの先読みを挿入します。 必要があれば、Bram SteinのFont Face Observerにフォールバックすることも可能です。 フォント読み込みのパフォーマンス測定に関心がある方は、 Andreas MarschkeがFont APIとUserTiming APIのパフォーマンス測定について追求した記事をご覧ください。

最後に、大規模なフォントをより小規模な言語別のフォントに分割するために、unicode-rangeを含めることを忘れないでください。 また、Monica Dinculescuのfont-style-matcherを使用すると、 フォールバックフォントとWebフォントのサイズの違いによるレイアウトの不快な変化を最小限にすることができます。

あるいは、フォールバックフォントでWebフォントをエミュレートするには、 @font-face記述子でフォントメトリックを上書きするという方法もあります (デモはこちら。Chrome 87で有効)(ただし、複雑なフォントスタックでは複雑な調整が必要であることにご注意ください)。

これで未来は明るいと言えるでしょうか。 フォントの段階的なエンリッチメントにより、 いずれは「最初はどのページでもフォントの必要な部分のみをダウンロードし、そのフォントの次のリクエストは、 その後のページの表示に必要となる追加的なグリフをダウンロードすることで、初回ダウンロードの動的な『パッチ』とする」ことができるようになるかもしれないと、 Jason Pamentalは説明しています。Incremental Transfer Demoはすでに利用可能であり、 フォントの段階的なエンリッチメントに向けた取り組みは進行中です

ビルドの最適化 #

28. 優先順位を定義しているか?

何を最初に処理すべきかを把握するのは良いアイデアです。 全てのアセット(JavaScript、画像、フォント、サードパーティのスクリプトや、カルーセル、複雑なインフォグラフィック、マルチメディアコンテンツなどのページ上の「コストが重い」モジュール)のインベントリを実行し、アセットをグループに分けましょう。

スプレッドシートを設定しましょう。 古いブラウザ向けの基本的なコアとなるユーザ体験(完全なアクセスが可能なコアとなるコンテンツ)、高性能なブラウザ向けの高水準なユーザ体験(リッチで完全な体験)、 それ以外の部分(絶対に必要というわけではなく、遅延読み込みが可能なアセット。Webフォント、不必要なスタイル、カルーセルのスクリプト、動画プレーヤ、ソーシャルメディアウィジェット、大きな画像)を定義してください。 数年前に私たちが公表した「Improving Smashing Magazine’s Performance」という記事では、 このアプローチについて詳しく説明しています。

パフォーマンスを最適化するときは、優先順位を反映する必要があります。 コアとなるユーザ体験がすぐに読み込まれ、次に高水準なユーザ体験、最後にそれ以外の部分が読み込まれるようにしましょう。

29. 本番環境でネイティブJavaScriptモジュールを使用しているか?

コアとなるユーザ体験を古いブラウザに、高水準なユーザ体験をモダンなブラウザに提供するために、 かつては環境に合わせたテクニックを使用していたことを覚えていますか。 このテクニックのアップデート版では、 ES2017+ <script type="module">、別名module/nomoduleパターンを使用できる可能性があります (Jeremy WagnerによってDifferential Servingとしても紹介されています)。

この手法のアイデアは、2つの異なるJavaScriptバンドルをコンパイルして提供するというものです。 1つ目はBabel-transformとポリフィル付きの「通常」のビルドで、それらを実際に必要とする古いブラウザのみに提供します。 もう1つのバンドル(機能は同一)にはtransformもポリフィルもありません。

その結果、ブラウザが処理する必要があるスクリプトの量が減少し、メインスレッドのブロックを削減するのに役立ちます。 Jeremy WagnerはDifferential Servingについて幅広く取り扱った記事を公表しています。 この記事では、ビルドのパイプラインにおいてDifferential Servingを設定する方法(Babelの設定から、Webpackにおいて必要な微修正まで)や、 一連の作業に労力を費やすメリットについて紹介しています。

ネイティブJavaScriptモジュールスクリプトはデフォルトで遅延するようになっているため、 HTMLをパースしている間にブラウザがメインモジュールをダウンロードします。

ネイティブJavaScriptモジュールはデフォルトで遅延するようになっています。ネイティブJavaScriptモジュールのほぼ全てに関する記事はこちら。(プレビューを拡大

ただし、注意すべき点として、module/nomoduleパターンは一部のクライアントでは逆効果となる可能性があります。 そのため、回避策を検討するほうが良いかもしれません。 Jeremyは、プリロードスキャナを避けるリスクが比較的低いDifferential Servingのパターンを紹介していますが、 この方法は予期しない形でパフォーマンスに影響を及ぼす恐れがあります(Jeremyに感謝します)。

実際、Rollupはモジュールを出力形式としてサポートしているため、 コードをバンドルしつつ、モジュールを本番環境にデプロイすることができます。 ParcelはParcel 2でモジュールをサポートしています。 Webpackでは、module-nomodule-pluginがmodule/nomoduleスクリプトの生成を自動化します。

注記:注意すべき点として、機能検出だけでは、そのブラウザに送るペイロードについて、十分な情報に基づく意思決定はできません。 ブラウザのバージョンだけでは、端末の能力を推測することは不可能です。 例えば、発展途上国の安価なAndroidスマートフォンのほとんどにはChromeが搭載されており、メモリとCPUの性能に制約があるにもかかわらず要求水準を満たしています。

最終的には、Device Memory Client Hints Headerを使用することで、 より信頼性が高い方法でローエンドの端末をターゲットとすることができるでしょう。 この記事の執筆時点では、このヘッダはBlinkのみでサポートされています(これは一般にクライアントヒントの代わりとなります)。 Device MemoryにはChromeで利用可能なJavaScript APIも存在します。 そのため、APIに基づく機能検知を行い、それがサポートされていない場合はmodule/nomoduleパターンにフォールバックするという選択肢もあり得るでしょう(Yoavに感謝します)。

30. ツリーシェイキング、スコープホイスティング、コード分割を使用しているか?

ツリーシェイキングは、 Webpackにおいて、本番環境で実際に使用されるコードのみを採用し 、使われていないインポートを削除することにより、ビルドのプロセスを整理する方法の1つです。 WebpackとRollupのスコープホイスティングは、 これらのツールが、importのチェーンをフラット化し、コードを傷つけることなく1つのインライン関数に変換できる場所を発見することを可能にします。 WebpackではJSON Tree Shakingも使用できます。

コード分割はWebpackの機能の1つで、必要に応じて読み込まれる「チャンク」にコードベースを分割するものです。 全てのJavaScriptをすぐにダウンロード、パース、コンパイルする必要はありません。コードを分割するポイントを定めれば、 Webpackが依存関係と出力ファイルを処理してくれます。 これにより、初期のダウンロードの容量を小規模に維持し、アプリケーションにリクエストされたときに必要に応じてコードをリクエストすることができます。 Alexander Kondrovは、WebpackとReactによるコード分割についての素晴らしい紹介記事を公表しています。

preload-webpack-pluginの使用を検討しましょう。 これはコード分割のルートを示し、<link rel="preload"><link rel="prefetch">を使用してブラウザによる先読みを促すものです。 Webpackインラインディレクティブpreloadprefetchをある程度コントロールできます (ただし、優先順位の問題には注意しましょう)。

分割のポイントを決定するには、CSSやJavaScriptのどのチャンクが使用され、どのチャンクが使用されていないかを追跡する必要があります。 Umar Hansaは、DevtoolsのCode Coverageによってそれをどのように実現するかについて説明しています。

シングルページアプリケーションに取り組む場合、ページをレンダリングできる前に、アプリケーションを初期化するのに一定の時間が必要です。 設定にはカスタムソリューションが必要ですが、初期のレンダリング時間を高速化するためのモジュールやテクニックも探すことができます。 例えば、Reactのパフォーマンスをデバッグする方法Reactのパフォーマンスに関するよくある問題の解決方法Angularのパフォーマンスを改善する方法が紹介されています。 通常、パフォーマンスに関するほとんどの問題は、アプリケーションを最初にブートストラップするための時間に起因します。

それでは、積極的にコードを分割しつつ、分割し過ぎないようにするための最善の方法は何でしょうか。Phil Waltonによれば、 「動的インポートを通じたコード分割に加え、パッケージレベルでのコード分割を活用することもできます。 このコード分割では、パッケージの名称に基づき、インポートした各ノードモジュールをチャンクに分割します」。 Philはパッケージレベルのコード分割をビルドするためのチュートリアルも提供しています。

31. Webpackの出力を改善できるか?

Webpackは通常、よく分からない存在とみなされているため、Webpackの出力を一段と削減できる便利なWebpackプラグインは数多くあります。 以下では、比較的無名で、もっと注目する必要があるかもしれない手法を紹介します。

Ivan Akulovのスレッドでは興味深い手法が紹介されています。 ある関数を1度呼び出し、その結果を変数として保存し、その変数を使用しない場合を想像してください。 ツリーシェイキングはその変数を削除しますが、関数は削除しません。別の場所で使用される可能性があるからです。 しかし、その関数が別の場所で使用されないならば、それを削除したいと思うでしょう。 そのためには、UglifyとTerserでサポートされている/*#__PURE__*/を関数呼び出しの先頭に追加するだけで十分です。

結果が使用されない関数を削除するには、関数呼び出しの先頭に/#PURE/を追加します。Ivan Akulovより。(プレビューを拡大)

Ivanは他にもいくつかのツールを推奨しています。

  • purgecss-webpack-pluginは、特にBootstrapやTailwindを使用している場合に、利用されていないクラスを削除します。
  • optimization.splitChunks: 'all'split-chunks-pluginで有効化しましょう。これにより、Webpackがエントリーバンドルを自動的にコード分割し、キャッシングを改善することができます。
  • optimization.runtimeChunk: trueを設定しましょう。これはWebpackのランタイムを別個のチャンクに移行し、キャッシングも改善するものです。
  • google-fonts-webpack-pluginはフォントファイルをダウンロードし、自分のサーバからフォントファイルを提供することを可能にします。
  • workbox-webpack-pluginは、サービスワーカーを生成し、全てのWebpackアセットについてプリキャッシュ設定を行うことを可能にします。すぐに適用できるモジュールの包括的なガイドであるService Worker Packagesもチェックしましょう。あるいは、preload-webpack-pluginを使用して、全てのJavaScriptチャンクについてpreloadprefetchを生成しましょう。
  • speed-measure-webpack-pluginは、Webpackのビルド速度を測定し、ビルドプロセスのどのステップに最も時間がかかっているかに関する情報を提供します。
  • duplicate-package-checker-webpack-pluginは、バンドルに同一のパッケージの複数のバージョンが含まれている場合に警告します。
  • コンパイルの際に、スコープの分離を活用し、CSSクラス名を動的に短縮しましょう。

小さい画面には小さい写真を提供することで、画像を高速化できます。これにはresponsive-loaderを活用します。Ivan Akulovより。(プレビューを拡大)

32. JavaScriptをWebワーカーへオフロードできるか?

Time To Interactiveへの悪影響を軽減するには、重いJavaScriptをWebワーカーへオフロードすることを検討してみると良いかもしれません。

コードベースの拡大に伴い、UIのパフォーマンスのボトルネックが発生し、ユーザ体験が減速します。 これはメインスレッドでJavaScriptと並行してDOMオペレーションが実行されているためです。 Webワーカーでは、こうしたコストが大きいオペレーションを、別のスレッドで実行されるバックグラウンドのプロセスに移行できます。 Webワーカーの一般的な使用法は、データとProgressive Web Appsを先読みして、 一部のデータを事前に読み込み・保存し、後で必要なときに使用できるようにすることです。 また、メインページとワーカーの間の通信を合理化するために、Comlinkを使用することもできます。 まだ完全ではありませんが、ゴールには近づいています。

Webワーカーに関してはいくつかの興味深いケーススタディがあります。 これらは、フレームワークとアプリケーションのロジックをWebワーカーへ移行するさまざまなアプローチを示すものです。 結論としては、全体的に課題は残っているものの、優れたユースケースもすでに存在すると言えます(Ivan Akulovに感謝します)。

Chrome 80以降、JavaScriptモジュールのパフォーマンス上のメリットを持つ新たなモードのWebワーカーが実装されています。 これはモジュールワーカーと呼ばれます。 script type="module"に合わせてスクリプトの読み込みと実行を変更できるうえに、 ワーカーの実行をブロックすることなく、コードの遅延読み込みのための動的なインポートを使用することが可能です。

Webワーカーを始めたいという方向けに、注目に値するいくつかのリソースを紹介します。

  • SurmaはJavaScriptをブラウザのメインスレッド以外で実行するための素晴らしい手引きと、When should you be using Web Workers?という記事を公表しています。
  • メインスレッドからの移行のアーキテクチャに関するSurmaの講演もチェックしてみてください。
  • Shubhie PanickerとJason MillerのA Quest to Guarantee Responsivenessという講演は、Webワーカーをどのように使用すべきか、どのような場合は使用すべきでないかについての詳細な情報を提供しています。
  • Getting Out of Users' Way: Less Jank With Web Workersという講演では、Webワーカーをうまく使用するための便利なパターンに加え、ワーカー間の通信、メインスレッド以外での複雑なデータ処理の取り扱い、それらのテストとデバッグの効果的な方法にスポットライトを当てています。
  • Workerizeは、モジュールをWebワーカーに移動し、エクスポートされた関数を非同期のプロキシとして自動的に反映することを可能にします。
  • Webpackを使用している場合、workerize-loaderを利用できます。あるいは、worker-pluginを利用しても良いでしょう。

コードが長期にわたりブロックとなっているなら、Webワーカーを使用しましょう。ただし、DOMに依存している場合、入力への応答を取り扱う場合、遅延を最小限に抑える必要がある場合は使用してはなりません。(Addy Osmaniより)(プレビューを拡大

DOMは「スレッドセーフ」ではないため、WebワーカーがDOMにアクセスできないことに注意しましょう。 Webワーカーが実行するコードは別個のファイルに収容する必要があります。

33.「ホットパス」をWebAssemblyにオフロードできるか?

計算のコストが大きいタスクをWebAssembly(WASM)にオフロードすることができます。 WebAssembyはバイナリの命令形式で、C/C++/Rustなどの高級言語のコンパイルのポータブルターゲットとして設計されています。 WebAssemblyはブラウザのサポートが充実しており、 最近はJavaScriptとWASMの間の関数呼び出しが高速化していることで実用的な手法となっています。 さらに、Fastlyのエッジクラウドでもサポートされています

もちろん、WebAssemblyはJavaScriptの代わりになるとは考えられていませんが、 CPUに大きい負荷がかかっている場合において、JavaScriptを補完することができます。 ほとんどのWebアプリケーションにとってはJavaScriptのほうが適しており、 WebAssemblyはゲームなどの計算の負荷が大きいWebアプリケーションにとって最も有用です。

WebAssemblyについて詳しく知りたい人は、以下が参考になります。

  • Lin ClarkはWebAssemblyに関する詳しいシリーズ記事を執筆しています。Milica Mihajlijaは、ブラウザでネイティブコードを実行する方法の概要、そのメリット、それがJavaScriptと未来のWeb開発にとって持つ意味について解説しています。
  • How We Used WebAssembly To Speed Up Our Web App By 20X (Case Study)という記事では、低速なJavaScriptの計算をコンパイル済みのWebAssemblyに置き換えることで、パフォーマンスが大幅に改善されたというケーススタディに焦点を当てています。
  • Patrick HamannはWebAssemblyの役割の拡大について語っています。彼はWebAssemblyに関する誤りを示し、WebAssemblyの課題について追求することで、WebAssemblyが現在のアプリケーションで実際に使用できることを明らかにしています。
  • Google CodelabsはIntroduction to WebAssemblyという60分の講義を提供しています。この講義では、C言語のネイティブコードをWebAssemblyにコンパイルし、JavaScriptから直接呼び出す方法を学習できます。
  • Alex Daniloは、Google I/Oでの講演で、WebAssemblyとその仕組みについて説明しています。Benedek GagyiもWebAssemblyに関する実用的なケーススタディを紹介しています。具体的な内容は、WebAssemblyをiOS、Android、Webサイト向けのC++コードベースの出力形式として使用する方法に関するものです。

Webワーカー、Web Assembly、ストリーム、あるいはGPUにアクセスするためのWebGL JavaScript APIを使用するタイミングに関して、 まだ自信が持てない方もいるかもしれません。そのような方にはAccelerating JavaScriptがお勧めです。 このガイドは、短いながらも便利な内容で、いつ、何を、なぜ使用すべきかについて説明しています。 さらに、使いやすいフローチャートや、数多くの有用なリソースも提供されています。

Milica MihajlijaはWebAssemblyの仕組みとそれが便利である理由の概要を説明しています。(プレビューを拡大

34. 古いコードを古いブラウザのみに提供しているか?

ES2017はモダンなブラウザによるサポートがとても充実しているため、 babelEsmPluginを使用するのは、 ターゲットとするモダンなブラウザがサポートしていないES2017+の機能をトランスパイルする場合に限定することができます。 Houssein DjirdehとJason Millerは最近、モダンなJavaScriptと古いJavaScriptのトランスパイルと提供方法についての包括的なガイドを公表しました。 このガイドでは、WebpackとRollupを活用した実現方法や、必要なツールについて詳しく紹介しています。 サイトやアプリケーションのバンドルのJavaScriptをどれだけ削減できるかを推定することも可能です。

JavaScriptモジュールは全ての主要なブラウザでサポートされているため、 ESモジュールをサポートしているブラウザにファイルを読み込ませるためにscript type="module"を使用することが可能です。 一方、古いブラウザにはscript nomoduleで古いビルドを読み込ませることができます。

最近では、トランスパイラやバンドラがなくても、ブラウザでネイティブに実行できるモジュールベースのJavaScriptを書けるようになりました。 <link rel="modulepreload">ヘッダは、 モジュールスクリプトの読み込みを早い段階で(優先的に)開始するための手段を提供します。 基本的に、ブラウザが取得する必要があるものを指示し、長いラウンドトリップの間に他の作業に邪魔されないようにすることは、 帯域幅を最大限に活用するための効果的な方法の1つです。 また、Jake Archibaldが公表しているESモジュールの落とし穴と注意点に関する詳しい記事も一読に値します。

Jake Archibaldは、ESモジュールの落とし穴と注意点(例:インラインスクリプトは、ブロックとなる外部スクリプトとインラインスクリプトが実行されるまで遅延するなど)に関する詳しい記事を公表しています。(プレビューを拡大

35. 段階的なデカップリングによって古いコードを特定し、書き換える。

長期のプロジェクトでは、ほこりをかぶった時代遅れのコードが蓄積する傾向があります。 依存関係を見直し、最近の問題の原因となっている古いコードのリファクタリングや書き換えのためにどれだけの時間が必要かを検討しましょう。 もちろん、これはいつでも大変な仕事ですが、古いコードの影響を把握すれば、 段階的なデカップリングを始めることができます。

まず、古いコードの呼び出しの比率が一定か減少しており、増加していないことを確認するための指標を設定します。 また、チームに対してライブラリを使用しないようにと明確に伝え、 プルリクエストでライブラリが使用された場合はCIが開発者にアラートを通知するようにしましょう。 ポリフィルは、 古いコードから、ブラウザの標準的な機能を使用する書き換え後のコードベースへの移行に役立つでしょう。

36. 使用していないCSSやJSを特定・削除する。

ChromeのCSSとJavaScriptのコードカバレッジを使用すると、 どのコードが実行・適用され、どのコードがそうではないのかを洗い出すことができます。カバレッジの記録を開始し、 ページにおけるアクションを実行し、コードカバレッジの結果を調査します。使用していないコードを発見したら、 そのモジュールを見つけ出し、import()で遅延読み込みするようにしましょう(詳しくはスレッド全体を参照してください)。 さらに、同じプロファイルでカバレッジを繰り返し、初期読み込み時のコードが少なくなったことを確認しましょう。

Puppeteerを使用すれば、コードカバレッジの収集をプログラム化できます。 Chromeでもコードカバレッジの結果をエクスポートすることが可能です。 ただし、Andy Daviesが述べているとおり、モダンなブラウザと古いブラウザの両方でコードカバレッジを収集したい場合もあるかもしれません。

Puppeteerには他にも、もっと知られるべき多くの使用方法とツールがあります。

Puppeteer RecorderとPuppeteer Sandboxを使用することで、ブラウザのインタラクションを記録し、PuppeteerとPlaywrightのスクリプトを生成できます。(プレビューを拡大

さらに、purgecssUnCSSHeliumは、 使用されていないスタイルをCSSから削除するのに役立ちます。不審なコードがどこかで使用されているかどうかが分からない場合は、 Harry Robertsのアドバイスに従うと良いでしょう。 すなわち、特定のクラス向けに1×1ピクセルの透明なGIFを作成し、dead/ディレクトリにドロップするのです(例えば/assets/img/dead/comments.gif)。

その後、CSSの対応するセレクタの背景としてその画像を設定し、数カ月待って、ファイルがログに出現するかを調べます。 エントリがなければ、その古いコンポーネントを誰も画面にレンダリングしていないことになるため、コンポーネントを完全に削除できるでしょう。 思い切ったことがしたいという方は、DevToolsを使用してDevToolsをモニタリングし、 複数のページを通して、使用されていないCSSの収集を自動化することもできます。

Benedikt Rötschの記事は、Moment.jsからdate-fnsへの移行によって、3G接続のローエンドのスマートフォンで最初の描画までの時間を約300ミリ秒削減できる可能性があることを明らかにしました。(プレビューを拡大)

37. JavaScriptバンドルの容量を削減する。

Addy Osmaniが述べているとおり、 JavaScriptライブラリの一部しか必要ではないにもかかわらず、ライブラリ全体を提供していることはよくあります。 また、ブラウザにとって不要な時代遅れのポリフィルや、単に重複したコードを提供してしまうケースもあります。 このようなオーバーヘッドを回避するために、webpack-libs-optimizationsを使用して、 ビルドのプロセスに不要なメソッドやポリフィルを削除することを検討してみてください。

古いブラウザとモダンなブラウザに送信しているポリフィルを確認・レビューし、より綿密な戦略を立てましょう。 polyfill.ioにも注目です。 このサービスは、一連のブラウザ機能のリクエストを受け付け、リクエストを実施したブラウザにとって必要なポリフィルのみを返します。

また、通常のワークフローにバンドル監査も追加しましょう。 何年も前に追加した重いライブラリに代わって、別の軽いライブラリを使用できるかもしれません。 例えば、Moment.js(現在は開発停止)は以下と置き換えられるでしょう。

Benedikt Rötschの調査は、 Moment.jsからdate-fnsへの移行によって、3G接続のローエンドのスマートフォンで最初の描画までの時間を約300ミリ秒削減できる可能性があることを明らかにしました。

バンドル監査については、バンドルにnpmパッケージを追加するコストの計算にBundlephobiaが役立つでしょう。 size-limitは、基本的なバンドル容量のチェックを拡張し、JavaScriptの実行時間を詳しく調べるものです。 これらのコストをLighthouse Custom Auditで統合することも可能です。 フレームワークとも相性が良い機能となっています。 Vue MDC Adapter(VueのMaterial Components)を削除・縮小することで、スタイルの容量は194KBから10KBに低下します。

他にも、依存関係の影響と実用的な選択肢について、十分な情報に基づいた意思決定に役立つ数多くのツールが存在します。

あるいは、フレームワーク全体を提供するために、フレームワークを縮小し、追加コードを必要としない生のJavaScriptバンドルにコンパイルするという手もあります。 SvelteRawact Babelプラグインならばそれが可能です。 Rawact Babelプラグインは、ビルド時にReact.jsコンポーネントをネイティブDOMオペレーションにトランスパイルします。 なぜでしょうか。 メンテナの説明によれば、「React DOMには、段階的レンダリング、スケジューリング、イベントハンドリングなど、レンダリングされる可能性がある全てのコンポーネントやHTML要素のコードが含まれます。 しかし、これら全ての機能を(初期のページ読み込み時には)必要としないアプリケーションもあります。 こうしたアプリケーションにとっては、インタラクティブなUIをビルドするのにネイティブDOMオペレーションを使用するのが合理的かもしれません。

size-limitは、基本的なバンドル容量のチェック機能とともに、JavaScriptの実行時間を詳しく調べる機能を提供します。(プレビューを拡大

38. パーシャルハイドレーションを使用するか?

アプリケーションで使用するJavaScriptの量が増えるにつれて、クライアントに送信する量を可能な限り小さくする手段を探す必要が出てきます。 その手段の1つが、この記事ですでに簡単に触れたパーシャルハイドレーションです。 考え方はとてもシンプルです。 サーバサイドレンダリング(SSR)を実施し、アプリケーション全体をクライアントに送る代わりに、 アプリケーションのJavaScriptのごく一部をクライアントに送り、ハイドレーションするのです。 これは、複数のレンダリングのルートを持つ、複数のとても小規模なReactアプリケーションが、そのアプリケーション以外は静的なWebサイトで機能していると考えることができます。

Lukas Bombachは、"The case of partial hydration (with Next and Preact)"という記事で、Welt.de(ドイツのニュースサイトの1つ)のチームが、パーシャルハイドレーションによってどのようにパフォーマンスを改善したかを説明しています。next-super-performanceというGitHubリポジトリにも説明とコードスニペットが掲載されています。

別の選択肢として、以下も検討できます。

Jason Millerは、プログレッシブハイドレーションをReactで実装する方法についての実用的なデモを公表しており、 デモ1デモ2デモ3をすぐに使用できます (GitHubでも利用可能)。 react-prerendered-componentライブラリもチェックしてみましょう。

ファーストパーティのコード向けのImport On Interactionは、インタラクションの前にリソースを先読みできない場合にのみ実行すべきです。(プレビューを拡大

39. ReactやSPA向けの戦略を最適化しているか?

シングルページアプリケーションのパフォーマンスに関して、困っていることはありませんか。 Jeremy Wagnerは、さまざまな端末に対するクライアントサイドのフレームワークのパフォーマンスの影響を調査し、 それが意味することや、フレームワークを使用するときに気を付けたいガイドラインに焦点を当てています。 調査を踏まえてJeremyがReactフレームワーク向けに提案するSPA戦略は以下となります (ただし、他のフレームワークでも大きくは変わらないでしょう)。

  • 可能な場合は常に、ステートフルなコンポーネントをステートレスなコンポーネントにリファクタリングします。
  • サーバの応答時間を最小限にするため、可能な場合はステートレスなコンポーネントをプリレンダリングします。サーバのみでレンダリングを行います。
  • シンプルなインタラクティブ性があるステートフルなコンポーネントについては、そのコンポーネントのプリレンダリングやサーバサイドレンダリングを検討し、インタラクティブ性をフレームワークに依存しないイベントリスナに置き換えます。
  • ステートフルなコンポーネントをクライアント側でハイドレートする必要がある場合、表示やインタラクション時の遅延ハイドレーションを使用しましょう。
  • 遅延ハイドレーションを行うコンポーネントについては、requestIdleCallbackを使用して、メインスレッドのアイドルタイムの間にハイドレーションをスケジューリングします。

他にも、調査やレビューに値するかもしれない戦略はいくつか存在します。

40. JavaScriptチャンクの予測的な先読みを使用しているか?

JavaScriptチャンクを先読みするタイミングの決定には、経験則を利用することができます。 Guess.jsは、Google Analyticsのデータを利用して、 ユーザがあるページの次にアクセスする可能性が最も高いページを判断する一連のツールとライブラリです。 Google Analyticsなどのソースから収集したユーザの操作パターンに基づき、Guess.jsは機械学習モデルを構築し、 それぞれの後続ページで必要とされるJavaScriptの予測・先読みを行います。

したがって、あらゆるインタラクティブな要素はエンゲージメントの確率スコアを付与され、そのスコアに基づいて、クライアントサイドのスクリプトが事前のリソースの先読みを決定します。 このテクニックはNext.jsアプリケーションAngular、Reactに統合できます。 設定プロセスを自動化するWebpackプラグインも存在します。

もちろん、ブラウザに不必要なデータを処理させたり、望ましくないページを先読みさせたりしてしまうこともあるでしょう。 ですから、先読みするリクエストの数をかなり保守的に設定するのは良いアイデアです。 優れたユースケースとしては、チェックアウトに必要な検証用スクリプトを先読みすることや、 クリティカルなコール・トゥ・アクション(CTA)がビューポートに表示されたときに投機的先読みを行うことが挙げられます。

もっと素朴なツールが必要な方には、DNStradamusが役立ちます。 これはビューポートに表示されるアウトバウンドリンクのDNSの先読みを行うものです。 Quicklink、InstantClick、Instant.pageという小規模なライブラリは、 次のページのナビゲーションの読み込みを高速化するために、アイドルタイムの間にビューポートのリンクを自動的に先読みします。 QuicklinkはReact RouterのルートとJavaScriptの先読みを可能にします。 さらに、データ通信の容量を考慮するため、2GやData-Saverがオンの場合は先読みを行いません。 Instant.pageも、モードがビューポート先読み(デフォルト)に設定されている場合は同様に動作します。

予測的な先読みの仕組みを詳しく知りたい場合は、 Divya TagtachianのThe Art of Predictive Prefetchという素晴らしい講演がお勧めです。 全ての選択肢が初めから終わりまで紹介されています。

41. ターゲットとするJavaScriptエンジンの最適化を利用する。

自社サイトのユーザ層で多数を占めるJavaScriptエンジンを調査し、そのエンジン向けに最適化する方法を探求しましょう。 例えば、Blinkブラウザ、Node.jsランタイム、Electronで使用されるV8向けに最適化するときは、 モノリシックなスクリプト向けのスクリプトストリーミングを利用します。

スクリプトストリーミングは、ダウンロードが始まった時点で、asyncdefer scriptsを別のバックグラウンドスレッドでパースすることを可能にします。 これにより、場合によってはページの読み込み時間を最大で10%改善できます。 実務では、<head>内で<script defer>を使用することにより、 ブラウザがリソースを早く発見し、バックグラウンドスレッドでリソースをパースできるようにしましょう。

注意点Opera Miniはスクリプト遅延をサポートしていません。 そのため、インドやアフリカ向けに開発をしている場合、deferは無視され、スクリプトの評価が終わるまでレンダリングをブロックすることとなります(Jeremyに感謝します)。

ライブラリとそれを利用するコードを分離して、V8のコードキャッシングを使用することもできます。 あるいは逆に、ライブラリとその用途を1つのスクリプトにマージし、小さいファイルを一緒にグループ化し、インラインスクリプトを回避することも可能です。 v8-compile-cacheを使用するのも良いかもしれません。

JavaScript全般に関しては、他にも覚えておくべき実務上のポイントがあります。

新たなブラウザAPIであるisInputPending()は読み込みと応答のギャップを埋めようとするものです。(プレビューを拡大

CNN.comのリクエストマップは、各リクエストから異なるドメインへのチェーンを示しており、最大で8次のパーティのスクリプトまで表示されています。出典はこちら。(プレビューを拡大

42. いつでもサードパーティアセットはなるべくセルフホストする。

繰り返しになりますが、静的アセットはセルフホストすることをデフォルトにしましょう。 多くのサイトが同じパブリックCDNと同一バージョンのJavaScriptライブラリやWebフォントを使用していれば、 訪問者がサイトにアクセスした時点でブラウザにスクリプトとフォントがキャッシュされており、ユーザ体験が大幅に高速化されると想定するのはよくあることです。 しかし、それが実現する可能性はとても低いと言えます。

セキュリティー上の理由で、フィンガープリントを避けるために、 ブラウザはパーティション化されたキャッシングを実装しています。 2013年にはSafari、2020年にはChromeが導入しました。 2つのサイトがまったく同じサードパーティのリソースのURLを指定していた場合、コードは1ドメインにつき1回ダウンロードされます。 また、キャッシュはプライバシーへの影響を考慮して、そのドメインに「サンドボックス化」されています(Calhounに感謝します)。 そのため、パブリックCDNを使用しても、自動的にパフォーマンスの改善につながるわけではありません

さらに、注目すべき点として、 リソースは私たちが予想するほど長くはブラウザのキャッシュに残らず、 サードパーティよりもファーストパーティのアセットのほうがキャッシュにとどまりやすい傾向があります。 そのため、通常はセルフホストのほうが信頼性と安全性が高く、パフォーマンスも向上します。

43. サードパーティのスクリプトの影響を抑える。

あらゆるパフォーマンスの最適化を導入しても、業務上必要なサードパーティのスクリプトを制御できないことはよくあります。 サードパーティのスクリプトに関する指標はエンドユーザの体験に影響されないため、1つのスクリプトが邪魔な長いサードパーティのスクリプトを呼び出し、 パフォーマンス向上のための懸命な努力を台無しにしてしまうケースはとても多いのです。 こうしたスクリプトがもたらすパフォーマンス上のデメリットを抑制・軽減するには、読み込みと実行を遅延させることや、 リソースヒント(dns-prefetchpreconnect)を通じて接続をウオームアップするだけでは不十分です。

現在、JavaScriptコードの実行時間全体の57%はサードパーティのコードに費やされています。 平均的なモバイルサイトは12個のサードパーティドメインにアクセスし、平均で37個の異なるリクエスト(各サードパーティにつき約3個のリクエスト)を送信しています。

さらに、これらのサードパーティは4次のパーティのスクリプトを呼び込むことが多く、最終的にパフォーマンス上のボトルネックが非常に大きくなり、 場合によっては最大で8次のパーティのスクリプトがページに呼び込まれることとなります。 そのため、依存関係とタグマネジャーを定期的に監査すると、思わぬ大きなコストがかかるかもしれません。

他の問題として、Yoav Weissは、サードパーティのスクリプトに関する講演で、 こうしたスクリプトが動的なリソースをダウンロードするケースが多いことを挙げています。リソースはページの読み込みの間に変化するため、 どのホストからリソースがダウンロードされるか、また、そのリソースがどのようなものかが分かるとは限りません。

上記のとおり、遅延は単なる出発点に過ぎないかもしれません。サードパーティのスクリプトはアプリケーションから帯域幅とCPU時間も奪うからです。 もっと積極的に、アプリケーションが初期化された場合にのみサードパーティのスクリプトを読み込むことにしても良いでしょう。

/* Before */
const App = () => {
  return <div>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){...}
      gtg('js', new Date());
    </script>
  </div>
}

/* After */
const App = () => {
  const[isRendered, setRendered] = useState(false);

  useEffect(() => setRendered(true));

  return <div>
  {isRendered ?
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){...}
      gtg('js', new Date());
    </script>
  : null}
  </div>
}

Andy Daviesは、"Reducing the Site-Speed Impact of Third-Party Tags"という素晴らしい記事で、 コストの特定から影響の軽減まで、サードパーティのフットプリントを最小化する戦略を追求しています。

Andyによれば、タグがサイトの速度に影響を与える要因は2通りです。 タグは訪問者の端末のネットワークの帯域幅と処理時間を奪い合い、実装方法によってはHTMLパーシングを遅らせる場合もあります。 そのため、最初のステップは、WebPageTestを使用して、スクリプトがある状態とない状態のサイトをテストし、サードパーティが及ぼす影響を特定することとなります。 Simon Hearneのリクエストマップでは、ページ上のサードパーティと、その容量、種類、読み込みが発生した原因を視覚化することもできます。

セルフホストと単一のホスト名を使用することが理想的ですが、 リクエストマップを使用して4次の呼び出しを明らかにし、 スクリプトの変更を検知することも可能です。Harry Robertsのサードパーティ監査のアプローチを使用し、 このようなスプレッドシートを作成することもできます (Harryの監査ワークフローもチェックしましょう)。

その後、既存のスクリプトの軽量な代わりを探し、重複やパフォーマンス低下の主な要因をそれらよりも軽い選択肢に少しずつ置き換えていきます。 場合によっては、一部のスクリプトは完全なタグの代わりにフォールバックのトラッキングピクセルに置き換えられるかもしれません。

lite-youtube-embedなどのファサードを利用したYouTubeの読み込み。実際のYouTubeプレーヤよりも容量が大幅に小さくなります。(画像の出典)(プレビューを拡大

これが不可能であっても、少なくともサードパーティのリソースをファサードで遅延読み込みすることはできます。 ファサードとは、実際に埋め込まれたサードパーティと似ているが、現実には機能せず、そのためページ読み込みの負荷が大幅に軽い静的要素を指します。 ここでのポイントは、インタラクション時にのみ、実際に埋め込まれた要素を読み込むことです。

例えば、以下を使用することができます。

一般にタグマネジャーの容量が大きい理由の1つは、多数の実験が同時並行的に、多くのユーザセグメント、ページURL、サイトに関して同一のタイミングで実行されるためです。 ですからAndyによれば、実験を減らすことで、ダウンロード容量と、ブラウザでスクリプトを実行する時間の両方を削減できます。

さらにanti-flickerスニペットもあります。 Google Optimize、Visual Web Optimizer(VWO)などのサードパーティは、いずれもこのスニペットを使用しています。 anti-flickerスニペットは通常、A/Bテストを実行する際に、異なるテストシナリオ間のちらつきを回避するために挿入されます。 このスニペットはドキュメントのbodyopacity: 0によって隠し、その数秒後に関数を呼び出してopacityを元に戻すものです。 これによりクライアントサイドの実行コストが膨大になるため、レンダリングが大幅に遅れることが多くなります。

A/Bテストを使用すると、顧客は図のようなちらつきを目にするケースが増えます。Anti-Flickerスニペットはこれを防ぎますが、パフォーマンスの面でコストがかかります。Andy Daviesより。(プレビューを拡大

そのため、ちらつきを防ぐためのタイムアウトがどれだけ発生しているかを調査し、タイムアウトを減らすことが重要です。 デフォルトではページの表示が最大で4秒ブロックされるため、コンバージョンレートは低下するでしょう。 Tim Kadlecは、「友人がクライアントサイドのA/Bテストを実行しようとしていたら止めるべきだ」と語っています。 CDN上のサーバサイドA/Bテスト(Edge ComputingやEdge Slice Rerenderingなど)のパフォーマンスは、クライアントサイドよりも常に高いと言えます。

「全能」のGoogle Tag Managerを使用する場合は、 Barry PollardのGoogle Tag Managerの影響を抑えるためのガイドラインをチェックしましょう。 また、Christian Schaeferは広告の読み込み戦略について追求しています

注意すべき点として、一部のサードパーティウィジェットは監査ツールから身を隠すため、 発見・測定が比較的難しいかもしれません。サードパーティのストレステストを実施するには、 DevToolsのPerformanceプロファイルページのボトムアップサマリーを調べ、リクエストがブロックされた場合やタイムアウトした場合に何が起きるかをテストしましょう。 後者については、hostsファイル内において、特定のドメインへのリクエストをWebPageTestのBlackholeサーバ(blackhole.webpagetest.org)に送信するように設定すれば良いでしょう。

選択肢の1つとして、サービスワーカーを使用し、タイムアウト付きのリソースのダウンロードを高速化することを検討してみましょう。 リソースが特定のタイムアウトまでに応答しない場合、空の応答を返し、ブラウザにページのパースを続けさせます。 うまく機能していないサードパーティのリクエストや、特定の基準を満たさないサードパーティのリクエストは、記録やブロックをすることができます。 可能であれば、サードパーティのスクリプトをベンダのサーバから遅延読み込みするのではなく、 自分のサーバから読み込むようにしましょう。

他の選択肢として、サードパーティのスクリプトの影響を制限するためのコンテンツセキュリティーポリシー(CSP)(音声や動画のダウンロードを許可しないなど)を定めることが挙げられます。 最善の選択肢は、<iframe>を通じてスクリプトを埋め込み、iframeのコンテキスト内でスクリプトを実行することで、 スクリプトがページのDOMにアクセスできず、ドメイン上の任意コード実行もできないようにすることです。 iframeは、sandbox属性を使用して制約を強化することもできます。 これにより、例えばスクリプトの実行、アラート、フォームの提出、プラグイン、トップナビゲーションへのアクセスなど、iframeが実行する可能性がある任意の機能を無効化することが可能です。

Feature Policyによるブラウザ内のパフォーマンスリントを通じたサードパーティのコントロールもできます。 これは比較的新しい機能で、サイト上で特定のブラウザ機能のオプトインやオプトアウトを選択することが可能です。 (ちなみに、寸法が大き過ぎる画像や最適化されていない画像、寸法が適切でないメディア、同期型のスクリプトなどを回避するためにも使用できます)。 現在はBlinkベースのブラウザでサポートされています。

/* Via Tim Kadlec. https://timkadlec.com/remembers/2020-02-20-in-browser-performance-linting-with-feature-policies/ */
/* Block the use of the Geolocation API with a Feature-Policy header. */
Feature-Policy: geolocation 'none'

多くのサードパーティのスクリプトはiframeで実行されるため、権限を徹底的に制限する必要があるでしょう。 iframeをサンドボックス化するのは常に良いアイデアと言えます。 それぞれの制約はsandbox属性の複数のallow値を通じて解除できます。サンドボックスはほとんどの場所でサポートされています。 ですから、サードパーティのスクリプトの権限を、許可すべき必要最低限まで制限しましょう。

ThirdPartyWeb.Todayは、全てのサードパーティのスクリプトをカテゴリ(アナリティクス、ソーシャル、広告、ホスティング、タグマネジャーなど)ごとにグループ化し、エンティティのスクリプトの実行までにどの程度の(平均)時間がかかるかを視覚化します。(プレビューを拡大

インターセクションオブザーバーの使用を検討しましょう。 これは、イベントをディスパッチしたり、DOMから必要な情報(広告の表示と非表示など)を取得したりしつつ、広告にiframeを適用することを可能にします。 ブラウザを減速させる有害なWeb機能やスクリプト(同期スクリプト、同期XHRリクエスト、document.write、時代遅れの実装など)を制限するために、 Feature policyなどの新たなポリシー、リソース容量の制限、CPU/帯域幅の優先順位に注目しましょう。

最後に、サードパーティのサービスを選ぶときは、Patrick HulceのThirdPartyWeb.Todayをチェックすることを検討してください。 このサービスは、全てのサードパーティのスクリプトをカテゴリ(アナリティクス、ソーシャル、広告、ホスティング、タグマネジャーなど)ごとにグループ化し、 各エンティティのスクリプトの実行までにどの程度の(平均)時間がかかるかを視覚化します。 もちろん、最大のエンティティがページのパフォーマンスに最も大きな悪影響を与えていることとなります。 ページをざっと読み取るだけで、想定すべきパフォーマンスのフットプリントの概要が分かります。

それと、パフォーマンスを減速させる「おなじみのメンバー」も忘れないでください。 シェア用のサードパーティウィジェットの代わりに静的なソーシャルシェアボタンSSBGによるものなど)を使用したり、インタラクティブマップの代わりに そのマップへの静的なリンクを設置したりすることができます。

Casper.comは、Optimizelyをセルフホストすることによって、サイトのレンダリング開始時間を1.7秒削減した事例の詳細なケーススタディを公表しています。これは労力に値する結果と言えるでしょう。(画像の出典)(プレビューを拡大

44. HTTPキャッシュヘッダを適切に設定する。

キャッシングをするのは当たり前に思えますが、適切なキャッシングは難しいかもしれません。expiresmax-agecache-controlなどのHTTPキャッシュヘッダが適切に設定されていることをダブルチェックする必要があります。適切なHTTPキャッシュヘッダが存在しない場合、ブラウザはこれらをlast-modified以降の経過時間の10%に自動的に設定するため、キャッシングの期間が短すぎたり、長すぎたりする可能性があります。

一般に、リソースのキャッシュ期間はとても短くする(変更される可能性が高い場合)か、無期限(静的である場合)にするべきです。必要な場合は、単にURLでバージョンを変更することもできます。この戦略は永久キャッシュ戦略と呼べます。これは、アセットを1年間のみ保持するために、Cache-ControlExpiresヘッダをブラウザに中継するものです。そのため、ブラウザは、アセットがキャッシュに存在すれば、リクエストを作成する必要さえありません。 例外はAPIの応答(例えば/api/user)です。キャッシングを防ぐにはprivate, no-storeを使用しましょう。max-age=0, no-storeではないことに注意してください

Cache-Control: private, no-store

ユーザが再読み込みボタンを押したときに、明示的な長期のキャッシュ存続期間の再検証を避けるためには、Cache-control: immutableを使用しましょう。再読み込みをする場合、immutableによってHTTPリクエストを省略できます。多数の304応答と競合しなくなるため、動的HTMLの読み込み時間を改善することが可能です。

immutableの使用に適した典型的な例は、その名称にハッシュが含まれているCSS/JavaScriptアセットです。これらは可能な限り長くキャッシュし、二度と再検証されないようにしたい場合が多いでしょう。 Cache-Control: max-age= 31556952, immutable Colin Bendellの調査によれば、immutable304リダイレクトを約50%削減します。これはmax-ageを使用していても、依然として更新時にクライアント側で再検証とブロックが行われるためです。immutableはFirefox、Edge、Safariでサポートされており、Chromeでは対応を検討中です

Web Almanacによれば、「使用率は3.5%まで上昇し、FacebookとGoogleへのサードパーティ応答に広く使われています」。

あなたは昔ながらのstale-while-revalidateを覚えているでしょうか。Cache-Control応答ヘッダ(例:Cache-Control: max-age=604800)でキャッシュ時間を指定した場合、max-ageが期限を迎えた後、ブラウザはリクエストされたコンテンツを再取得するため、ページの読み込みが遅くなります。こうした減速はstale-while-revalidateによって回避できます。これは基本的に、追加的な時間枠を定め、その間はキャッシュが(バックグラウンドにおいて非同期で再検証を行うという条件で)古いアセットを使用できるようにするものです。このように、レイテンシを(ネットワークとサーバの両方で)クライアントから「隠す」のです。

2019年6~7月、ChromeとFirefoxはHTTP Cache-Controlヘッダにおけるstale-while-revalidateのサポートを開始しました。これにより、古いアセットはクリティカルパスに存在しなくなるため、次のページを読み込む際のレイテンシが改善する見込みでした。結果として、繰り返し表示した場合のRTTは0となりました。

Varyヘッダは、特にCDNとの関連で警戒が必要です。HTTP Representation Variantsにも注目しましょう。これは、新たなリクエストが過去のリクエストと(大きくは違わないが)少し違うときに、検証のための追加的なラウンドトリップを回避するのに役立ちます(GuyとMarkに感謝します)。

また、不要なヘッダx-powered-bypragmax-ua-compatibleexpiresX-XSS-Protectionなど)を送信していないことや、便利なセキュリティヘッダやパフォーマンスヘッダContent-Security-PolicyX-Content-Type-Optionsなど)が含まれていることをダブルチェックしましょう。最後に、シングルページアプリケーションにおけるCORSリクエストのパフォーマンスコストに気をつけましょう。

注記:キャッシュしたアセットはすぐに取得できると考えがちですが、調査によれば、オブジェクトをキャッシュから取得するには数百ミリ秒かかることがあります。それどころか、Simon Hearneによれば、「場合によってはネットワークのほうがキャッシュより速いかもしれません。キャッシュされたアセットの数(ファイルの容量ではない)が大量である場合、ユーザの端末によっては、キャッシュからアセットを取得するのには大きなコストがかかる可能性があります。例えば、Chrome OSの平均的なキャッシュ取得時間は、キャッシュされたリソースが5個である場合は約50ミリ秒で、リソースが25個の場合は約100ミリ秒に倍増します」。

さらに、バンドルの容量は大きな問題ではないと考えられがちですが、ユーザはバンドルを一度にダウンロードしてからキャッシュされたバージョンを使用します。また、CI/CDではコードを本番環境に毎日数回プッシュし、キャッシュが毎回無効化されるため、キャッシングについて戦略を立てるのは重要です。

キャッシングに関しては、一読に値するリソースが豊富に存在します。

ブラウザはキャッシュしたアセットをすぐに取得できると考えられがちですが、データによれば、オブジェクトをキャッシュから取得するには数百ミリ秒かかることがあります。Simon HearneによるWhen Network Is Faster Than Cacheの調査より。(プレビューを拡大) <後編に続く

※編注:本記事は2021年4月時点に公開されていた原文記事を翻訳した内容となります。翻訳記事公開にあたり、2023年6月時点で原文にてリンク切れとなっている箇所の削除や注記、原文に現在表示されていない内容を掲載している場合がございます。ご了承ください。