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

ウェブ向けに構築することで、比類のないリーチを実現できます。ウェブ アプリケーションは、ブランドやプラットフォームに関係なく、スマートフォン、タブレット、ノートパソコン、デスクトップ、テレビなど、ほとんどの接続デバイスでワンクリックで利用できます。最適なエクスペリエンスを提供するために、各フォーム ファクタに合わせて表示と機能を適応させるレスポンシブ サイトを構築しました。次に、アプリケーションをできるだけ速く読み込むためにパフォーマンス チェックリストを実行します。クリティカル レンダリング パスを最適化し、テキスト リソースを圧縮してキャッシュに保存しました。次に、転送されるバイト数の大部分を占める画像リソースを確認します。問題は、画像の最適化は難しいことです。

  • 適切な形式(ベクターとラスター)を決定する
  • 最適なエンコード形式(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. 最適な画質を得るには、1x、1.5x、2x、2.5x、3x など、さまざまな解像度で各画像の複数のバリエーションを生成する必要があります。
  3. 不要なピクセルを配信しないようにするには、「ユーザーのビューポートの 50%」が実際に何を意味するのかを理解する必要があります。ビューポートの幅はさまざまです。
  4. また、低速ネットワークを使用しているユーザーに対しては、自動的に低解像度を取得する復元力のあるエクスペリエンスを提供することも理想的です。結局のところ、ガラスに映る時間なのです。
  5. また、アプリケーションには、取得する画像リソースに影響するユーザー コントロールも公開されているため、その点も考慮する必要があります。

デザイナーは、ビューポートのサイズが小さい場合は読みやすさを最適化するために、別の画像を幅 100% で表示する必要があることに気付きました。つまり、もう一つのアセットに対して同じプロセスを繰り返して、ビューポートのサイズに応じてフェッチを条件付きにする必要があります。ここまでで、この作業が難しいことをお伝えしましたか?では、始めましょう。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 やその他のコンテンツ変換ツールを使用すると、上記のボイラープレートを自動的に生成できます。

クライアント ヒントによるリソース選択の自動化

深呼吸して、非現実的な状況を受け入れ、次の例について考えてみましょう。

<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>

上記の例は、上記の長い画像マークアップと同じ機能をすべて提供するのに十分です。また、後述するように、画像リソースの取得方法、取得する画像、取得するタイミングをデベロッパーが完全に制御できます。最初の行が「魔法」です。この行は、クライアント ヒント レポートを有効にし、デバイスのピクセル比率(DPR)、レイアウト ビューポートの幅(Viewport-Width)、リソースの想定表示幅(Width)をサーバーに通知するようブラウザに指示します。

クライアント ヒントを有効にすると、生成されるクライアントサイド マークアップには表示要件のみが保持されます。デザイナーは、画像の種類、クライアントの解像度、配信バイトを削減するための最適なブレークポイント、その他のリソース選択基準について心配する必要はありません。実際のところ、YouTube はそうしたことは決して行っていませんし、行う必要もありません。さらに、実際のリソース選択はクライアントとサーバーでネゴシエートされるため、デベロッパーは上記のマークアップを書き換えて拡張する必要もありません。

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

クライアント ヒントのネゴシエーション図

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

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

魔法のような方法はありません。リソースの選択を HTML マークアップから、クライアントとサーバー間のリクエスト / レスポンス ネゴシエーションに移動しました。その結果、HTML は表示要件のみを扱い、どのデザイナーやデベロッパーでも信頼して記述できるものになりました。一方、画像最適化空間の検索はコンピュータに委ねられ、大規模に簡単に自動化できるようになりました。パフォーマンスを重視するデベロッパーを思い出してください。次に、提供されたヒントを活用して適切なレスポンスを返す画像サービスを作成します。任意の言語またはサーバーを使用できます。また、サードパーティ サービスまたは CDN に代行してもらうこともできます。

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

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

サービス ワーカーによるリソース選択の制御

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

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 を使用すると、リソースの選択をクライアントサイドで完全に制御できます。これは非常に重要です。可能性は無限に近いため、よく理解してください。

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

picture 要素は、HTML マークアップで必要なアートディレクション制御を提供します。クライアント ヒントは、結果の画像リクエストにアノテーションを提供し、リソース選択の自動化を可能にします。ServiceWorker は、クライアントでリクエストとレスポンスを管理する機能を提供します。これは拡張可能なウェブの機能です。

クライアント ヒントに関するよくある質問

  1. クライアント ヒントはどこで利用できますか?Chrome 46 でリリースされました。FirefoxEdge で検討中。

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

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

  4. クライアント ヒントは画像リソース専用ですか?DPR、Viewport-Width、Width のヒントの主なユースケースは、画像アセットのリソース選択を可能にすることです。ただし、タイプに関係なく、すべてのサブリソースに同じヒントが配信されます。たとえば、CSS リクエストと JavaScript リクエストも同じ情報を取得し、それらのリソースの最適化にも使用できます。

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

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

  7. 詳細情報やデモはどこで確認できますか?説明ドキュメントをご覧ください。フィードバックやその他のご質問がある場合は、GitHub で問題を報告してください。