Tính năng tải suy đoán trong WebView

Độ trễ khi điều hướng là một chỉ số quan trọng đối với trải nghiệm người dùng. Để giúp nhà phát triển giảm độ trễ này, WebView cung cấp các API để tải suy đoán, cho phép ứng dụng của bạn tìm nạp hoặc hiển thị nội dung trước khi người dùng chuyển đến nội dung đó một cách rõ ràng.

WebView hỗ trợ 3 loại tải suy đoán chính: Kết nối trước, Tìm nạp trước và Kết xuất trước.

Bằng cách triển khai chiến lược tải suy đoán, bạn có thể đạt được những điều sau:

  • Giảm đáng kể độ trễ khi tải nội dung web: Chuyển thời gian bắt đầu mạng sang thời điểm sớm hơn trong vòng đời của ứng dụng.
  • Tỷ lệ điều hướng thành công cao hơn: Bằng cách làm nóng trước mạng và bộ nhớ đệm, các thao tác điều hướng ít có khả năng thất bại do các vấn đề mạng tạm thời.
  • Cải thiện khả năng phản hồi theo cảm nhận: Đặc biệt, việc kết xuất trước cho phép các hiệu ứng chuyển cảnh diễn ra tức thì, giúp ứng dụng có cảm giác nhanh hơn đáng kể.

Chọn một chiến lược tải suy đoán

Điểm khác biệt chính giữa các chiến lược này nằm ở phạm vi của chúng: Preconnect API dựa trên nguồn gốc, tức là chỉ yêu cầu miền mục tiêu. API tìm nạp trước và kết xuất trước dựa trên URL, tức là chúng yêu cầu đường dẫn chính xác của trang web.

Vì Preconnect hoạt động ở cấp nguồn gốc, nên có thể được khởi tạo sớm hơn nhiều trong vòng đời của ứng dụng, ngay cả trước khi bạn biết nội dung hoặc trang cụ thể mà người dùng sẽ chuyển đến.

Bảng sau đây so sánh 3 chiến lược này để giúp bạn chọn chiến lược phù hợp cho trường hợp sử dụng của mình:

Tính năng Kết nối trước Tìm nạp trước Kết xuất trước
Mục tiêu chính Khởi động kết nối Chỉ lưu vào bộ nhớ đệm HTML (không có JavaScript hoặc CSS) Kết xuất trước toàn bộ trang
Phạm vi Cấp độ hồ sơ (được chia sẻ trên các WebView) Cấp độ hồ sơ (được chia sẻ trên các WebView) Cấp độ WebView (được liên kết với một WebView cụ thể)
Jetpack WebKit API androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
Các phương thức API cốt lõi preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
Configuration Không áp dụng PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
Mức sử dụng tài nguyên Thấp (mạng) Trung bình (mạng, bộ nhớ) Cao (CPU, bộ nhớ, mạng)
Thời điểm sử dụng Khi bạn biết nguồn gốc mục tiêu nhưng chưa xác định được URL cụ thể. Khi biết chính xác URL và có khả năng điều hướng, với bộ nhớ đệm được chia sẻ trên các WebView. Khi bạn biết chính xác URL và chắc chắn điều hướng trong một WebView cụ thể.
Lợi ích Thiết lập kết nối nhanh hơn cho mọi URL trên nguồn Tải mạng nhanh hơn cho các URL phù hợp Điều hướng thực sự tức thì khi kích hoạt

Kết nối trước với các điểm gốc

Tính năng kết nối trước giúp tăng tốc độ tải trong tương lai bằng cách chủ động thực hiện các hoạt động tra cứu DNS và bắt tay TCP/TLS cho một nguồn gốc cụ thể.

Không giống như Prefetch và Prerender (yêu cầu phải có URL đích chính xác), Preconnect hoàn toàn dựa trên nguồn gốc. Điều này cho phép bạn thực hiện lệnh gọi Preconnect sớm hơn nhiều so với Prefetch và Prerender.

Chiến lược cấp hồ sơ có mức sử dụng tài nguyên thấp này giúp giảm độ trễ ban đầu cho mọi hoạt động chia sẻ WebView của Hồ sơ, miễn là nguồn chưa được truy cập. Kết nối vẫn mở trong khoảng 30 giây, mang lại lợi ích cho mọi yêu cầu HTTP, thao tác điều hướng hoặc tài nguyên phụ trên nhiều nguồn tiếp theo bằng cách loại bỏ chi phí bắt tay.

Triển khai

Để bắt đầu một quy trình kết nối trước, hãy gọi preconnect(String url) trên một thực thể Profile. Bạn phải gọi API này trên luồng giao diện người dùng và cần hỗ trợ WebViewFeature.PRECONNECT.

API này hoạt động trên nguồn gốc, nhưng để thuận tiện, bạn có thể cung cấp một URL đầy đủ (chẳng hạn như https://www.example.com/index.html). URL này sẽ tự động được coi là một lệnh gọi đến nguồn gốc (ví dụ: https://www.example.com). Bạn có thể kết nối nhiều nguồn gốc bằng cách gọi API này nhiều lần.

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
}

Cấu hình phổ biến: PrefetchParametersPrerenderParameters

Cả Prefetch và Prerender đều sử dụng PrefetchParameters hoặc PrerenderParameters để tuỳ chỉnh yêu cầu. Các lớp này cho phép bạn cung cấp tiêu đề và gợi ý bổ sung để so khớp URL, chẳng hạn như cấu hình 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();

Tìm nạp trước nội dung

Việc tìm nạp trước sẽ tải tài nguyên HTML chính của một URL xuống và lưu trữ trong bộ nhớ đệm mạng của hồ sơ. Trong WebView, Profile đóng vai trò là vùng chứa cho dữ liệu trình duyệt, bao gồm cả cookie, bộ nhớ đệm HTTP và các worker dịch vụ. Vì tìm nạp trước là một thao tác ở cấp hồ sơ, nên mọi WebView được liên kết với hồ sơ đó đều có thể tận dụng phản hồi được lưu vào bộ nhớ đệm.

Triển khai

Để bắt đầu tìm nạp trước, hãy gọi prefetchUrlAsync() trên một thực thể Profile. Thao tác này chỉ hỗ trợ giao thức 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
            }
        }
    }
);

Vòng đời chặn

Yêu cầu tìm nạp trước WebView sẽ thay đổi thời điểm và cách thức kích hoạt lệnh gọi lại shouldInterceptRequest(). Vì điều này ảnh hưởng trực tiếp đến việc nội dung được tìm nạp trước có được sử dụng thành công hay không, nên bạn cần hiểu rõ vòng đời gồm 2 bước:

Sơ đồ cho thấy vòng đời chặn tìm nạp trước WebView gồm 2 bước trong các giai đoạn suy đoán và điều hướng.
Hình 1. Vòng đời chặn hai bước cho các yêu cầu và thao tác điều hướng tìm nạp trước WebView.

1. Giai đoạn suy đoán (Yêu cầu tìm nạp trước)

Khi prefetchUrlAsync() được gọi, WebView sẽ tải tài nguyên HTML chính xuống ở chế độ nền. shouldInterceptRequest() hoàn toàn bị bỏ qua đối với yêu cầu nền này. Mọi logic tuỳ chỉnh, mã thông báo uỷ quyền hoặc nội dung chèn tiêu đề thường được xử lý bên trong trình chặn sẽ không được áp dụng cho tài nguyên HTML được tìm nạp trước.

2. Giai đoạn điều hướng (kích hoạt người dùng)

Khi ứng dụng điều hướng rõ ràng đến URL (ví dụ: sử dụng loadUrl()) hoặc người dùng nhấp vào một đường liên kết phù hợp, WebView sẽ xác định xem có thể sử dụng bộ nhớ đệm tìm nạp trước hay không:

  • Đánh giá HTML chính: WebView sẽ kích hoạt shouldInterceptRequest() cho HTML chính tại thời điểm này. Để phân phát thành công trang từ bộ nhớ đệm tìm nạp trước, trình chặn của bạn phải trả về null. Nếu bạn trả về một WebResourceResponse tuỳ chỉnh, WebView sẽ tuân theo trình chặn của bạn và hoàn toàn bỏ qua bộ nhớ đệm tìm nạp trước.

  • Đánh giá tài nguyên phụ: Sau khi HTML được tìm nạp trước được xoá để sử dụng, shouldInterceptRequest() sẽ kích hoạt bình thường cho tất cả các tài nguyên phụ tiếp theo (chẳng hạn như hình ảnh, tập lệnh và CSS) cần thiết để hoàn tất việc hiển thị trang.

Các hành vi chính

Các đặc điểm hoạt động và quy trình kiểm tra điều kiện sau đây chi phối cách WebView bắt đầu và quản lý các yêu cầu tìm nạp trước:

  • Độ an toàn của luồng: Bạn có thể bắt đầu yêu cầu từ bất kỳ luồng nào.
  • Điều kiện: Trước khi bắt đầu tìm nạp, WebView đảm bảo rằng yêu cầu an toàn và phù hợp theo ngữ cảnh bằng cách kiểm tra những điều sau:
    • Cookie hiện có: Để bảo vệ quyền riêng tư của người dùng và ngăn chặn các tác dụng phụ tương tự như CSRF, WebView có thể bỏ qua việc tìm nạp trước nếu yêu cầu đòi hỏi các cookie được xác thực cụ thể có thể kích hoạt thay đổi trạng thái trên máy chủ.
    • Sự hiện diện của trình chạy dịch vụ: Nếu một Trình chạy dịch vụ đã kiểm soát phạm vi của URL, WebView có thể chuyển sang trình xử lý tìm nạp của Trình chạy dịch vụ thay vì bắt đầu tìm nạp trước mạng tiêu chuẩn.
    • Tính sẵn có của proxy: WebView xác minh rằng đường dẫn mạng hiện tại (bao gồm mọi proxy đã định cấu hình) ổn định để tránh thất bại trong các yêu cầu suy đoán trong cấu hình mạng phức tạp.
  • Nếu một hoạt động tìm nạp trước không bắt đầu được (ngay cả khi có các tham số hợp lệ), thì thường là do WebView đã xác định rằng một yêu cầu ở chế độ nền có thể gây trở ngại cho phiên hiện tại hoặc trạng thái bảo mật của người dùng.
  • Huỷ: Sử dụng CancellationSignal để chấm dứt một yêu cầu đang diễn ra và ngăn yêu cầu đó được lưu vào bộ nhớ đệm.

Kết xuất trước các trang

Kết xuất trước sẽ tạo ra "nội dung web" ẩn để kết xuất đầy đủ một trang ở chế độ nền, bao gồm cả việc thực thi tập lệnh và tìm nạp tài nguyên phụ. Kết xuất trước dựa trên cùng cơ sở hạ tầng cơ bản như tìm nạp trước. Nếu một ứng dụng bắt đầu quá trình kết xuất trước, WebView sẽ thực hiện tìm nạp trước phản hồi để phân phát hoạt động điều hướng kết xuất trước, tránh hoạt động mạng dư thừa.

Triển khai

Kết xuất trước là một thao tác ở cấp phiên bản WebView. Gọi prerenderUrlAsync() bằng WebViewCompat từ luồng giao diện người dùng.

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).
    }
});

Cả tính năng tìm nạp trước và kết xuất trước đều hoàn toàn không đồng bộ. Bạn có thể gọi prefetchUrlAsync() từ bất kỳ luồng nào, trong khi prerenderUrlAsync() phải được bắt đầu từ luồng giao diện người dùng.

Các hạn chế về kỹ thuật

Để cân bằng giữa hoạt động điều hướng tức thì và tình trạng hệ thống, WebView sẽ thực thi các ràng buộc sau đây trong thời gian chạy:

  • Áp lực bộ nhớ: WebView huỷ các URL được kết xuất trước nếu thiết bị có ít RAM.
  • Các API không được phép: Mọi nỗ lực của JavaScript để truy cập vào một số API (ví dụ: phát âm thanh, cảnh báo) trong bối cảnh nền sẽ ngay lập tức chấm dứt quá trình kết xuất trước.
  • Giới hạn số phiên bản: Có giới hạn về số lượng URL được kết xuất trước đang hoạt động được phép cho mỗi WebView.

So khớp URL và No-Vary-Search (NVS)

WebView yêu cầu một thuật toán so khớp đáng tin cậy để đảm bảo rằng tài nguyên được tải sẵn chỉ được phân phát cho hoạt động điều hướng dự kiến.

Kiểu khớp chính xác so với kiểu khớp NVS

Theo mặc định, tính năng tìm nạp trước và kết xuất trước yêu cầu phải có URL khớp chính xác. Nếu URL được chuyển hướng giống hệt với URL được tải trước, thì URL đó sẽ được phân phát ngay lập tức từ bộ nhớ đệm. Nếu các tham số truy vấn khác nhau, WebView sẽ sử dụng các quy tắc No-Vary-Search (NVS) sau đây:

  • Gợi ý: Nhà phát triển cung cấp một gợi ý setExpectedNoVarySearchHeader() trong quá trình khởi tạo. Nếu URL được điều hướng khớp với URL yêu cầu trừ đi các tham số được gợi ý, WebView sẽ chặn trong thời gian ngắn để chờ tiêu đề thực tế của máy chủ.
  • Tiêu đề máy chủ: Tiêu đề phản hồi NVS từ máy chủ là cơ quan có thẩm quyền tối cao. Nếu máy chủ xác nhận rằng bạn nên bỏ qua các điểm khác biệt trong truy vấn, thì kết quả trùng khớp sẽ được phân phát từ bộ nhớ đệm. Nếu không, WebView sẽ quay lại tải mạng lạnh.

No-Vary-Search (NVS) là để sử dụng nâng cao và hầu hết nhà phát triển có thể không cần đến NVS vì họ truyền chính xác cùng một URL cho cả tìm nạp trước và loadUrl(). Hướng dẫn này chỉ cần thiết nếu có sự khác biệt về tham số truy vấn giữa URL tìm nạp trước và URL loadUrl().

Cấu hình chung

Điều chỉnh hành vi tải suy đoán ở cấp hồ sơ bằng cách định cấu hình giới hạn PrefetchCache và số lượng kết xuất trước tối đa. Bạn cũng có thể đặt lại các giới hạn tìm nạp trước tuỳ chỉnh về giá trị mặc định của hệ thống:

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

Xử lý lỗi và ngoại lệ

Các thao tác suy đoán sử dụng OutcomeReceiverCompat hoặc PrerenderOperationCallback để báo cáo kết quả.

Trường hợp ngoại lệ chính

Khi một thao tác tải suy đoán không thành công, trình xử lý lỗi sẽ báo cáo một trong các loại ngoại lệ chính sau đây để giúp bạn chẩn đoán các trường hợp không thành công cụ thể:

  • PrefetchException: Lớp cơ sở cho tất cả các lỗi tìm nạp trước không đồng bộ.
  • PrefetchNetworkException: Cho biết lỗi ở cấp mạng hoặc máy chủ. Bạn có thể thêm trường httpStatusCode (chẳng hạn như 404 hoặc 503) để giúp chẩn đoán các vấn đề phía máy chủ.
  • PrerenderException: Lớp cha cho tất cả các lỗi liên quan đến việc kết xuất trước, chẳng hạn như lỗi do áp lực bộ nhớ hoặc việc sử dụng các API không được phép (chẳng hạn như phát âm thanh) ở chế độ nền.

Chiến lược tối ưu hoá

Hãy làm theo các đề xuất sau để tối đa hoá lợi ích của tính năng tải suy đoán trong khi vẫn tiết kiệm tài nguyên hệ thống:

  • Bắt đầu sớm: Bắt đầu tìm nạp trước trong quá trình khởi động ứng dụng hoặc ngay khi có khả năng xuất hiện đích đến điều hướng.
  • Chiến lược tích hợp: Nếu bạn kết xuất trước một URL đã có trong bộ nhớ đệm tìm nạp trước, thì thao tác điều hướng kết xuất trước sẽ được phân phát từ bộ nhớ đệm đó, tránh các yêu cầu mạng dư thừa.
  • Giám sát hạn mức: Việc kết xuất trước tốn nhiều tài nguyên. Ưu tiên tìm nạp trước cho một số ứng viên có khả năng và dành trước tính năng kết xuất trước cho một thao tác điều hướng có khả năng xảy ra nhất.
  • Hỗ trợ lược đồ: Đảm bảo tất cả URL đều sử dụng lược đồ HTTPS bắt buộc. Các lược đồ không hợp lệ hoặc đầu vào rỗng sẽ kích hoạt một IllegalArgumentException đồng bộ.

Tài nguyên khác

Để tìm hiểu thêm về cách gỡ lỗi ứng dụng web, tối ưu hoá hiệu suất khởi động WebView và xử lý việc chấm dứt quy trình kết xuất, hãy xem các tài nguyên sau: