WebView 中的推测加载

导航延迟时间是衡量用户体验的一项关键指标。为了帮助开发者缩短此延迟时间,WebView 提供了用于推测加载的 API,让您的应用能够在用户明确导航到内容之前提取或呈现内容。

WebView 支持三种主要类型的推测加载:预连接、预提取和 Prerender(预渲染)。

通过实现推测加载策略,您可以实现以下目标:

  • 大幅缩短网页内容加载延迟时间: 将网络启动时间提前到应用生命周期的早期阶段。
  • 提高导航成功率: 通过预热网络和缓存,导航不太可能因瞬时网络问题而失败。
  • 提升感知响应速度: 尤其是 Prerender(预渲染),可实现即时转换,让应用感觉明显更快。

选择推测加载策略

这些策略之间的主要区别在于其范围:预连接 API 基于来源,这意味着它只需要目标网域。预提取和 Prerender(预渲染)API 基于网址,这意味着它们需要确切的网页路径。

由于预连接在来源级别运行,因此可以在应用生命周期的早期阶段启动,甚至在您知道用户将导航到的具体内容或网页之前启动。

下表对这三种策略进行了比较,以帮助您为自己的使用场景选择合适的策略:

功能 预连接 预提取 Prerender(预渲染)
主要目标 预热连接 仅缓存 HTML(不缓存 JavaScript 或 CSS) Prerender(预渲染)整个网页
范围 配置文件级别(在 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、内存、网络)
适用情形 当目标来源已知,但具体网址尚未确定时。 当确切网址已知且导航很可能发生时,缓存会在 WebView 之间共享。 当确切网址已知且导航在特定 WebView 中非常确定时。
福利 加快来源上任何网址的连接设置速度 加快匹配网址的网络加载速度 激活后真正实现即时导航

预连接到来源

预连接通过预先执行指定来源的 DNS 查找和 TCP/TLS 握手来加快未来的加载速度。

与需要确切目标网址的预提取和 Prerender(预渲染)不同,预连接严格基于来源。这让您能够比预提取和 Prerender(预渲染)更早地进行预连接调用。

这种低资源、配置文件级别的策略可缩短共享该配置文件的任何 WebView 的初始延迟时间,前提是该来源尚未被访问过。 连接保持打开状态约 30 秒,通过消除握手开销,使任何后续的跨源 HTTP 请求、导航或子资源受益。

实现

如需启动预连接,请在 Profile 实例上调用 preconnect(String url)。此 API 必须在界面线程上调用,并且需要支持 WebViewFeature.PRECONNECT

该 API 在来源上运行,但为方便起见,可以提供完整网址 (例如 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

预提取和 Prerender(预渲染)都使用 PrefetchParametersPrerenderParameters 来自定义请求。借助这些类,您可以提供 额外的标头和提示,例如 No-Vary-Search 配置

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();

预提取内容

预提取会下载网址的主要 HTML 资源,并将其存储在配置文件的网络缓存中。在 WebView 中,Profile 充当浏览器数据的容器,包括 Cookie、HTTP 缓存和服务工作器。由于预提取是配置文件级别的操作,因此与该配置文件关联的任何 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() 回调的触发时间和方式。由于这会直接影响预提取的内容是否成功使用,因此了解两步生命周期至关重要:

图表:显示在推测阶段和导航阶段,两步 WebView 预提取拦截生命周期。
图 1.WebView 预提取 请求和导航的两步拦截生命周期。

1. 推测阶段(预提取请求)

调用 prefetchUrlAsync() 时,WebView 会在后台下载主要 HTML 资源。对于此后台请求,系统会完全跳过 shouldInterceptRequest()。通常在拦截器内处理的任何自定义逻辑、授权令牌或标头注入都不会应用于预提取的 HTML 资源。

**2. 导航阶段(用户激活)

当应用明确导航到网址(例如,使用 loadUrl())或用户点击匹配的链接时,WebView 会确定是否可以使用预提取的缓存:

  • 主要 HTML 评估: WebView 此时会触发主要 HTML 的 shouldInterceptRequest()。如需成功从预提取缓存传送网页,拦截器必须返回 null。如果您返回自定义 WebResourceResponse,WebView 会尊重您的拦截器并完全绕过预提取缓存。

  • 子资源评估: 在清除预提取的 HTML 以供使用后,shouldInterceptRequest() 会针对完成网页呈现所需的所有后续子资源(例如,图片、脚本和 CSS)正常触发。

关键行为

以下操作特征和资格检查决定了 WebView 如何启动和管理预提取请求:

  • 线程安全: 可以从任何线程启动请求。
  • 资格: 在启动提取之前,WebView 会通过检查以下内容来确保请求安全且在上下文中适当:
    • 现有 Cookie: 为了保护用户隐私并防止类似 CSRF 的副作用,如果请求需要特定的经过身份验证的 Cookie,而这些 Cookie 可能会触发服务器上的状态更改,WebView 可能会跳过预提取。
    • Service Worker 存在: 如果 Service Worker 已控制网址的范围,WebView 可以遵循 Service Worker 的提取处理程序,而不是启动标准网络预提取。
    • 代理可用性: WebView 会验证当前网络路径(包括任何已配置的代理)是否稳定,以避免在复杂的网络配置下推测性请求失败。
  • 如果预提取无法启动(即使使用有效的参数),通常是因为 WebView 已确定后台请求可能会干扰用户的当前会话或安全状态。
  • 取消: 使用 CancellationSignal 终止正在进行的请求并阻止其被缓存。

Prerender(预渲染)网页

Prerender(预渲染)会创建隐藏的“网页内容”,以便在后台完全呈现网页,包括脚本执行和子资源提取。Prerender(预渲染)依赖于与预提取相同的底层基础架构。如果应用启动 Prerender(预渲染),WebView 会先预提取响应,以传送 Prerender(预渲染)导航,从而避免冗余的网络活动。

实现

Prerender(预渲染)是 WebView 实例级别的操作。从界面线程使用 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).
    }
});

预提取和 Prerender(预渲染)都是完全异步的。prefetchUrlAsync() 可以从任何线程调用,而 prerenderUrlAsync() 必须从界面线程启动。

技术限制

为了平衡即时导航与系统健康状况,WebView 强制执行以下运行时限制:

  • 内存压力: 如果设备 RAM 不足,WebView 会取消预渲染的网址。
  • 禁止使用的 API: JavaScript 在后台上下文中尝试访问某些 API(例如,音频播放、提醒)的任何操作都会立即终止 Prerender(预渲染)。
  • 实例限制: 每个 WebView 允许的活跃预渲染网址数量有限制。

网址匹配和 No-Vary-Search (NVS)

WebView 需要可靠的匹配算法,以确保预加载的资源仅用于其预期的导航。

完全匹配与 NVS 匹配

默认情况下,预提取和 Prerender(预渲染)需要完全匹配的网址。如果导航的网址与预加载的网址相同,则会立即从缓存传送。如果查询参数不同,WebView 会使用以下 No-Vary-Search (NVS) 规则:

  • 提示: 开发者在启动期间提供 setExpectedNoVarySearchHeader() 提示。如果导航的网址与请求网址减去提示的参数匹配,WebView 会短暂阻止,以等待服务器的实际标头。
  • 服务器标头: 来自服务器的 NVS 响应标头是最终权威。如果服务器确认应忽略查询差异,则匹配项会从缓存传送。否则,WebView 会回退到冷网络加载。

No-Vary-Search (NVS) 适用于高级使用场景,大多数开发者可能不需要它,因为他们会将完全相同的网址传递给预提取和 loadUrl()。只有当预提取网址和 loadUrl() 网址之间的查询参数存在差异时,才需要此指南。

全局配置

通过配置 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);

错误处理和异常

推测性操作使用 OutcomeReceiverCompatPrerenderOperationCallback 来报告结果。

主要异常

当推测加载操作失败时,错误处理程序会报告以下主要异常类型之一,以帮助您诊断特定的失败场景:

  • PrefetchException: 所有异步预提取错误的基类。
  • PrefetchNetworkException: 表示网络或服务器级别失败。 它可以包含 httpStatusCode 字段(例如,404 或 503),以帮助诊断服务器端问题。
  • PrerenderException: 所有与 Prerender(预渲染)相关的错误的超类,例如因内存压力或在后台使用禁止使用的 API(如音频播放)而导致的失败。

优化策略

遵循以下建议,以最大限度地发挥推测性加载的优势,同时节省系统资源:

  • 尽早启动: 在应用启动期间或导航目的地很可能出现时立即开始预提取。
  • 集成策略: 如果您 Prerender(预渲染)已在预提取缓存中的网址,则 Prerender(预渲染)导航会从该缓存传送,从而避免冗余的网络请求。
  • 监控配额: Prerender(预渲染)会占用大量资源。最好为几个可能的候选对象进行预提取,并为最有可能的导航保留 Prerender(预渲染)。
  • 架构支持: 确保所有网址都使用强制性 HTTPS 架构。无效的架构或 null 输入会触发同步 IllegalArgumentException

其他资源

如需详细了解如何调试 Web 应用、优化 WebView 启动性能以及处理渲染器进程终止,请参阅以下资源: