WebView 中的推测加载

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

WebView 支持三种主要的推测加载类型:预连接、预提取和预渲染,同时还支持 QUIC 提示来优化协议协商。

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

  • 大幅缩短了网页内容加载延迟时间:将网络启动时间提前到应用生命周期的早期阶段,并优先使用 HTTP/3 等更快的协议。
  • 更高的导航成功率:通过预热网络和缓存,导航不太可能因暂时性网络问题而失败。
  • 更好的感知响应速度:预渲染尤其能实现即时过渡,让应用感觉快得多。

选择推测加载策略

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

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

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

功能 预连接 预取 预渲染
主要目标 热身连接 仅缓存 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、内存、网络)
适用情形 当目标来源已知,但具体网址尚未确定时。 当确切网址已知且可能发生导航时,在 WebView 之间共享缓存。 当确切网址已知,并且在特定 WebView 中的导航高度确定时。
益处 为来源中的任何网址更快地设置连接 更快地加载匹配网址的网络内容 激活后即可立即开始导航

预连接到来源

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

与需要确切目标网址的预提取和预渲染不同,预连接严格基于来源。这样一来,您就可以比预提取和预渲染更早地进行预连接调用。

这种低资源、配置文件级策略可减少共享相应配置文件的任何 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
}

通过 QUIC 提示表明 QUIC 协议支持

HTTP/3(基于 QUIC 传输协议运行)在延迟方面比 HTTP/2 有了显著改进,包括 0-RTT 握手、改进的连接弹性,以及在丢包期间消除队头阻塞。

默认情况下,WebView 仅在有迹象表明源支持 QUIC 时(例如通过之前的互动获取的 Alt-Svc 标头或 DNS HTTPS 记录)尝试建立 QUIC 连接。如果没有此先验知识,WebView 会使用 HTTP/2 或 HTTP/1.1 进行初始连接。

调用 addQuicHints 会预先填充此协议支持信息,从而允许 WebView 在首次连接到指定来源时立即使用 QUIC 进行连接。

使用 QUIC 提示进行预连接

虽然预连接提示和 QUIC 提示都是 Profile 上的源级优化,但它们的作用各不相同,且互为补充:

  • preconnect:主动打开并维护网络连接(DNS 查找和 TCP/TLS 或 QUIC 握手),持续时间约为 30 秒。由于它会保持打开的活跃网络连接,因此会消耗设备和网络资源,应仅用于高概率目标来源。
  • addQuicHints:不会产生任何即时网络流量。它会更新 Profile 网络堆栈的内存中服务器属性,以记录协议支持情况。由于开销可忽略不计,因此您可以在应用启动期间为所有已知的支持 HTTP/3 的来源安全地配置 QUIC 提示。

为获得最佳性能,请在调用 preconnectprefetchUrlAsyncloadUrl 之前调用 addQuicHints。这样可确保任何后续预连接或网页请求从一开始就协商 HTTP/3。

实现

如需配置 QUIC 提示,请对 Profile 实例调用 addQuicHints(Set<String> urls)。您必须在界面线程上调用此 API,并验证 WebView 是否支持 WebViewFeature.ADD_QUIC_HINTS_V1 功能。

preconnect 类似,addQuicHints 也针对来源运行,但可以提供完整网址(例如 https://www.example.com/index.html),并且会自动将其标准化为来源 (https://www.example.com)。

该方法是累加的:多次调用该方法会将提供的来源合并到整个 Profile 中。

Kotlin

// Must be called on the @UiThread
@OptIn(Profile.ExperimentalAddQuicHints::class)
if (WebViewFeature.isFeatureSupported(WebViewFeature.ADD_QUIC_HINTS_V1)) {
    val quicOrigins = setOf(
        "https://www.example.com",
        "https://api.example.com"
    )
    profile.addQuicHints(quicOrigins)
    // WebView now prioritizes HTTP/3 over QUIC for connections to these origins
}

Java

// Must be called on the @UiThread
// Requires @Profile.ExperimentalAddQuicHints annotation or suppression
if (WebViewFeature.isFeatureSupported(WebViewFeature.ADD_QUIC_HINTS_V1)) {
    Set<String> quicOrigins = new HashSet<>(Arrays.asList(
        "https://www.example.com",
        "https://api.example.com"
    ));
    profile.addQuicHints(quicOrigins);
    // WebView now prioritizes HTTP/3 over QUIC for connections to these origins
}

常见配置:PrefetchParametersPrerenderParameters

预提取和预渲染都使用 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. 导航阶段(用户激活)

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

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

  • 子资源评估:预提取的 HTML 清除完毕后,shouldInterceptRequest() 会正常触发,以获取完成网页渲染所需的所有后续子资源(例如图片、脚本和 CSS)。

主要行为

以下操作特征和资格条件检查决定了 WebView 如何发起和管理预取请求:

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

预渲染网页

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

实现

预渲染是一项 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).
    }
});

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

技术限制

为了在即时导航和系统健康状况之间取得平衡,WebView 强制执行以下运行时限制:

  • 内存压力:如果设备的 RAM 不足,WebView 会取消预渲染的网址。
  • 不允许使用的 API:如果 JavaScript 尝试在后台情境中访问某些 API(例如音频播放、提醒),系统会立即终止预渲染。
  • 实例限制:每个 WebView 允许的有效预渲染网址数量有限。

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

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

完全匹配与 NVS 匹配

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

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

No-Vary-Search (NVS) 适用于高级用法,大多数开发者可能不需要它,因为他们会向预提取和导航传递完全相同的网址(WebViewCompat.navigateloadUrl)。只有在预提取网址和导航网址的查询参数存在差异时,才需要遵循此指南。

全局配置

通过配置 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:所有与预渲染相关的错误的超类,例如因内存压力或在后台使用不允许的 API(如音频播放)而导致的失败。

优化策略

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

  • 尽早启动:在应用启动期间或在可能出现导航目的地时立即开始预提取。
  • 将 QUIC 提示与连接预热配对:在启动 preconnect()prefetchUrlAsync() 或标准导航之前调用 addQuicHints(),以确保 WebView 尝试使用 HTTP/3 建立连接。
  • 集成策略:如果您预渲染预提取缓存中已有的网址,则预渲染导航会从该缓存中提供,从而避免冗余的网络请求。
  • 监控配额:预渲染会消耗大量资源。最好为多个可能的候选对象预提取,并为最有可能的导航保留预渲染。
  • 架构支持:确保所有网址都使用强制性 HTTPS 架构。无效的方案或 null 输入会触发同步 IllegalArgumentException

其他资源

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