WebView での投機的読み込み

ナビゲーション レイテンシは、ユーザー エクスペリエンスの重要な指標です。デベロッパーがこのレイテンシを短縮できるように、WebView には投機的読み込み用の API が用意されています。これにより、ユーザーが明示的に移動する前に、アプリがコンテンツをフェッチまたはレンダリングできます。

WebView では、事前接続、プリフェッチ、プリレンダリングの 3 種類の投機的読み込みがサポートされています。

投機的読み込み戦略を実装すると、次のことが可能になります。

  • ウェブ コンテンツの読み込みレイテンシを大幅に短縮する: ネットワークの開始時間をアプリのライフサイクルの早い段階に移動します。
  • ナビゲーションの成功率を高める: ネットワークとキャッシュを事前に準備することで、一時的なネットワークの問題によるナビゲーションの失敗を減らすことができます。
  • 応答性を向上させる: 特に事前レンダリングでは、瞬時の切り替えが可能になり、アプリの動作が大幅に高速化されたように感じられます。

投機的読み込み戦略を選択する

これらの戦略の主な違いはスコープにあります。事前接続 API はオリジンベースであるため、ターゲット ドメインのみが必要です。プリフェッチ API とプリレンダリング API は URL ベースであるため、正確なウェブページ パスが必要です。

事前接続はオリジン レベルで動作するため、ユーザーが移動する特定のコンテンツやページがわからなくても、アプリのライフサイクルの早い段階で開始できます。

次の表に、これらの 3 つの戦略を比較して、ユースケースに適した戦略を選択できるようにします。

機能 事前接続 プリフェッチ プリレンダリング
最終目標 接続を準備する HTML のみをキャッシュに保存する(JavaScript や CSS は除く) ページ全体をプリレンダリングする
スコープ プロファイル レベル(WebView 間で共有) プロファイル レベル(WebView 間で共有) WebView レベル(特定の WebView にバインド)
Jetpack WebKit API androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
コア API メソッド preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
構成 なし PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
リソースの使用量 低(ネットワーク) 中(ネットワーク、メモリ) 高(CPU、メモリ、ネットワーク)
使用場面 ターゲット オリジンはわかっているが、特定の URL はまだ決まっていない場合。 正確な URL がわかっていて、ナビゲーションの可能性が高く、WebView 間でキャッシュが共有されている場合。 正確な URL がわかっていて、特定の WebView 内でナビゲーションがほぼ確実に行われる場合。
特典 オリジンの任意の URL の接続設定を高速化 一致する URL のネットワーク読み込みを高速化 有効化時に瞬時にナビゲーション

オリジンに事前接続する

事前接続により、指定されたオリジンの DNS ルックアップと TCP/TLS handshake を事前に行うことで、今後の読み込みを高速化できます。

正確な宛先 URL が必要なプリフェッチとプリレンダリングとは異なり、事前接続は厳密にオリジンベースです。これにより、プリフェッチとプリレンダリングよりもはるかに早く事前接続呼び出しを行うことができます。

このリソース消費量の少ないプロファイル レベルの戦略により、オリジンがまだアクセスされていない場合、そのプロファイルを共有する WebView の初期レイテンシが短縮されます。 接続は約 30 秒間開いたままになり、handshake のオーバーヘッドを排除することで、後続のクロスオリジン HTTP リクエスト、ナビゲーション、サブリソースにメリットをもたらします。

実装

事前接続を開始するには、Profile インスタンスで preconnect(String url) を呼び出します。この API は UI スレッドで呼び出す必要があり、WebViewFeature.PRECONNECT がサポートされている必要があります。

API はオリジンで動作しますが、便宜上、完全な URL を指定できます (https://www.example.com/index.html など)。これは オリジン(https://www.example.com など)への呼び出しとして自動的に処理されます。この API を複数回呼び出すことで、複数のオリジンに接続できます 。

Kotlin

// Must be called on the @UiThread
if (WebViewFeature.isFeatureSupported(WebViewFeature.PRECONNECT)) {
    profile.preconnect("https://www.example.com/index.html")
    // This initiates a connection to the origin https://www.example.com
}

Java

// Must be called on the @UiThread
if (WebViewFeature.isFeatureSupported(WebViewFeature.PRECONNECT)) {
    profile.preconnect("https://www.example.com/index.html");
    // This initiates a connection to the origin https://www.example.com
}

共通構成: PrefetchParametersPrerenderParameters

プリフェッチとプリレンダリングの両方で、PrefetchParameters または PrerenderParameters を使用してリクエストをカスタマイズします。これらのクラスを使用すると、 No-Vary-Search 構成など、URL マッチング用の追加のヘッダーとヒントを指定できます。

Kotlin

// Isolated configuration specifically for Cache-Level Prefetching
val prefetchParams = PrefetchParameters.Builder()
    .addAdditionalHeader("X-Custom-Client", "Android-App-V2")
    .setExpectedNoVarySearchHeader(
        NoVarySearchHeader.varyExcept(true, listOf("session_id", "click_ref"))
    )
    .build()

Java

PrefetchParameters prefetchParams = new PrefetchParameters.Builder()
    .addAdditionalHeader("X-Custom-Header", "value")
    /**
     * Hint to ignore specific query parameters during cache matching.
     * This allows the cache to match even if the tracking_id differs.
     */
    .setExpectedNoVarySearchHeader(
        NoVarySearchHeader.varyExcept(true, Arrays.asList("tracking_id"))
    )
    /**
     * Determines if Client Hints are sent.
     * NOTE: This is ignored for Prerendering API requests, which default to
     * the WebView's WebSettings.getJavaScriptEnabled() value.
     */
    .setJavaScriptEnabled(true)
    .build();

コンテンツのプリフェッチ

プリフェッチでは、URL のメイン HTML リソースをダウンロードして、プロファイルのネットワーク キャッシュに保存します。WebView では、Profile は Cookie、HTTP キャッシュ、Service Worker などのブラウザデータのコンテナとして機能します。プリフェッチはプロファイル レベルのオペレーションであるため、そのプロファイルに関連付けられた WebView はキャッシュに保存されたレスポンスを利用できます。

実装

プリフェッチを開始するには、Profile インスタンスで prefetchUrlAsync() を呼び出します。このオペレーションでは、HTTPS スキームのみがサポートされています。

Kotlin

profile.prefetchUrlAsync(
    url,
    prefetchParams,
    cancellationSignal,
    executor,
    object : WebViewOutcomeReceiver<PrefetchResult, PrefetchException> {
        override fun onResult(result: PrefetchResult) {
            if (result.wasDuplicate()) {
                // URL and No-Vary-Search permutations already exist in the cache layer
            } else {
                // The HTML payload has been successfully secured in the HTTP cache
            }
        }

        override fun onError(error: PrefetchException) {
            when (error) {
                is PrefetchNetworkException -> {
                    // Isolates network layer or server-side HTTP anomalies
                    val code = error.httpStatusCode
                    // Facilitates rapid diagnosis of 4xx or 5xx server responses
                }
                else -> {
                    // Catches generalized execution failures and system constraints
                }
            }
        }
    }
)

Java

profile.prefetchUrlAsync(
    url,
    prefetchParams,
    cancellationSignal,
    executor,
    new WebViewOutcomeReceiver<PrefetchResult, PrefetchException>() {
        @Override
        public void onResult(PrefetchResult result) {
            if (result.wasDuplicate()) {
                // URL and No-Vary-Search permutations already exist in the cache layer
            } else {
                // The HTML payload has been successfully secured in the HTTP cache
            }
        }

        @Override
        public void onError(PrefetchException error) {
            if (error instanceof PrefetchNetworkException) {
                // Isolates network layer or server-side HTTP anomalies
                int code = ((PrefetchNetworkException) error).httpStatusCode;
                // Facilitates rapid diagnosis of 4xx or 5xx server responses
            } else {
                // Catches generalized execution failures and system constraints
            }
        }
    }
);

インターセプトのライフサイクル

WebView のプリフェッチ リクエストにより、shouldInterceptRequest() コールバックがトリガーされるタイミングと方法が変わります。プリフェッチされたコンテンツが正常に使用されるかどうかに直接影響するため、2 ステップのライフサイクルを理解することが重要です。

投機的フェーズとナビゲーション フェーズにおける 2 段階の WebView プリフェッチ インターセプトのライフサイクルを示す図。
図 1.WebView のプリフェッチ リクエストとナビゲーションの 2 ステップのインターセプト ライフサイクル。

1. 投機的フェーズ(プリフェッチ リクエスト)

prefetchUrlAsync() が呼び出されると、WebView はバックグラウンドでメイン HTML リソースをダウンロードします。このバックグラウンド リクエストでは、shouldInterceptRequest() は完全にスキップされます。通常インターセプタ内で処理されるカスタム ロジック、認証トークン、ヘッダー挿入は、プリフェッチされた HTML リソースには適用されません。

**2. ナビゲーション フェーズ(ユーザーの有効化)

アプリが URL に明示的に移動した場合(loadUrl() を使用するなど)、またはユーザーが一致するリンクをクリックした場合、WebView はプリフェッチされたキャッシュを使用できるかどうかを判断します。

  • メイン HTML の評価: この時点で、WebView はメイン HTML に対して shouldInterceptRequest() をトリガーします。プリフェッチ キャッシュからページを正常に配信するには、インターセプタが null を返す必要があります。カスタム WebResourceResponse を返すと、WebView はインターセプタを尊重し、プリフェッチ キャッシュを完全にバイパスします。

  • サブリソースの評価: プリフェッチされた HTML が使用可能になると、ページのレンダリングを完了するために必要な後続のサブリソース(画像、スクリプト、CSS など)に対して、shouldInterceptRequest() が通常どおりトリガーされます。

主な動作

WebView がプリフェッチ リクエストを開始して管理する方法は、次のオペレーション特性と適格性チェックによって決まります。

  • スレッド セーフティ: リクエストはどのスレッドからでも開始できます。
  • 適格性: フェッチを開始する前に、WebView は次のことを確認して、リクエストが安全でコンテキストに適していることを確認します。
    • 既存の Cookie: ユーザーのプライバシーを保護し、CSRF のような副作用を防ぐため、リクエストで特定の認証済み Cookie が必要な場合、WebView はプリフェッチをスキップすることがあります。この Cookie はサーバーで状態の変更をトリガーする可能性があります。
    • Service Worker の存在: Service Worker がすでに URL のスコープを制御している場合、WebView は標準のネットワーク プリフェッチを開始するのではなく、Service Worker のフェッチ ハンドラに委譲できます。
    • プロキシの可用性: WebView は、現在のネットワーク パス(構成済みのプロキシを含む)が安定していることを確認し、複雑なネットワーク構成で投機的リクエストが失敗しないようにします。
  • プリフェッチが開始されない場合(パラメータが有効であっても)、WebView がバックグラウンド リクエストがユーザーの現在のセッションまたはセキュリティ状態に干渉する可能性があると判断したことが原因であることがよくあります。
  • キャンセル: CancellationSignal を使用して、処理中のリクエストを終了し、キャッシュに保存されないようにします。

ページの事前レンダリング

事前レンダリングでは、非表示の「ウェブ コンテンツ」を作成して、スクリプトの実行やサブリソースのフェッチなど、ページをバックグラウンドで完全にレンダリングします。プリレンダリングは、プリフェッチと同じ基盤となるインフラストラクチャに依存しています。アプリがプリレンダリングを開始すると、WebView はまずレスポンスのプリフェッチを実行してプリレンダリング ナビゲーションを提供し、冗長なネットワーク アクティビティを回避します。

実装

プリレンダリングは WebView インスタンス レベルのオペレーションです。UI スレッドから WebViewCompat を使用して prerenderUrlAsync() を呼び出します。

Kotlin

WebViewCompat.prerenderUrlAsync(
    webView,
    url,
    cancellationSignal,
    executor,
    params,
    object : PrerenderOperationCallback {
        override fun onPrerenderActivated() {
            // Called when the user navigates to the URL and the hidden page is swapped in
        }

        override fun onError(exception: Throwable) {
            // exception is an instance of PrerenderException
            // Handle prerender failure (for example, memory pressure or disallowed JavaScript APIs)
        }
    }
)

Java

WebViewCompat.prerenderUrlAsync(webView, url, cancellationSignal, executor, params, new PrerenderOperationCallback() {
    @Override
    public void onPrerenderActivated() {
        // Called when the user navigates to the URL and the hidden page is swapped in.
    }

    @Override
    public void onError(@NonNull Throwable exception) {
        // Handle prerender failure (for example, resource constraints or disallowed APIs).
    }
});

プリフェッチとプリレンダリングはどちらも完全に非同期です。prefetchUrlAsync() はどのスレッドからでも呼び出すことができますが、prerenderUrlAsync() は UI スレッドから開始する必要があります。

技術的な制約

WebView は、瞬時のナビゲーションとシステムの健全性のバランスを取るために、次のランタイム制約を適用します。

  • メモリ プレッシャー: デバイスの RAM が不足している場合、WebView はプリレンダリングされた URL をキャンセルします。
  • 許可されていない API: JavaScript がバックグラウンド コンテキストで特定の API(音声再生、アラートなど)にアクセスしようとすると、プリレンダリングは直ちに終了します。
  • インスタンスの上限: WebView ごとに許可されるアクティブなプリレンダリング URL の数には上限があります。

URL マッチングと No-Vary-Search(NVS)

WebView では、プリロードされたリソースが目的のナビゲーションでのみ提供されるように、信頼性の高いマッチング アルゴリズムが必要です。

完全一致と NVS 一致

デフォルトでは、プリフェッチとプリレンダリングには正確な URL の一致が必要です。移動先の URL がプリロードされた URL と同一の場合は、キャッシュからすぐに提供されます。クエリ パラメータが異なる場合、WebView は次の No-Vary-Search (NVS)ルールを使用します。

  • ヒント: デベロッパーは、開始時に setExpectedNoVarySearchHeader() ヒントを指定します。移動先の URL が、ヒント付きパラメータを除いたリクエスト URL と一致する場合、WebView はサーバーの実際のヘッダーを待つために一時的にブロックします。
  • サーバー ヘッダー: サーバーからの NVS レスポンス ヘッダーが最終的な権限です。サーバーがクエリの違いを無視する必要があることを確認した場合、一致はキャッシュから提供されます。そうでない場合、WebView はコールド ネットワーク読み込みにフォールバックします。

No-Vary-Search(NVS)は高度な使用を目的としており、ほとんどのデベロッパーはプリフェッチと loadUrl() の両方にまったく同じ URL を渡すため、必要ない場合があります。このガイダンスは、プリフェッチ URL と loadUrl() URL の間でクエリ パラメータが異なる場合にのみ必要です。

グローバル構成

PrefetchCache の上限と最大プリレンダリング数を構成して、プロファイル レベルで投機的読み込みの動作を調整します。カスタム プリフェッチの上限をシステムのデフォルトに戻すこともできます。

Kotlin

// Configure prefetch cache limits
profile.prefetchCache.setMaxPrefetches(10)
profile.prefetchCache.setPrefetchTtlSeconds(60)

// Reset to system defaults when needed
profile.prefetchCache.clearMaxPrefetches()

// Configure maximum active prerenders
profile.setMaxPrerenders(2)

Java

// Configure prefetch cache limits
PrefetchCache prefetchCache = profile.getPrefetchCache();
prefetchCache.setMaxPrefetches(10);
prefetchCache.setPrefetchTtlSeconds(60);

// Reset to system defaults when needed
prefetchCache.clearMaxPrefetches();

// Configure maximum active prerenders
profile.setMaxPrerenders(2);

エラー処理と例外

投機的オペレーションでは、OutcomeReceiverCompat または PrerenderOperationCallback を使用して結果を報告します。

主な例外

投機的読み込みオペレーションが失敗した場合、エラー ハンドラは次のいずれかの主な例外タイプを報告して、特定の失敗シナリオを診断できるようにします。

  • PrefetchException: すべての非同期プリフェッチ エラーの基本クラス。
  • PrefetchNetworkException: ネットワークまたはサーバー レベルの障害を示します。 サーバーサイドの問題を診断するのに役立つ httpStatusCode フィールド(404 や 503 など)を含めることができます。
  • PrerenderException: メモリ プレッシャーによる失敗や、バックグラウンドでの許可されていない API(音声再生など)の使用など、プリレンダリング関連のすべてのエラーのスーパークラス。

最適化戦略

システム リソースを節約しながら投機的読み込みのメリットを最大限に活用するには、次の推奨事項に従ってください。

  • 早期に開始する: アプリの起動時、またはナビゲーションの宛先がほぼ確定したらすぐにプリフェッチを開始します。
  • 統合戦略: プリフェッチ キャッシュにすでに存在する URL をプリレンダリングする場合、プリレンダリング ナビゲーションはそのキャッシュから提供されるため、冗長なネットワーク リクエストを回避できます。
  • 割り当てをモニタリングする: プリレンダリングはリソースを大量に消費します。可能性の高い候補をいくつかプリフェッチし、プリレンダリングは最も可能性の高いナビゲーション用に予約することをおすすめします。
  • スキームのサポート: すべての URL で必須の HTTPS スキームを使用してください。無効なスキームまたは null 入力は、同期 IllegalArgumentException をトリガーします。

参考情報

ウェブアプリのデバッグ、WebView の起動パフォーマンスの最適化、レンダラ プロセスの終了の処理について詳しくは、次のリソースをご覧ください。