Упреждающая загрузка в WebView

Задержка навигации – важный показатель удобства сайта для пользователей. Чтобы помочь разработчикам уменьшить задержку, WebView предоставляет API для упреждающей загрузки и оптимизации подключения, позволяющие приложению получать или отображать контент до того, как пользователь явно перейдет к нему.

WebView поддерживает три основных типа упреждающей загрузки: предварительное подключение, предварительная выборка и предварительная отрисовка. Кроме того, используются подсказки QUIC для оптимизации согласования протокола.

Реализовав стратегию упреждающей загрузки, вы сможете:

  • Значительное сокращение задержки при загрузке веб-контента. Перенесите время запуска сети на более ранний этап жизненного цикла приложения и используйте более быстрые протоколы, например HTTP/3.
  • Более высокая доля успешных переходов. Предварительная инициализация сети и кеша снижает вероятность сбоев при переходах из-за временных проблем с сетью.
  • Более высокая скорость отклика. Предварительная отрисовка позволяет выполнять мгновенные переходы, благодаря чему приложение работает значительно быстрее.

Как выбрать стратегию упреждающей загрузки

Основное различие между этими стратегиями заключается в их области действия: API подсказок для предварительного подключения и QUIC основаны на источнике, а это значит, что им требуется только целевой домен. API предварительной выборки и предварительной отрисовки основаны на URL, а значит, требуют точного пути к веб-странице.

Поскольку подсказки Preconnect и QUIC работают на уровне источника, вы можете вызывать их до того, как узнаете, какой контент или страницу посетит пользователь.

В таблице ниже сравниваются эти три стратегии, чтобы помочь вам выбрать подходящую для вашего варианта использования:

Функция Предварительное подключение Упреждающий запрос Prerender (Предварительная обработка)
Основная цель Как подготовить подключение Кэшировать только 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()
Использование ресурсов Низкий (сеть) Средний (сеть, память) Высокая (ЦП, память, сеть)
Когда использовать Если целевой источник известен, но URL ещё не определен. Когда известен точный URL и вероятна навигация с общим кешированием в WebViews. Если точный URL известен и переход в определенном WebView почти гарантирован.
Преимущества Более быстрое установление соединения для любого URL в источнике. Ускоренная загрузка сети для подходящих URL Мгновенная навигация после активации

Предварительное подключение к источникам

Предварительное подключение ускоряет загрузку в будущем, поскольку позволяет заранее выполнять DNS-запросы и подтверждения TCP/TLS или QUIC для указанного источника.

В отличие от предзагрузки и предварительной отрисовки, для которых требуется точный целевой URL, предварительное подключение основано исключительно на источнике. Это позволяет вызывать Preconnect до того, как станет известен точный URL. Советы о том, когда вызывать этот метод, приведены в разделе Реализация.

Эта стратегия на уровне профиля с низким потреблением ресурсов уменьшает начальную задержку для любого общего доступа к WebView, если профиль ещё не посещался. Подключение остается открытым примерно 30 секунд, что позволяет избежать избыточных рукопожатий при последующих междоменных HTTP-запросах, переходах или запросах к вложенным ресурсам. Результат DNS-запроса кешируется дольше, чем длится подключение, поэтому последующие запросы к источнику могут по-прежнему пропускать поиск после закрытия подключения.

Реализация

Чтобы инициировать предварительное подключение, вызовите preconnect(String url) на экземпляре Profile. Этот 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
  }

Как указать поддержку протокола QUIC с помощью подсказок QUIC

Протокол HTTP/3 (который работает на основе транспортного протокола QUIC) обеспечивает значительное снижение задержки по сравнению с HTTP/2, в том числе благодаря рукопожатию 0-RTT, улучшенной устойчивости соединения и устранению блокировки начала очереди при потере пакетов.

По умолчанию WebView пытается установить подключение QUIC, только если у него есть информация о том, что источник поддерживает QUIC, например из заголовка Alt-Svc или записи DNS HTTPS из предыдущего взаимодействия. Без этих предварительных знаний WebView использует HTTP/2 или HTTP/1.1 для первоначального подключения.

При вызове addQuicHints эта информация о поддержке протокола заполняется автоматически, что позволяет WebView подключаться с помощью QUIC сразу при первом подключении к указанным источникам.

Предварительное подключение с помощью подсказок QUIC

Хотя подсказки Preconnect и QUIC являются оптимизациями на уровне источника на Profile, они выполняют разные, дополняющие друг друга роли:

  • preconnect Активно открывает и поддерживает сетевое подключение (поиск DNS и подтверждение TCP/TLS или QUIC) в течение примерно 30 секунд. Поскольку он поддерживает активные сетевые подключения, он потребляет ресурсы устройства и сети и должен использоваться только для целевых источников с высокой вероятностью.
  • addQuicHints не приводит к немедленному сетевому трафику. Он обновляет свойства сервера в памяти сетевого стека Profile, чтобы записать поддержку протокола. Поскольку накладные расходы на QUIC незначительны, вы можете безопасно настроить подсказки QUIC во время запуска приложения для всех известных источников, поддерживающих HTTP/3.

Чтобы тег работал эффективно, вызывайте addQuicHints до preconnect, prefetchUrlAsync или loadUrl. Это гарантирует, что все последующие подключения или запросы страниц будут с самого начала использовать HTTP/3.

Реализация

Чтобы настроить подсказки QUIC, вызовите метод addQuicHints(Set<String> urls) для экземпляра Profile. Вызывать этот API нужно в потоке UI, а также убедиться, что WebView поддерживает функцию WebViewFeature.ADD_QUIC_HINTS_V1.

Как и preconnect, addQuicHints работает с источниками, но можно указать полные URL (например, 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
}

Распространенная конфигурация: PrefetchParameters и PrerenderParameters

Для предварительной выборки и предварительной отрисовки используются теги PrefetchParameters или PrerenderParameters, чтобы настроить запрос. Эти классы позволяют добавлять заголовки и подсказки для сопоставления URL, например конфигурации 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-ресурс URL и сохраняет его в сетевом кеше профиля. В WebView Profile выступает в качестве контейнера для данных браузера, в том числе файлов cookie, HTTP-кеша и сервисных работников. Поскольку предварительная выборка выполняется на уровне профиля, любой тег WebView, связанный с этим профилем, может использовать кешированный ответ.

Реализация

Чтобы запустить предварительную выборку, вызовите prefetchUrlAsync() в экземпляре Profile. Эта операция поддерживает только схему 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. Фаза навигации (активация пользователем)

Когда приложение явно переходит по URL (например, с помощью WebViewCompat.navigate или loadUrl) или пользователь нажимает на соответствующую ссылку, WebView определяет, можно ли использовать предварительно загруженный кеш:

  • Оценка основного HTML-кода. В этот момент WebView активирует shouldInterceptRequest() для основного HTML-кода. Чтобы страница была успешно загружена из кеша предварительной выборки, ваш перехватчик должен вернуть значение null. Если вы вернете пользовательский объект WebResourceResponse, WebView будет учитывать ваш перехватчик и полностью обойдет кеш предварительной выборки.

  • Оценка дополнительных ресурсов. После того как предварительно загруженный HTML-код будет готов к использованию, для всех дополнительных ресурсов (например, изображений, скриптов и CSS), необходимых для завершения отрисовки страницы, обычно запускается shouldInterceptRequest().

Основные действия

Ниже описаны операционные характеристики и проверки соответствия требованиям, которые определяют, как WebView инициирует и управляет запросами на предварительную выборку.

  • Безопасность потоков. Запросы можно инициировать из любого потока.
  • Соответствие требованиям. Перед тем как начать выборку, WebView проверяет, безопасен ли запрос и соответствует ли он контексту. Для этого выполняются следующие проверки:
    • Существующие файлы cookie. Чтобы защитить конфиденциальность пользователей и предотвратить побочные эффекты, похожие на CSRF, WebView может пропустить предварительную выборку, если для запроса требуются определенные файлы cookie с аутентификацией, которые могут вызвать изменение состояния на сервере.
    • Наличие сервис-воркера. Если сервис-воркер уже управляет областью действия URL, WebView может передать управление обработчику запросов сервис-воркера, а не инициировать стандартную предзагрузку сети.
    • Доступность прокси. WebView проверяет, стабилен ли текущий сетевой путь (включая все настроенные прокси-серверы), чтобы избежать сбоев при выполнении спекулятивных запросов в сложных сетевых конфигурациях.
  • Если предварительная выборка не запускается (даже при наличии действительных параметров), это часто происходит потому, что WebView определил, что фоновый запрос может помешать текущему сеансу пользователя или нарушить безопасность.
  • Отмена. Используйте CancellationSignal, чтобы отменить выполняющийся запрос и предотвратить его кеширование.

Предварительная отрисовка страниц

Предварительная отрисовка создает скрытый веб-контент, чтобы полностью отрисовать страницу в фоновом режиме, включая выполнение скриптов и получение дополнительных ресурсов. Предварительная отрисовка использует ту же инфраструктуру, что и предварительная выборка. Если приложение инициирует предварительную отрисовку, WebView сначала выполняет предварительную выборку ответа, чтобы выполнить переход с предварительной отрисовкой, избегая избыточной сетевой активности.

Реализация

Предварительная отрисовка выполняется на уровне экземпляра WebView. Вызовите prerenderUrlAsync() с помощью WebViewCompat из потока 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).
    }
});

Предварительная загрузка и предварительная отрисовка полностью асинхронны. prefetchUrlAsync() можно вызывать из любого потока, а prerenderUrlAsync() – только из потока UI.

Технические ограничения

Чтобы обеспечить баланс между мгновенной навигацией и работоспособностью системы, WebView применяет следующие ограничения времени выполнения:

  • Нехватка памяти. WebView отменяет предварительно отрисованные URL, если на устройстве мало оперативной памяти.
  • Запрещенные API. Любые попытки JavaScript получить доступ к определенным API (например, для воспроизведения аудио или показа оповещений) в фоновом режиме приведут к немедленному прекращению предварительной отрисовки.
  • Ограничение на количество экземпляров. В WebView можно использовать только определенное количество активных URL, предварительно обработанных для отрисовки.

Сопоставление URL и заголовок No-Vary-Search (NVS)

Для WebView требуется надежный алгоритм сопоставления, чтобы предварительно загруженный ресурс показывался только при переходе, для которого он предназначен.

Точное соответствие и соответствие по названию

По умолчанию для предварительной выборки и предварительной отрисовки требуется точное соответствие URL. Если URL, на который выполнен переход, идентичен предварительно загруженному URL, он сразу же загружается из кеша. Если параметры запроса различаются, WebView использует следующие правила No-Vary-Search (NVS):

  • Подсказка. Разработчики предоставляют подсказку setExpectedNoVarySearchHeader() во время инициации. Если URL перехода совпадает с URL запроса без параметров подсказки, WebView ненадолго блокируется, чтобы дождаться фактических заголовков сервера.
  • Заголовок сервера. Заголовок ответа NVS от сервера является окончательным. Если сервер подтверждает, что различия в запросах можно игнорировать, показ выполняется из кеша. Если нет, WebView возвращается к холодной загрузке сети.

Директива No-Vary-Search (NVS) предназначена для опытных пользователей. Большинству разработчиков она не нужна, поскольку они передают один и тот же URL как для предзагрузки, так и для перехода (WebViewCompat.navigate или loadUrl). Эти инструкции необходимы только в том случае, если параметры запроса в URL предзагрузки и 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 (таких как воспроизведение аудио).

Стратегии оптимизации

Чтобы максимально эффективно использовать спекулятивную загрузку и при этом экономить системные ресурсы, следуйте приведенным ниже рекомендациям.

  • Начинайте заранее. Начните предварительную загрузку при запуске приложения или как только станет известно вероятное место назначения.
  • Используйте подсказки QUIC вместе с предварительным прогревом подключения. Вызывайте addQuicHints() до инициализации preconnect(), prefetchUrlAsync() или стандартной навигации, чтобы WebView пытался установить соединения с помощью HTTP/3.
  • Интегрированная стратегия. Если вы предварительно отрисовываете URL, который уже есть в кеше предварительной выборки, навигация с предварительной отрисовкой будет выполняться из этого кеша, что позволит избежать лишних сетевых запросов.
  • Отслеживайте квоты. Предварительная отрисовка требует много ресурсов. Используйте предварительную выборку для нескольких вероятных вариантов и предварительную отрисовку для наиболее вероятного перехода.
  • Поддержка схемы. Убедитесь, что все URL используют обязательную схему HTTPS. Недействительные схемы или нулевые входные данные вызывают синхронный вызов IllegalArgumentException.

Дополнительные ресурсы

Чтобы узнать больше об отладке веб-приложений, оптимизации производительности WebView при запуске и обработке завершения процесса отрисовки, ознакомьтесь со следующими ресурсами: