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 recupere o renderice contenido antes de que el usuario navegue a él de forma explícita.

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 en la latencia de carga del contenido web: Adelanta la hora de inicio de la red en el ciclo de vida de la app.
  • Mayores tasas de éxito de navegación: Al precalentar 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

El factor diferenciador clave 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 URLs, lo que significa que requieren la ruta exacta de la página web.

Dado que Preconnect opera a nivel del origen, se puede iniciar mucho antes en el ciclo de vida de la app, incluso antes de que sepas 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 Recuperación previa Renderización previa
Objetivo principal Calienta la conexión Almacenar en caché solo el código HTML (sin JavaScript ni CSS) Realiza una renderización previa de toda la página
Alcance Nivel de perfil (se comparte en todos los WebView) Nivel de perfil (se comparte en todos los WebView) Nivel de WebView (vinculado a un WebView específico)
API de Jetpack WebKit androidx.webkit.Profile androidx.webkit.Profile androidx.webkit.WebViewCompat
Métodos de la API principal preconnect(...) prefetchUrlAsync(...) prerenderUrlAsync(...)
Configuration N/A PrefetchCache.setMaxPrefetches()
PrefetchCache.setPrefetchTtlSeconds()
setMaxPrerenders()
Uso de recursos Baja (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 navegue, con 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 verdaderamente instantánea al activarse

Conexión previa a orígenes

La preconexión acelera las cargas futuras, ya que realiza de forma anticipada 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 las de Prefetch y Prerender.

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

Implementación

Para iniciar una preconexión, 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 la recuperación previa como la renderización previa 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 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();

Carga 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 objeto Profile actúa como contenedor de datos del navegador, incluidas las cookies, la caché HTTP y los service workers. Como la recuperación previa es una operación a nivel del perfil, cualquier WebView asociado a 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 interceptación

La solicitud de recuperación previa de WebView altera cuándo y cómo se activa la devolución de llamada shouldInterceptRequest(). Dado que esto tiene un impacto directo en si el contenido prefetch se usa correctamente, 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: Ciclo de vida de la interceptación en 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 inserciones de encabezado que, por lo general, se controlan dentro de tu interceptor al recurso HTML prefetch.

2. Fase de navegación (activación del usuario)

Cuando la app navega de forma explícita a la URL (por ejemplo, con loadUrl()) o el usuario hace clic en un vínculo coincidente, WebView determina si puede usar la caché previa:

  • Evaluación del HTML principal: En este momento, WebView activará shouldInterceptRequest() para el HTML principal. Para publicar correctamente la página desde la caché previa a la búsqueda, tu interceptor debe devolver null. Si devuelves 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 autoriza el uso del código HTML prefetch, 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 la forma en que WebView inicia y administra las solicitudes de carga previa:

  • Seguridad de subprocesos: Las solicitudes se pueden iniciar desde cualquier subproceso.
  • Elegibilidad: Antes de iniciar una recuperación, WebView se asegura de que la solicitud sea segura y adecuada para el contexto verificando lo siguiente:
    • Cookies existentes: Para proteger la privacidad del usuario y evitar efectos secundarios similares a los de CSRF, es posible que WebView omita 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 red actual (incluidos los proxies configurados) sea estable para evitar errores en las solicitudes especulativas en configuraciones de red complejas.
  • Si una recuperación previa no se inicia (incluso con parámetros válidos), suele deberse a que WebView determinó que una solicitud en segundo plano podría interferir en la sesión actual del usuario o en 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, lo que incluye la ejecución de secuencias de comandos y la recuperación de recursos secundarios. 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

El procesamiento previo es una operación a nivel de la 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.

Restricciones 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 con renderización previa 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 su navegación prevista.

Comparación entre la concordancia exacta y la concordancia de NVS

De forma predeterminada, la recuperación previa y la renderización previa requieren una coincidencia exacta de la URL. Si la URL a la que se navegó es idéntica a la URL precargada, se entrega desde la caché de inmediato. 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 a la que se navegó 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 en la búsqueda, la coincidencia se publica desde la caché. De lo contrario, WebView recurre a una carga de red en frío.

No-Vary-Search (NVS) es para un 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 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 de loadUrl().

Configuración global

Configura los límites de PrefetchCache y la cantidad máxima de renderizaciones previas para ajustar el comportamiento de carga especulativa a nivel del perfil. También puedes restablecer los límites de la recuperación previa personalizada 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, el 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íncrona.
  • PrefetchNetworkException: Indica una falla a nivel de la red o del 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 conservar los recursos del sistema:

  • Inicia la recuperación anticipada pronto: Comienza la recuperación anticipada 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 la recuperación previa, la navegación de la renderización previa se entrega 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 el esquema: 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 control de la finalización del proceso del renderizador, consulta los siguientes recursos: