Client Hints を使用したリソース選択の自動化

Ilya Grigorik

ウェブ向けの構築は、圧倒的なリーチをもたらします。ウェブ アプリケーションは、クリックするだけで、ブランドやプラットフォームに関係なく、スマートフォン、タブレット、ノートパソコン、デスクトップ パソコン、テレビなど、接続されているほぼすべてのデバイスで利用できます。最適なエクスペリエンスを実現するために、フォーム ファクタごとに表示と機能を調整するレスポンシブ サイトを構築しました。次に、アプリが可能な限り迅速に読み込まれるようにパフォーマンス チェックリストを実行しました。重要なレンダリング パスを最適化し、テキスト リソースを圧縮してキャッシュに保存しました。これで、アカウントの主な転送バイト数を確認しています。問題は、画像の最適化が難しいことです。

  • 適切な形式を決定する(ベクターかラスターか)
  • 最適なエンコード形式(jpeg、webp など)を決定する
  • 適切な圧縮設定を決定する(非可逆圧縮と可逆圧縮の切り替え)
  • 保持または削除するメタデータを決定する
  • ディスプレイごとに複数のバリエーションを作成し、DPR 解像度を設定する
  • ...
  • ユーザーのネットワークの種類、速度、設定を考慮

個々の問題として、これらは十分に理解されている問題です。これらが一体となって大きな最適化空間を生み出します。この領域はデベロッパーにとって見過ごされがちです。特に多くのステップが関係する場合、人間は同じ検索空間を繰り返し探索する作業はうまくいきません。一方、コンピュータはこの種のタスクに優れています。

類似の特性を持つ画像やその他のリソースに対する適切で持続可能な最適化戦略に対する答えは自動化です。リソースを手作業でチューニングしているのも間違いです。忘れたり、怠ったりすると、誰かが間違いを犯してしまいます。

パフォーマンス志向のデベロッパーのサガ

画像最適化空間の検索には、ビルド時と実行時の 2 つの異なるフェーズがあります。

  • 一部の最適化はリソース自体に固有のものです。たとえば、適切な形式とエンコード タイプの選択、各エンコーダの圧縮設定の調整、不要なメタデータの削除などがあります。これらの手順は「ビルド時」に実行できます。
  • その他の最適化は、それをリクエストしているクライアントのタイプとプロパティに応じて決定され、「ランタイム」で実行する必要があります。クライアントの DPR と目的のディスプレイ幅に合わせて適切なリソースを選択し、クライアントのネットワーク速度、ユーザーとアプリケーションの設定などを考慮します。

ビルド時のツールは存在しますが、改善の余地があります。たとえば、各画像と各画像形式の「品質」設定を動的に調整することでコストを大幅に削減できますが、これを研究以外で実際に使用する人はまだ見つかっていません。これはイノベーションの見込みが高い領域ですが、この投稿では そのままにしておきます本編の実行時の部分を見ていきましょう。

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

アプリのインテントは非常にシンプルです。画像を取得してユーザーのビューポートの 50% に表示します。ほとんどのデザイナーはここで手を洗い、一方、パフォーマンスを重視するチームの開発者は、作業が終わっていません。

  1. 最適な圧縮率を得るために、Chrome では WebP、Edge では JPEG XR、それ以外は JPEG など、クライアントごとに最適な画像形式を使用したいと考えています。
  2. 最高の画質を得るには、各画像の解像度を 1 倍、1.5 倍、2 倍、2.5 倍、3 倍に、あるいはその中間など、さまざまな解像度で複数パターンを生成する必要があります。
  3. 不要なピクセルの配信を避けるために、「ユーザーのビューポートの 50% が実際に何を意味するか」を理解する必要があります。ビューポートの幅は多種多様です。
  4. また、低速のネットワークを使用するユーザーが自動的に低い解像度を自動的に取得できる、復元性に優れたエクスペリエンスを実現することも理想的です。結局のところがガラスのときです。
  5. このアプリケーションは、どの画像リソースを取得するかに影響を与えるユーザー コントロールも公開しているので、これも考慮する必要があります。

するとデザイナーは、読みやすさを最適化するためビューポートのサイズが小さい場合、別の画像を 100% の幅で表示する必要があることに気付きます。つまり、もう 1 つのアセットに対して同じプロセスを繰り返してから、ビューポート サイズでフェッチを条件にする必要があります。ここまでは難しいと言いましたが、では取りかかりましょうpicture 要素を使用すると、かなり効率的になります。

<picture>
    <!-- serve WebP to Chrome and Opera -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.webp 200w, /image/thing-400.webp 400w,
        /image/thing-800.webp 800w, /image/thing-1200.webp 1200w,
        /image/thing-1600.webp 1600w, /image/thing-2000.webp 2000w"
    type="image/webp">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.webp 200w, /image/thing-crop-400.webp 400w,
        /image/thing-crop-800.webp 800w, /image/thing-crop-1200.webp 1200w,
        /image/thing-crop-1600.webp 1600w, /image/thing-crop-2000.webp 2000w"
    type="image/webp">
    <!-- serve JPEGXR to Edge -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpgxr 200w, /image/thing-400.jpgxr 400w,
        /image/thing-800.jpgxr 800w, /image/thing-1200.jpgxr 1200w,
        /image/thing-1600.jpgxr 1600w, /image/thing-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpgxr 200w, /image/thing-crop-400.jpgxr 400w,
        /image/thing-crop-800.jpgxr 800w, /image/thing-crop-1200.jpgxr 1200w,
        /image/thing-crop-1600.jpgxr 1600w, /image/thing-crop-2000.jpgxr 2000w"
    type="image/vnd.ms-photo">
    <!-- serve JPEG to others -->
    <source
    media="(min-width: 50em)"
    sizes="50vw"
    srcset="/image/thing-200.jpg 200w, /image/thing-400.jpg 400w,
        /image/thing-800.jpg 800w, /image/thing-1200.jpg 1200w,
        /image/thing-1600.jpg 1600w, /image/thing-2000.jpg 2000w">
    <source
    sizes="(min-width: 30em) 100vw"
    srcset="/image/thing-crop-200.jpg 200w, /image/thing-crop-400.jpg 400w,
        /image/thing-crop-800.jpg 800w, /image/thing-crop-1200.jpg 1200w,
        /image/thing-crop-1600.jpg 1600w, /image/thing-crop-2000.jpg 2000w">
    <!-- fallback for browsers that don't support picture -->
    <img src="/image/thing.jpg" width="50%">
</picture>

アート ディレクションとフォーマットの選択を処理し、DPR とクライアント デバイスのビューポートの幅のばらつきに対応するために、各画像の 6 つのバリエーションを用意しました。すごい!

残念ながら、picture 要素には、クライアントの接続タイプや速度に基づいて動作させるルールを定義することはできません。ただし、その処理アルゴリズムでは、フェッチするリソースをユーザー エージェントで調整できる場合があります(ステップ 5 を参照)。ユーザー エージェントが十分に頭がいいことだけを願うだけです。(注: 現在の実装には対応していません)。同様に、picture 要素には、アプリやユーザー設定を考慮したアプリ固有のロジックを可能にするフックはありません。最後の 2 つのビットを取得するには、上記のロジックをすべて JavaScript に移動する必要がありますが、picture によるプリロード スキャナ最適化は無効になります。承知しました。

こうした制限は除いて、適切に機能します。少なくともこのアセットではここでの現実的で長期的な課題は、デザイナーやデベロッパーがすべてのアセットに対してこのようなコードを手作りすることは想定できないということです。最初のやり方は面白い脳パズルですが、直後には魅力的ではなくなります。自動化が必要です。おそらく、IDE やその他のコンテンツ変換ツールを使用すれば、時間を節約し、上記のボイラープレートを自動的に生成できます。

Client Hints を使用したリソース選択の自動化

深く息を吸い、自信を捨て、次の例を考えてみましょう。

<meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">
...
<picture>
    <source media="(min-width: 50em)" sizes="50vw" srcset="/image/thing">
    <img sizes="100vw" src="/image/thing-crop">
</picture>

意外かもしれませんが、上記の例は、前述のはるかに長い画像のマークアップと同じ機能をすべて提供するのに十分であり、これから説明するように、画像リソースをいつ、どの方法で取得するかをデベロッパーが完全に制御できるようにします。最初の行にある「マジック」は、Client Hints レポートを有効にして、デバイスのピクセル比(DPR)、レイアウト ビューポートの幅(Viewport-Width)、リソースの想定ディスプレイの幅(Width)をサーバーにアドバタイズするようブラウザに指示します。

Client Hints を有効にすると、結果のクライアントサイドのマークアップは表示要件のみを保持します。デザイナーは、画像タイプ、クライアントの解像度、配信されるバイト数を削減するための最適なブレークポイント、他のリソース選択基準を気にする必要はありません。実際に彼らはそうした経験をしたことがなく、その必要もないはずです。さらに、実際のリソースの選択はクライアントとサーバーによってネゴシエートされるため、上記のマークアップを書き換えたり拡張したりする必要もないため、デベロッパー側での作業も不要です。

Chrome 46 では、DPRWidthViewport-Width のヒントがネイティブにサポートされています。ヒントはデフォルトで無効になっています。上記の <meta http-equiv="Accept-CH" content="..."> は、指定されたヘッダーを送信リクエストに追加するよう Chrome に指示するオプトイン シグナルとして機能します。それを踏まえて、サンプル画像リクエストのリクエスト ヘッダーとレスポンス ヘッダーを調べましょう。

Client Hints ネゴシエーション図

Chrome は Accept リクエスト ヘッダーを介して WebP 形式のサポートをアドバタイズします。新しい Edge ブラウザも同様に、Accept ヘッダーを介して JPEG XR のサポートをアドバタイズします。

次の 3 つのリクエスト ヘッダーは、クライアントのデバイスのデバイス ピクセル比(3x)、レイアウト ビューポートの幅(460px)、リソースの意図する表示幅(230px)をアドバタイズする Client-Hint ヘッダーです。これにより、独自のポリシーのセットに基づいて最適な画像バリアントを選択するために必要なすべての情報(事前生成リソースの可用性、リソースの再エンコードまたはサイズ変更の費用、リソースの人気度、現在のサーバー負荷など)がサーバーに提供されます。この場合、サーバーは DPR ヒントと Width ヒントを使用して、Content-TypeContent-DPRVary ヘッダーで示される WebP リソースを返します。

魔法などありません。リソースの選択を、HTML マークアップから、クライアントとサーバー間のリクエスト / レスポンス ネゴシエーションに移動しました。その結果、HTML は表示の要件のみを考慮し、デザイナーやデベロッパーなら誰でも記述できる内容になっています。一方、画像最適化空間の検索はコンピュータに委ねられ、大規模な自動化も容易になりました。パフォーマンスを重視するデベロッパーの皆様を覚えていますか?現在の仕事は、提供されたヒントを利用して適切なレスポンスを返す画像サービスを作成することです。ユーザーは任意の言語やサーバーを使用するか、サードパーティのサービスまたは CDN にこの処理を任せることができます。

<img src="/image/thing" sizes="50vw"
        alt="image thing displayed at 50% of viewport width">

また、上の男性のことを覚えていますか?Client Hints を使用すると、シンプルな画像タグは、追加のマークアップなしで DPR、ビューポート、幅を認識するようになりました。アート ディレクションを追加する必要がある場合は、前述のように picture タグを使用できます。そうしないと、既存のすべてのイメージタグが強化されています。Client Hints は、既存の img 要素と picture 要素を強化します。

Service Worker でリソースの選択を制御する

Service Worker は、実質的にはブラウザ内で実行されるクライアントサイド プロキシです。すべての送信リクエストをインターセプトし、レスポンスの検査、書き換え、キャッシュ保存、合成を行うことができます。イメージも同様です。Client Hints を有効にすると、アクティブな Service Worker は画像リクエストを特定し、提供されたクライアント ヒントを調べて、独自の処理ロジックを定義できます。

self.onfetch = function(event) {
    var req = event.request.clone();
    console.log("SW received request for: " + req.url)
    for (var entry of req.headers.entries()) {
    console.log("\t" + entry[0] +": " + entry[1])
    }
    ...
}
ServiceWorker。

ServiceWorker では、クライアントサイドでリソースの選択を完全に制御できます。これは非常に重要です。可能性はほぼ無限にあるため これを念頭に置いてください。

  • ユーザー エージェントが設定した Client Hints ヘッダー値は書き換えることができます。
  • 新しい Client Hints ヘッダー値をリクエストに追加できます。
  • URL を書き換え、画像リクエストを代替サーバー(CDN など)を指すようにできます。
    • インフラストラクチャへのデプロイが容易になる場合は、ヒントの値をヘッダーから URL 自体に移動することもできます。
  • レスポンスをキャッシュに保存し、リソースを提供する独自のロジックを定義できます。
  • ユーザーの接続に応じてレスポンスを調整できます。
  • アプリケーションとユーザー設定のオーバーライドを考慮できます。
  • 自分の心が望むなら、何でもできるのです。

picture 要素は、必要なアート ディレクション コントロールを HTML マークアップで提供します。クライアント ヒントは、リソース選択の自動化を有効にするイメージ リクエストの結果にアノテーションを提供します。Service Worker は、クライアントでリクエストとレスポンスの管理機能を提供します。これが拡張可能なウェブの実例です。

Client Hints に関するよくある質問

  1. Client Hints はどこで利用できますか?Chrome 46 で出荷されています。FirefoxEdge では検討が必要です。

  2. Client Hints がオプトインされるのはなぜですか?Google では、Client Hints を使用しないサイトのオーバーヘッドを最小限に抑えるためです。クライアントのヒントを有効にするには、サイトのページ マークアップで、Accept-CH ヘッダーまたは同等の <meta http-equiv> ディレクティブを指定する必要があります。いずれかが存在する場合、ユーザー エージェントはすべてのサブリソース リクエストに適切なヒントを追加します。将来的には、特定のオリジンに対してこの設定を保持するための追加のメカニズムを提供する可能性があります。これにより、ナビゲーション リクエストに対して同じヒントを配信できるようになります。

  3. Service Worker を使用している場合に Client Hints が必要な理由Service Worker はレイアウト、リソース、ビューポートの幅の情報にアクセスできません。少なくとも、コストのかかるラウンドトリップが発生し、画像リクエストが大幅に遅延することが必要になります(プリロード パーサーによって画像リクエストが開始される場合など)。Client Hints はブラウザと統合されており、このデータをリクエストの一部として利用できるようにします。

  4. Client Hints は画像リソース専用ですか?DPR、ビューポートの幅、幅のヒントの背景にある主なユースケースは、画像アセットのリソース選択を有効にすることです。ただし、タイプに関係なく、すべてのサブリソースに同じヒントが提供されます。たとえば、CSS リクエストと JavaScript リクエストも同じ情報を取得し、これらのリソースの最適化に使用できます。

  5. 一部の画像リクエストで幅が報告されません。なぜですか?サイトは固有の画像サイズに依存しているため、ブラウザは意図された表示幅を把握できない場合があります。そのため、このようなリクエストや、「表示幅」がないリクエスト(JavaScript リソースなど)では、幅のヒントが省略されます。幅のヒントを取得するには、画像のサイズ値を必ず指定してください。

  6. <insert my favorite Hint> について Service Worker を使用すると、デベロッパーはすべての送信リクエストをインターセプトして変更(新しいヘッダーを追加するなど)できます。たとえば、現在の接続タイプを示す NetInfo ベースの情報を簡単に追加できます。詳しくは、「ServiceWorker による機能レポート」をご覧ください。純粋な SW ベースの実装ではすべての画像リクエストが遅延するため、Chrome に付属する「ネイティブ」のヒント(DPR、Width、Resource-Width)がブラウザに実装されています。

  7. 詳細情報やデモはどこで見ることができますか?また、どうすればよいですか?説明ドキュメントを確認し、フィードバックやその他の質問がある場合は、お気軽に GitHub で問題を公開してください。