Optimiza el inicio de WebView

Cuando tu app usa una 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 la IU la primera vez que la aplicación llama a muchas APIs dentro de los paquetes android.webkit o androidx.webkit, o bien aumenta un diseño que contiene una etiqueta WebView.

¿Por qué es importante?

Debido a que este inicio implícito ocurre por completo en el subproceso principal, impide que tu app procese la entrada del usuario y aumenta drásticamente el riesgo de errores de Application Not Responding (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 overview.

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().
  • Usar diseños: Llamar a setContentView() o layoutInflater.inflate() en un recurso XML que incluya un <WebView>.

El inicio implícito también puede afectar negativamente tus métricas comerciales, 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 de inicio de WebView con la API de startUpWebView.

Toma el control del 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 la IU se realice en fragmentos, en lugar de un gran bloque monolítico. Esto libera el subproceso de la IU para controlar 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 correctas.

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 optimizas tu flujo de inicio depende de cuándo tu app realmente necesita mostrar la 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 esperar 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 bloquea el subproceso de la IU mientras espera que se complete la inicialización. Es posible que tu app obtenga algún beneficio de rendimiento del trabajo en segundo plano ya completado, pero no el máximo beneficio.

Cuando WebView está en la ruta crítica

Si el recorrido del usuario principal de tu app requiere una WebView de inmediato, es probable que no puedas esperar a que se complete el inicio de WebView. En esta situación, 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 una WebView o llamar a las APIs de WebView hasta que no queden otras operaciones de subprocesos de la IU de ruta crítica para ejecutar (como aumentar las jerarquías de diseño, inicializar otros SDKs o dibujar el marco inicial).

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

Si el uso de WebView puede estar en la ruta crítica, pero no quieres iniciar WebView por completo, puedes elegir ejecutar de forma selectiva las tareas de inicio de WebView que pueden ejecutarse en un subproceso en segundo plano, lo que libera el subproceso de la 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 la IU. Si esperas la devolución de llamada en ese momento depende 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 las fallas 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 tu app antes de que se ejecute tu llamada. Esto podría deberse a las siguientes razones:

  • Bibliotecas de terceros o SDKs inicializados al principio del ciclo de vida de la app.

  • ContentProviders insertados en tu APK que activan las APIs de WebView durante el inicio de la app.

  • Aumentos de diseño o llamadas programáticas (como la obtención de cadenas de agente de usuario) que ocurren de forma inesperada antes de tiempo.

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

  • getUiThreadBlockingStartUpLocations(): Muestra 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(): Muestra sitios de llamadas específicos en los que la ejecución de tareas de inicio bloqueó 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 de rendimiento:

  • Busca seguimientos de pila inesperados: Si getUiThreadBlockingStartUpLocations() no está vacío, observa 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 registro muestran que se produjo una inicialización implícita antes de la llamada manual a startUpWebView, debes mover la inicialización de startUpWebView antes en tu app o configurar el SDK ofensivo para retrasar sus tareas dependientes de WebView.

Migra 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 obtener la cadena de agente de usuario.

Estas soluciones alternativas se consideran prácticas no compatibles, y su comportamiento subyacente puede cambiar en las próximas versiones. 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 en su lugar. La API de startUpWebView funciona en todas las versiones de Android y WebView compatibles con la biblioteca de Jetpack Webkit.

Garantiza la resiliencia y la estabilidad de la app

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 están disponibles las optimizaciones más recientes, la API mantiene la paridad de rendimiento con las soluciones alternativas 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 bien la optimización del inicio de WebView reduce el riesgo de errores de ANR durante el lanzamiento de la app, también debes proteger tu app contra fallas del renderizador en tiempo de ejecución y la recuperación de memoria del sistema. Para mantener la estabilidad integral de la app una vez que se ejecuta tu WebView, consulta Controla la finalización de WebView.

Si tienes problemas o comentarios sobre la API de startUpWebView, registra un error en el registro de errores público.