Ładowanie spekulacyjne w WebView

Opóźnienie nawigacji to kluczowy wskaźnik wrażeń użytkownika. Aby pomóc deweloperom zmniejszyć to opóźnienie, WebView udostępnia interfejsy API do ładowania spekulacyjnego, dzięki czemu aplikacja może pobierać lub renderować treści, zanim użytkownik do nich przejdzie.

Komponent WebView obsługuje 3 główne rodzaje ładowania spekulacyjnego: Preconnect, Prefetch i Prerender.

Wdrażając strategię ładowania spekulacyjnego, możesz osiągnąć następujące korzyści:

  • Znaczne skrócenie czasu oczekiwania na wczytanie treści internetowych: przenieś czas rozpoczęcia działania sieci na wcześniejszy etap cyklu życia aplikacji.
  • Wyższy odsetek udanych nawigacji: dzięki wstępnemu rozgrzaniu sieci i pamięci podręcznej nawigacje rzadziej kończą się niepowodzeniem z powodu przejściowych problemów z siecią.
  • Lepsza postrzegana szybkość reakcji: renderowanie wstępne umożliwia natychmiastowe przejścia, dzięki czemu aplikacja wydaje się znacznie szybsza.

Wybieranie strategii ładowania spekulacyjnego

Kluczowa różnica między tymi strategiami polega na ich zakresie: interfejs Preconnect API jest oparty na źródle, co oznacza, że wymaga tylko domeny docelowej. Interfejsy API pobierania z wyprzedzeniem i renderowania z wyprzedzeniem są oparte na adresach URL, co oznacza, że wymagają dokładnej ścieżki strony internetowej.

Funkcja Preconnect działa na poziomie źródła, więc można ją uruchomić znacznie wcześniej w cyklu życia aplikacji, nawet zanim poznasz konkretne treści lub stronę, do której przejdzie użytkownik.

Poniższa tabela porównuje te 3 strategie, aby pomóc Ci wybrać odpowiednią dla Twojego przypadku:

Funkcja Preconnect Pobieranie z wyprzedzeniem Prerender
Cel główny Rozgrzej połączenie Pamięć podręczna tylko dla HTML (bez JavaScriptu ani CSS) Wstępne renderowanie całej strony
Zakres Poziom profilu (udostępniany w widokach WebView) Poziom profilu (udostępniany w widokach WebView) Poziom WebView (powiązany z określonym widokiem WebView)
Jetpack WebKit API androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
Podstawowe metody interfejsu API preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
Konfiguracja Nie dotyczy PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
Wykorzystanie zasobów Niska (sieć) Średnie (sieć, pamięć) Wysokie (procesor, pamięć, sieć)
Kiedy używać Gdy znane jest źródło docelowe, ale konkretny adres URL nie został jeszcze określony. Gdy dokładny adres URL jest znany, a nawigacja jest prawdopodobna, z pamięcią podręczną udostępnianą w widokach WebView. Gdy dokładny adres URL jest znany, a nawigacja w określonym widoku WebView jest bardzo pewna.
Zalety Szybsze konfigurowanie połączenia z dowolnym adresem URL w domenie źródłowej Szybsze wczytywanie sieci w przypadku pasujących adresów URL Natychmiastowa nawigacja po aktywacji

Łączenie się ze źródłami w trybie preconnect

Wstępne nawiązywanie połączeń przyspiesza przyszłe ładowanie, ponieważ z wyprzedzeniem wykonuje wyszukiwanie DNS i uzgadnianie połączeń TCP/TLS dla określonego punktu początkowego.

W odróżnieniu od pobierania z wyprzedzeniem i wstępnego renderowania, które wymagają dokładnego docelowego adresu URL, wstępne nawiązywanie połączenia jest oparte wyłącznie na źródle. Dzięki temu możesz wykonać połączenie wstępne znacznie wcześniej niż pobieranie wstępne i wstępne renderowanie.

Ta strategia na poziomie profilu, która nie wymaga dużych zasobów, zmniejsza początkowe opóźnienie w przypadku udostępniania dowolnego elementu WebView w tym profilu, pod warunkiem że źródło nie zostało jeszcze odwiedzone. Połączenie pozostaje otwarte przez około 30 sekund, co ułatwia kolejne żądania HTTP z innych domen, nawigacje i zasoby podrzędne, ponieważ eliminuje narzut związany z uzgadnianiem połączenia.

Implementacja

Aby zainicjować wstępne połączenie, zadzwoń pod numer preconnect(String url) na instancji Profile. Ten interfejs API musi być wywoływany w wątku UI i wymaga obsługi WebViewFeature.PRECONNECT.

Interfejs API działa na źródle, ale dla wygody można podać pełny adres URL (np. https://www.example.com/index.html). Jest on automatycznie traktowany jako wywołanie źródła (np. https://www.example.com). Z wieloma źródłami można się połączyć, wywołując ten interfejs API wiele razy.

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
}

Typowa konfiguracja: PrefetchParametersPrerenderParameters

Zarówno w przypadku wstępnego pobierania, jak i wstępnego renderowania używa się PrefetchParameters lub PrerenderParameters do dostosowywania żądania. Te klasy umożliwiają podawanie dodatkowych nagłówków i wskazówek dotyczących dopasowywania adresów URL, np. konfiguracji 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();

Pobieranie treści z wyprzedzeniem

Wstępne pobieranie polega na pobraniu głównego zasobu HTML adresu URL i zapisaniu go w pamięci podręcznej sieci profilu. W komponencie WebView obiekt Profile pełni funkcję kontenera danych przeglądarki, w tym plików cookie, pamięci podręcznej HTTP i procesów roboczych usługi. Prefetch to operacja na poziomie profilu, więc każda WebView powiązana z tym profilem może korzystać z odpowiedzi zapisanej w pamięci podręcznej.

Implementacja

Aby zainicjować pobieranie wstępne, wywołaj funkcję prefetchUrlAsync() w instancji Profile. Ta operacja obsługuje tylko schemat 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
            }
        }
    }
);

Cykl życia przechwytywania

Prośba o wstępne pobranie WebView zmienia czas i sposób wywoływania shouldInterceptRequest() wywołania zwrotnego. Ma to bezpośredni wpływ na to, czy wstępnie pobrane treści zostaną użyte, dlatego ważne jest, aby poznać 2-etapowy cykl życia:

Diagram przedstawiający dwuetapowy cykl życia przechwytywania wstępnego pobierania WebView podczas fazy spekulacyjnej i nawigacji.
Rysunek 1. Dwuetapowy cykl życia przechwytywania w przypadku żądań wstępnego pobierania i nawigacji w WebView.

1. Faza spekulacyjna (żądanie pobierania z wyprzedzeniem)

Gdy wywoływana jest funkcja prefetchUrlAsync(), WebView pobiera w tle główny zasób HTML. W przypadku tego żądania w tle shouldInterceptRequest() jest całkowicie pomijany. Żadna niestandardowa logika, tokeny autoryzacji ani wstrzykiwanie nagłówków, które są zwykle obsługiwane w interceptorze, nie są stosowane do wstępnie pobranego zasobu HTML.

2. Faza nawigacji (aktywacja użytkownika)

Gdy aplikacja wyraźnie przechodzi do adresu URL (np. za pomocą funkcji loadUrl()) lub użytkownik kliknie pasujący link, WebView sprawdza, czy może użyć wstępnie pobranej pamięci podręcznej:

  • Ocena głównego kodu HTML: w tym momencie WebView wywoła shouldInterceptRequest() dla głównego kodu HTML. Aby strona została wyświetlona z pamięci podręcznej wstępnego pobierania, przechwytujący musi zwrócić wartość null. Jeśli zwrócisz niestandardowyWebResourceResponse, WebView uwzględni Twój interfejs i całkowicie pominie pamięć podręczną wstępnego pobierania.

  • Ocena zasobów podrzędnych: gdy wstępnie pobrany kod HTML zostanie dopuszczony do użycia,shouldInterceptRequest() zwykle uruchamia się w przypadku wszystkich kolejnych zasobów podrzędnych (takich jak obrazy, skrypty i arkusze CSS) wymaganych do zakończenia renderowania strony.

Najważniejsze zachowania

Sposób inicjowania i zarządzania żądaniami wstępnego pobierania przez WebView zależy od tych cech operacyjnych i sprawdzania kryteriów:

  • Bezpieczeństwo wątków: żądania można inicjować z dowolnego wątku.
  • Kryteria kwalifikacji: przed rozpoczęciem pobierania WebView sprawdza, czy żądanie jest bezpieczne i odpowiednie w kontekście, wykonując te czynności:
    • Istniejące pliki cookie: aby chronić prywatność użytkowników i zapobiegać efektom ubocznym podobnym do CSRF, WebView może pominąć wstępne pobieranie, jeśli żądanie wymaga określonych uwierzytelnionych plików cookie, które mogą spowodować zmianę stanu na serwerze.
    • Obecność skryptu service worker: jeśli skrypt service worker kontroluje już zakres adresu URL, WebView może przekazać żądanie do jego modułu obsługi pobierania zamiast inicjować standardowe pobieranie z wyprzedzeniem z sieci.
    • Dostępność serwera proxy: komponent WebView sprawdza, czy bieżąca ścieżka sieciowa (w tym skonfigurowane serwery proxy) jest stabilna, aby uniknąć niepowodzenia spekulacyjnych żądań w przypadku złożonych konfiguracji sieci.
  • Jeśli wstępne pobieranie nie rozpocznie się (nawet z prawidłowymi parametrami), często dzieje się tak, ponieważ komponent WebView uznał, że żądanie w tle może zakłócić bieżącą sesję użytkownika lub stan zabezpieczeń.
  • Anulowanie: użyj CancellationSignal, aby zakończyć żądanie w trakcie realizacji i zapobiec jego zapisaniu w pamięci podręcznej.

Renderowanie wstępne stron

Renderowanie wstępne tworzy ukryte „treści z internetu”, aby w pełni renderować stronę w tle, w tym wykonywać skrypty i pobierać zasoby podrzędne. Wstępne renderowanie korzysta z tej samej infrastruktury co wstępne pobieranie. Jeśli aplikacja zainicjuje wstępne renderowanie, komponent WebView najpierw pobierze wstępnie odpowiedź, aby obsłużyć nawigację wstępnego renderowania, unikając zbędnej aktywności sieciowej.

Implementacja

Wstępne renderowanie to operacja na poziomie instancji WebView. Wywołaj funkcję prerenderUrlAsync() za pomocą funkcji WebViewCompat z wątku UI.

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

Zarówno pobieranie z wyprzedzeniem, jak i wstępne renderowanie są w pełni asynchroniczne. prefetchUrlAsync() można wywołać z dowolnego wątku, a prerenderUrlAsync() musi być zainicjowany z wątku UI.

Ograniczenia techniczne

Aby zachować równowagę między natychmiastową nawigacją a stanem systemu, WebView wymusza te ograniczenia w czasie działania:

  • Niski poziom pamięci: WebView anuluje wstępnie renderowane adresy URL, jeśli urządzenie ma mało pamięci RAM.
  • Niedozwolone interfejsy API: wszelkie próby uzyskania przez JavaScript dostępu do niektórych interfejsów API (np. odtwarzania dźwięku czy alertów) w kontekście tła spowodują natychmiastowe zakończenie wstępnego renderowania.
  • Limit instancji: w przypadku każdego elementu WebView obowiązuje limit aktywnych wstępnie renderowanych adresów URL.

Dopasowywanie adresów URL i No-Vary-Search (NVS)

WebView wymaga niezawodnego algorytmu dopasowywania, aby mieć pewność, że wstępnie załadowany zasób jest wyświetlany tylko w przypadku zamierzonej nawigacji.

Dopasowanie ścisłe a dopasowanie NVS

Domyślnie wstępne pobieranie i wstępne renderowanie wymagają dokładnego dopasowania adresu URL. Jeśli adres URL, do którego nastąpiło przekierowanie, jest identyczny z wstępnie załadowanym adresem URL, jest on od razu wyświetlany z pamięci podręcznej. Jeśli parametry zapytania są różne, komponent WebView stosuje te reguły No-Vary-Search (NVS):

  • Wskazówka: deweloperzy podają setExpectedNoVarySearchHeader() wskazówkę podczas inicjowania. Jeśli adres URL, do którego nastąpiło przejście, pasuje do adresu URL żądania bez parametrów sugerowanych, WebView na krótko blokuje działanie, aby poczekać na rzeczywiste nagłówki serwera.
  • Nagłówek serwera: nagłówek odpowiedzi NVS z serwera jest ostatecznym źródłem informacji. Jeśli serwer potwierdzi, że różnice w zapytaniu należy zignorować, odpowiedź zostanie pobrana z pamięci podręcznej. Jeśli nie, WebView wraca do zimnego wczytywania sieci.

Dyrektywa No-Vary-Search (NVS) jest przeznaczona do zaawansowanego użytku i większość deweloperów może jej nie potrzebować, ponieważ przekazują ten sam adres URL zarówno do pobierania z wyprzedzeniem, jak i do loadUrl(). Te wskazówki są potrzebne tylko wtedy, gdy parametry zapytania różnią się między adresem URL pobierania z wyprzedzeniem a loadUrl() adresem URL.

Konfiguracja globalna

Dostosuj działanie ładowania spekulacyjnego na poziomie profilu, konfigurując PrefetchCachelimity i maksymalną liczbę wstępnych renderowań. Możesz też przywrócić niestandardowe limity wstępnego pobierania do ustawień domyślnych:

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

Obsługa błędów i wyjątków

Operacje spekulacyjne używają symbolu OutcomeReceiverCompat lub PrerenderOperationCallback do raportowania wyników.

Wyjątki podstawowe

Gdy operacja ładowania spekulacyjnego zakończy się niepowodzeniem, moduł obsługi błędów zgłasza jeden z tych podstawowych typów wyjątków, aby ułatwić Ci diagnozowanie konkretnych scenariuszy niepowodzenia:

  • PrefetchException: klasa bazowa dla wszystkich błędów asynchronicznego pobierania z wyprzedzeniem.
  • PrefetchNetworkException: oznacza błąd na poziomie sieci lub serwera. Może zawierać pole httpStatusCode (np. 404 lub 503), które pomaga w diagnozowaniu problemów po stronie serwera.
  • PrerenderException: klasa nadrzędna wszystkich błędów związanych z wstępnym renderowaniem, np. błędów spowodowanych brakiem pamięci lub użyciem w tle niedozwolonych interfejsów API (np. odtwarzania dźwięku).

Strategie optymalizacji

Aby zmaksymalizować korzyści wynikające z ładowania spekulacyjnego, a jednocześnie oszczędzać zasoby systemu, postępuj zgodnie z tymi zaleceniami:

  • Rozpocznij wcześnie: rozpocznij wstępne pobieranie podczas uruchamiania aplikacji lub gdy tylko pojawi się prawdopodobieństwo, że użytkownik przejdzie do miejsca docelowego nawigacji.
  • Strategia zintegrowana: jeśli wstępnie renderujesz adres URL, który jest już w pamięci podręcznej wstępnego pobierania, nawigacja wstępnego renderowania jest obsługiwana z tej pamięci podręcznej, co pozwala uniknąć zbędnych żądań sieciowych.
  • Monitoruj limity: wstępne renderowanie jest zasobożerne. W przypadku kilku prawdopodobnych kandydatów preferuj wstępne pobieranie, a wstępne renderowanie rezerwuj dla najbardziej prawdopodobnej nawigacji.
  • Obsługa schematu: upewnij się, że wszystkie adresy URL używają obowiązkowego schematu HTTPS. Nieprawidłowe schematy lub puste dane wejściowe wywołują synchroniczny błąd IllegalArgumentException.

Dodatkowe materiały

Więcej informacji o debugowaniu aplikacji internetowych, optymalizowaniu wydajności uruchamiania komponentu WebView i obsłudze zakończenia procesu renderowania znajdziesz w tych materiałach: