Optimiza el inicio de WebView

Cuando tu app usa un objeto WebView por primera vez, el sistema realiza tareas de inicio específicas. Este proceso de inicio es pesado. De forma predeterminada, sucede de manera implícita en el subproceso de IU la primera vez que la aplicación llama a muchas APIs dentro de los paquetes android.webkit o androidx.webkit, o bien cuando se expande un diseño que contiene una etiqueta WebView.

Por qué es importante

Dado que este inicio implícito se produce por completo en el subproceso principal, impide que tu app procese la entrada del usuario y aumenta drásticamente el riesgo de errores de aplicación no responde (ANR). Para obtener más información sobre cómo Android controla el modelo de ejecución de un solo subproceso, consulta Descripción general de procesos y subprocesos.

Activadores para el inicio implícito

El inicio implícito se puede activar de las siguientes maneras:

  • De manera programática: Llamar a APIs como WebSettings.getUserAgentString()
  • Uso de diseños: Llamar a setContentView() o layoutInflater.inflate() en un recurso XML que incluye un <WebView>

El inicio implícito también puede afectar de forma negativa las métricas de tu empresa, como el tiempo de inicio de la app y el tiempo hasta la primera visualización. Si la inicialización implícita no es óptima para tu app, usa startUpWebView en su lugar.

En esta página, se explica cómo optimizar el rendimiento del inicio de WebView con la API de startUpWebView.

Cómo controlar el inicio de WebView

Para mejorar el rendimiento y minimizar los errores de ANR, usa la API de startUpWebView disponible en la biblioteca de Jetpack Webkit. Esta API te brinda control explícito sobre cuándo se inicia WebView. Traslada una cantidad significativa de la carga de trabajo de inicio a un subproceso en segundo plano y permite que cualquier trabajo que deba realizarse en el subproceso de IU se haga en partes, en lugar de un gran bloque monolítico. Esto libera el subproceso de IU para que controle otras tareas críticas de la app en paralelo, lo que reduce la posibilidad de bloquear la experiencia del usuario.

La API usa la devolución de llamada androidx.webkit.WebViewOutcomeReceiver, lo que te permite hacer un seguimiento de las inicializaciones exitosas.

Para usar esta API, agrega la biblioteca de Jetpack Webkit a tu archivo build.gradle. Asegúrate de usar la versión 1.16.0 o una posterior:

dependencies {
    implementation("androidx.webkit:webkit:1.16.0")
}

Usa la API de startUpWebView

La forma en que optimices el flujo de inicio dependerá de cuándo tu app realmente necesite mostrar el WebView.

Cuando WebView no está en la ruta crítica

Si tu app no necesita cargar una WebView de inmediato, puedes ocultar por completo el costo de inicialización. Llama a startUpWebView al principio del ciclo de vida de tu app y espera a que se active la devolución de llamada de éxito.

Lo ideal es que esperes la devolución de llamada antes de llamar a otras APIs de WebView. Si activas startUpWebView, pero no esperas a que finalice antes de tocar otros componentes de WebView, el sistema bloqueará el subproceso de IU mientras espera a que se complete la inicialización. Es posible que tu app obtenga algunos beneficios de rendimiento del trabajo en segundo plano ya completado, pero no el máximo beneficio.

Cuándo WebView está en la ruta crítica

Si el recorrido del usuario principal de tu app requiere una WebView de inmediato, probablemente no puedas permitirte esperar a que se complete el inicio de WebView. En este caso, debes llamar a startUpWebView lo antes posible en el ciclo de vida de la app (por ejemplo, en Application.onCreate), pero no esperes a que se active la devolución de llamada. En su lugar, usa las APIs de WebView directamente cuando sean necesarias.

Para obtener el máximo beneficio del inicio asíncrono, es fundamental diferir la creación de instancias de WebView o la llamada a las APIs de WebView hasta que no queden otras operaciones críticas del subproceso de IU para ejecutar (como inflar jerarquías de diseño, inicializar otros SDKs o dibujar el fotograma inicial).

Si llamas a startUpWebView y, luego, invocas de inmediato las APIs de WebView en el subproceso principal, el subproceso de IU se bloqueará mientras espera que se ponga al día la inicialización. En esta situación, no hay ningún beneficio de rendimiento.

Si el uso de WebView puede convertirse en una ruta crítica, pero no quieres iniciar WebView por completo, puedes optar por ejecutar de forma selectiva las tareas de inicio de WebView que se pueden ejecutar en un subproceso en segundo plano, lo que libera el subproceso de IU para otras tareas críticas de la app. Para ello, puedes usar shouldRunUiThreadStartUpTasks(false).

Más adelante en el ciclo de vida de tu app, puedes volver a llamar a startUpWebView con shouldRunUiThreadStartUpTasks(true) para finalizar las tareas de inicio restantes en el subproceso de IU. Si esperas la devolución de llamada en ese punto, dependerá de si el uso de WebView está en la ruta crítica.

Ejemplo de implementación

La API usa la devolución de llamada androidx.webkit.WebViewOutcomeReceiver, lo que te permite hacer un seguimiento de las inicializaciones correctas o controlar los errores de diagnóstico.

Es seguro llamar a startUpWebView varias veces desde diferentes partes de tu app. Te recomendamos que evites implementar un bucle de reintento ingenuo.

En el siguiente ejemplo de código, se muestra cómo usar la API de WebViewCompat.startUpWebView para la inicialización asíncrona.

Kotlin

import android.content.Context
import android.util.Log
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewOutcomeReceiver
import androidx.webkit.WebViewStartUpConfig
import androidx.webkit.WebViewStartUpResult
import androidx.webkit.WebViewStartupException
import java.util.concurrent.Executors

fun initializeWebView(context: Context) {
    // 1. Create a startup configuration specifying the background thread
    // that WebView will use to run its initialization tasks.
    val startUpConfig = WebViewStartUpConfig.Builder(
        Executors.newSingleThreadExecutor()
    ).build()

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
        context,
        startUpConfig,
        object : WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException> {

            override fun onResult(result: WebViewStartUpResult) {
                // Success: The WebView has finished its background initialization.
                // This callback is guaranteed to be invoked on the UI thread.
                setupWebView()
            }

            override fun onError(error: WebViewStartupException) {
                // Failure: The initialization encountered a startup exception.
                Log.e("WebViewStartup", "Failed to initialize WebView", error)
            }
        }
    )
}

Java

import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.webkit.WebViewCompat;
import androidx.webkit.WebViewOutcomeReceiver;
import androidx.webkit.WebViewStartUpConfig;
import androidx.webkit.WebViewStartUpResult;
import androidx.webkit.WebViewStartupException;
import java.util.concurrent.Executors;

public void initializeWebView(Context context) {
    // 1. Create the startup configuration specifying the background thread pool
    // to handle internal non-UI initialization processes.
    WebViewStartUpConfig startUpConfig = new WebViewStartUpConfig.Builder(
            Executors.newSingleThreadExecutor()
    ).build();

    // 2. Trigger WebView startup asynchronously
    WebViewCompat.startUpWebView(
            context,
            startUpConfig,
            new WebViewOutcomeReceiver<WebViewStartUpResult, WebViewStartupException>() {

                @Override
                public void onResult(@NonNull WebViewStartUpResult result) {
                    // Success: The WebView has finished its background initialization.
                    // This callback is invoked directly on the UI thread.
                    setupWebView();
                }

                @Override
                public void onError(@NonNull WebViewStartupException error) {
                    // Failure: Handled using the concrete WebViewStartupException
                    Log.e("WebViewStartup", "Failed to initialize WebView", error);
                }
            }
    );
}

Depura problemas de inicio asíncrono

Si startUpWebView no produce los beneficios de rendimiento esperados, suele deberse a que WebView se inicializa de forma implícita en otro lugar de la app antes de que se ejecute tu llamada. Esto podría deberse a las siguientes razones:

  • Bibliotecas de terceros o SDKs que se inicializan al principio del ciclo de vida de la app

  • ContentProviders que se inyecta en tu APK y que activa las APIs de WebView durante el inicio de la app

  • Inflaciones de diseño o llamadas programáticas (como la recuperación de cadenas de agentes de usuario) que ocurren de forma inesperada y demasiado pronto.

Para ayudarte a diagnosticar dónde y por qué se producen estas inicializaciones inesperadas, el objeto WebViewStartUpResult proporciona capacidades de auditoría integradas:

  • getUiThreadBlockingStartUpLocations(): Devuelve una lista de objetos StartUpLocation que representan ubicaciones en las que las tareas de inicio de WebView bloquearon el subproceso de la IU principal.

  • getNonUiThreadBlockingStartUpLocations(): Devuelve sitios de llamadas específicos en los que las tareas de inicio en ejecución bloquearon los subprocesos en segundo plano.

Cada StartUpLocation contiene un seguimiento de pila que puedes registrar o inspeccionar para encontrar la clase y el método exactos que activaron la inicialización.

Ejemplo de implementación

Puedes inspeccionar estas ubicaciones dentro de tu devolución de llamada onResult para auditar tu ruta de inicio:

override fun onResult(result: WebViewStartUpResult) {
    // Check if WebView startup was blocked on the UI thread prior to or during initialization
    val uiBlockingLocations = result.getUiThreadBlockingStartUpLocations()
    if (!uiBlockingLocations.isNullOrEmpty()) {
        for (location in uiBlockingLocations) {
            // Log the stack trace of the call site that triggered the UI-blocking startup
            Log.w("WebViewDebug", "WebView startup blocked the UI thread here:", location.getStack())
        }
    } else {
        Log.i("WebViewDebug", "Excellent! No UI-blocking WebView startup detected.")
    }

    // Check where background initialization tasks were executed
    val backgroundLocations = result.getNonUiThreadBlockingStartUpLocations()
    backgroundLocations?.forEach { location ->
        Log.d("WebViewDebug", "WebView background startup occurred at: ${location.getStack()}")
    }

    setupWebView()
}

Cómo usar estos datos durante una auditoría

Cuando audites el inicio de WebView de tu app, usa las siguientes estrategias para analizar los datos de diagnóstico y abordar los cuellos de botella del rendimiento:

  • Busca seguimientos de pila inesperados: Si getUiThreadBlockingStartUpLocations() no está vacío, consulta los seguimientos de pila impresos. Si ves clases que pertenecen a SDKs de terceros o componentes inesperados, encontraste un cuello de botella de inicialización implícito.

  • Verifica el orden de las llamadas: Si los resultados de tu registro muestran que se produjo una inicialización implícita antes de tu llamada manual a startUpWebView, debes adelantar la inicialización de startUpWebView en tu app o configurar el SDK infractor para que retrase sus tareas dependientes de WebView.

Cómo migrar desde soluciones alternativas anteriores

En el pasado, es posible que hayas usado soluciones alternativas explícitas para forzar la inicialización de WebView en un subproceso en segundo plano, como recuperar la cadena de usuario-agente.

Estas soluciones alternativas se consideran prácticas no admitidas, y su comportamiento subyacente puede cambiar en versiones futuras. Si tu app depende de soluciones alternativas explícitas y no documentadas para activar o administrar el inicio de WebView, te recomendamos que uses la API de startUpWebView. La API de startUpWebView funciona en todas las versiones de Android y WebView que admite la biblioteca de Jetpack Webkit.

El uso de la implementación de Jetpack Webkit ayuda a garantizar un comportamiento coherente en todo el ecosistema de Android. Una ventaja clave de esta API es su resiliencia: en dispositivos heredados en los que no hay optimizaciones más recientes disponibles, la API mantiene la paridad de rendimiento con las soluciones manuales. Esto te permite adoptar los beneficios de inicio modernos en dispositivos más nuevos sin incurrir en una penalización de rendimiento en los más antiguos.

Si tienes problemas o comentarios sobre la API de startUpWebView, informa un error en la herramienta pública de seguimiento de errores.