Carga especulativa en WebView

La latencia de navegación es una métrica fundamental para la experiencia del usuario. Para ayudar a los desarrolladores a reducir esta latencia, WebView proporciona APIs para la carga especulativa, lo que permite que tu app obtenga o renderice contenido antes de que el usuario navegue explícitamente a él.

WebView admite tres tipos principales de carga especulativa: Preconnect, Prefetch y Prerender.

Si implementas una estrategia de carga especulativa, puedes lograr lo siguiente:

  • Reducción significativa de la latencia de carga de contenido web: Mueve la hora de inicio de la red a un momento anterior del ciclo de vida de la app.
  • Tasas de éxito de navegación más altas: Si se prepara la red y la caché, es menos probable que las navegaciones fallen debido a problemas de red transitorios.
  • Mejor capacidad de respuesta percibida: La renderización previa, en particular, permite transiciones instantáneas que hacen que la app se sienta mucho más rápida.

Elige una estrategia de carga especulativa

La principal diferencia entre estas estrategias radica en su alcance: la API de Preconnect se basa en el origen, lo que significa que solo requiere el dominio de destino. Las APIs de Prefetch y Prerender se basan en la URL, lo que significa que requieren la ruta de acceso exacta de la página web.

Debido a que Preconnect opera a nivel de origen, se puede iniciar mucho antes en el ciclo de vida de la app, incluso antes de que conozcas el contenido o la página específicos a los que navegará el usuario.

En la siguiente tabla, se comparan estas tres estrategias para ayudarte a elegir la adecuada para tu caso de uso:

Función Preconnect Prefetch Prerender
Objetivo principal Preparar la conexión Almacenar en caché solo HTML (sin JavaScript ni CSS) Renderizar previamente toda la página
Alcance Nivel de perfil (compartido en WebViews) Nivel de perfil (compartido en WebViews) Nivel de WebView (vinculado a una WebView específica)
API de Jetpack WebKit androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
Métodos principales de la API preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
Configuration N/A PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
Uso de recursos Bajo (red) Medio (red, memoria) Alto (CPU, memoria, red)
Cuándo debe usarse Cuando se conoce el origen de destino, pero aún no se determinó la URL específica. Cuando se conoce la URL exacta y es probable que se realice la navegación, con el almacenamiento en caché compartido en WebViews. Cuando se conoce la URL exacta y la navegación es muy segura dentro de una WebView específica.
Beneficios Configuración de conexión más rápida para cualquier URL en el origen Carga de red más rápida para las URLs coincidentes Navegación realmente instantánea tras la activación

Establece una conexión previa con los orígenes

La conexión previa acelera las cargas futuras mediante la realización preventiva de búsquedas de DNS y protocolos de enlace TCP/TLS para un origen especificado.

A diferencia de Prefetch y Prerender, que requieren una URL de destino exacta, Preconnect se basa estrictamente en el origen. Esto te permite realizar la llamada de Preconnect mucho antes que Prefetch y Prerender.

Esta estrategia de bajo consumo de recursos y a nivel de perfil reduce la latencia inicial de cualquier WebView que comparta ese perfil, siempre que el origen aún no se haya visitado. La conexión permanece abierta durante aproximadamente 30 segundos, lo que beneficia las solicitudes HTTP, las navegaciones o los subrecursos de origen cruzado posteriores, ya que elimina la sobrecarga del protocolo de enlace.

Implementación

Para iniciar una conexión previa, llama a preconnect(String url) en una instancia de Profile. Se debe llamar a esta API en el subproceso de IU y requiere que se admita WebViewFeature.PRECONNECT.

La API opera en el origen, pero, para mayor comodidad, se puede proporcionar una URL completa (como https://www.example.com/index.html). Esto se trata automáticamente como una llamada al origen (por ejemplo, https://www.example.com). Se pueden conectar varios orígenes llamando a esta API varias veces.

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
}

Configuración común: PrefetchParameters y PrerenderParameters

Tanto Prefetch como Prerender usan PrefetchParameters o PrerenderParameters para personalizar la solicitud. Estas clases te permiten proporcionar encabezados y sugerencias adicionales para la coincidencia de URLs, como las configuraciones de 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();

Recuperación previa de contenido

La recuperación previa descarga el recurso HTML principal de una URL y lo almacena en la caché de red del perfil. En WebView, un Profile actúa como un contenedor para los datos del navegador, incluidas las cookies, la caché HTTP y los service workers. Debido a que la carga previa es una operación a nivel de perfil, cualquier WebView asociada con ese perfil puede aprovechar la respuesta almacenada en caché.

Implementación

Para iniciar una recuperación previa, llama a prefetchUrlAsync() en una instancia de Profile. Esta operación solo admite el esquema 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
            }
        }
    }
);

Ciclo de vida de la intercepción

La solicitud de recuperación previa de WebView altera cuándo y cómo se activa la devolución de llamada shouldInterceptRequest(). Debido a que esto tiene un impacto directo en si se usa correctamente el contenido recuperado previamente, es fundamental comprender el ciclo de vida de dos pasos:

Diagrama que muestra el ciclo de vida de la interceptación de la recuperación previa de WebView en dos pasos durante las fases especulativa y de navegación.
Figura 1. El ciclo de vida de intercepción de dos pasos para las solicitudes de recuperación previa y las navegaciones de WebView

1. La fase especulativa (solicitud de carga previa)

Cuando se invoca prefetchUrlAsync(), WebView descarga el recurso HTML principal en segundo plano. shouldInterceptRequest() se omite por completo para esta solicitud en segundo plano. No se aplica ninguna lógica personalizada, tokens de autorización ni inyecciones de encabezado que se suelen controlar dentro de tu interceptor al recurso HTML recuperado previamente.

2. La fase de navegación (activación del usuario)

Cuando la app navega explícitamente a la URL (por ejemplo, con WebViewCompat.navigate o loadUrl) o el usuario hace clic en un vínculo coincidente, WebView determina si puede usar la caché recuperada previamente:

  • Evaluación de HTML principal: WebView activará shouldInterceptRequest() para el HTML principal en este momento. Para publicar correctamente la página desde la caché de recuperación previa, tu interceptor debe mostrar null. Si muestras un WebResourceResponse personalizado, WebView respeta tu interceptor y omite por completo la caché de recuperación previa.

  • Evaluación de subrecursos: Después de que se borra el HTML recuperado previamente para su uso, shouldInterceptRequest() se activa normalmente para todos los subrecursos posteriores (como imágenes, secuencias de comandos y CSS) necesarios para terminar de renderizar la página.

Comportamientos clave

Las siguientes características operativas y verificaciones de elegibilidad rigen cómo WebView inicia y administra las solicitudes de recuperación previa:

  • Seguridad de subprocesos: Las solicitudes se pueden iniciar desde cualquier subproceso.
  • Elegibilidad: Antes de iniciar una recuperación, WebView verifica que la solicitud sea segura y contextualmente adecuada. Para ello, verifica lo siguiente:
    • Cookies existentes: Para proteger la privacidad del usuario y evitar efectos secundarios similares a CSRF, WebView podría omitir la recuperación previa si la solicitud requiere cookies autenticadas específicas que podrían activar un cambio de estado en el servidor.
    • Presencia de service worker: Si un service worker ya controla el alcance de la URL, WebView puede diferir al controlador de recuperación del service worker en lugar de iniciar una carga previa de red estándar.
    • Disponibilidad del proxy: WebView verifica que la ruta de acceso de red actual (incluidos los proxies configurados) sea estable para evitar que fallen las solicitudes especulativas en configuraciones de red complejas.
  • Si no se puede iniciar una recuperación previa (incluso con parámetros válidos), suele deberse a que WebView determinó que una solicitud en segundo plano podría interferir con la sesión actual del usuario o el estado de seguridad.
  • Cancelación: Usa CancellationSignal para finalizar una solicitud en curso y evitar que se almacene en caché.

Renderización previa de páginas

La renderización previa crea "contenido web" oculto para renderizar por completo una página en segundo plano, incluida la ejecución de secuencias de comandos y la recuperación de subrecursos. La renderización previa se basa en la misma infraestructura subyacente que la recuperación previa. Si una app inicia una renderización previa, WebView primero realiza una recuperación previa de la respuesta para publicar la navegación de renderización previa, lo que evita la actividad de red redundante.

Implementación

La renderización previa es una operación a nivel de instancia de WebView. Llama a prerenderUrlAsync() con WebViewCompat desde el subproceso de IU.

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

Tanto la recuperación previa como la renderización previa son completamente asíncronas. Se puede llamar a prefetchUrlAsync() desde cualquier subproceso, mientras que prerenderUrlAsync() se debe iniciar desde el subproceso de IU.

Limitaciones técnicas

Para equilibrar la navegación instantánea con el estado del sistema, WebView aplica las siguientes restricciones de tiempo de ejecución:

  • Presión de memoria: WebView cancela las URLs renderizadas previamente si el dispositivo tiene poca RAM.
  • APIs no permitidas: Cualquier intento de JavaScript para acceder a ciertas APIs (por ejemplo, reproducción de audio, alertas) en un contexto en segundo plano finalizará de inmediato la renderización previa.
  • Límite de instancias: Existe un límite para la cantidad de URLs renderizadas previamente activas permitidas por WebView.

Coincidencia de URLs y No-Vary-Search (NVS)

WebView requiere un algoritmo de coincidencia confiable para garantizar que un recurso precargado solo se publique para la navegación prevista.

Coincidencia exacta versus coincidencia de NVS

De forma predeterminada, la recuperación previa y la renderización previa requieren una coincidencia exacta de la URL. Si la URL navegada es idéntica a la URL precargada, se publica de inmediato desde la caché. Si los parámetros de consulta difieren, WebView usa las siguientes reglas de No-Vary-Search (NVS):

  • La sugerencia: Los desarrolladores proporcionan una sugerencia setExpectedNoVarySearchHeader() durante la inicialización. Si la URL navegada coincide con la URL de la solicitud menos los parámetros sugeridos, WebView se bloquea brevemente para esperar los encabezados reales del servidor.
  • Encabezado del servidor: El encabezado de respuesta de NVS del servidor es la autoridad definitiva. Si el servidor confirma que se deben ignorar las diferencias de consulta, la coincidencia se publica desde la caché. De lo contrario, WebView vuelve a una carga de red en frío.

No-Vary-Search (NVS) es para uso avanzado, y es posible que la mayoría de los desarrolladores no lo necesiten porque pasan la misma URL exacta a la carga previa y a la navegación (WebViewCompat.navigate o loadUrl). Esta guía solo es necesaria si hay diferencias en los parámetros de consulta entre la URL de carga previa y la URL navegada.

Configuración global

Ajusta el comportamiento de carga especulativa a nivel de perfil configurando los límites de PrefetchCache y las renderizaciones previas máximas. También puedes restablecer los límites de carga previa personalizados a los valores predeterminados del sistema:

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

Manejo de errores y excepciones

Las operaciones especulativas usan un OutcomeReceiverCompat o un PrerenderOperationCallback para informar los resultados.

Excepciones principales

Cuando falla una operación de carga especulativa, tu controlador de errores informa uno de los siguientes tipos de excepción principales para ayudarte a diagnosticar situaciones de falla específicas:

  • PrefetchException: Es la clase base para todos los errores de recuperación previa asíncronos.
  • PrefetchNetworkException: Indica una falla a nivel de red o servidor. Puede incluir un campo httpStatusCode (como 404 o 503) para ayudar a diagnosticar problemas del servidor.
  • PrerenderException: Es la superclase para todos los errores relacionados con la renderización previa, como las fallas debido a la presión de la memoria o el uso de APIs no permitidas (como la reproducción de audio) en segundo plano.

Estrategias de optimización

Sigue estas recomendaciones para maximizar los beneficios de la carga especulativa y, al mismo tiempo, conservar los recursos del sistema:

  • Inicia temprano: Comienza la recuperación previa durante el inicio de la app o tan pronto como sea probable un destino de navegación.
  • Estrategia integrada: Si renderizas previamente una URL que ya está en la caché de recuperación previa, la navegación de renderización previa se publica desde esa caché, lo que evita solicitudes de red redundantes.
  • Supervisa las cuotas: La renderización previa consume muchos recursos. Prefiere la recuperación previa para varios candidatos probables y reserva la renderización previa para la navegación más probable.
  • Compatibilidad con esquemas: Asegúrate de que todas las URLs usen el esquema HTTPS obligatorio. Los esquemas no válidos o las entradas nulas activan un IllegalArgumentException síncrono.

Recursos adicionales

Para obtener más información sobre la depuración de apps web, la optimización del rendimiento de inicio de WebView y el manejo de la finalización del proceso de renderizador, consulta los siguientes recursos: