Administra el estado de WebView de manera eficiente

Cuando se administra el ciclo de vida de una app para Android, preservar el estado del usuario durante la recuperación de recursos en segundo plano es un componente fundamental de una experiencia del usuario fluida. En el caso de las apps que incorporan flujos de trabajo web, WebView.saveState(Bundle) te permite serializar el historial de navegación y el estado de WebView en un Bundle. Posteriormente, estos datos se pueden restablecer con WebView.restoreState(Bundle).

Sin embargo, las implementaciones estándar pueden tener limitaciones de tamaño de transacción en sesiones de navegación pesadas. En esta página, se describen estas limitaciones arquitectónicas y se proporcionan estrategias para evitar excepciones relacionadas con la memoria mientras se mantiene el historial de navegación.

El límite de transacción de 1 MB y el borrado de estado

Android impone un límite estricto de 1 MB en el volumen total de datos que se pueden almacenar en savedInstanceState. Este presupuesto de 1 MB se comparte en todo el proceso de la app. Si una app incorpora varias instancias de WebView, su estado e historial de navegación colectivos deben caber dentro de esta única asignación compartida. Si se supera este límite, se activa una TransactionTooLargeException, lo que provoca que la app falle.

Una estrategia de mitigación común, pero problemática, consiste en supervisar el tamaño del paquete de estado de WebView y borrar por completo el historial de WebView si cruza un umbral de seguridad arbitrario (como 300 KB). Si bien esto evita una falla, introduce regresiones graves en la experiencia del usuario:

  • Pérdida de navegación hacia atrás: Android suele finalizar los procesos de apps en segundo plano para recuperar memoria para otras tareas. Puedes usar saveState(Bundle) dentro de la devolución de llamada del ciclo de vida onSaveInstanceState() para preservar el historial de navegación. Si borras este historial para evitar el límite de transacción de 1 MB, se pierde toda la pila de navegación. Cuando el usuario regresa a la app, el botón Atrás del sistema sale de inmediato del componente o de la app porque no queda contexto histórico para admitir la navegación hacia atrás, independientemente de si se produjo un reinicio del proceso.

  • Invalidación de BFCache: Borrar el historial impide que la app use la caché de atrás y adelante (BFCache), lo que quita la capacidad de renderizar instantáneamente las páginas visitadas anteriormente.

  • Mayor latencia: Los usuarios pierden su estado actual en WebView, lo que requiere una nueva navegación y una reinicialización completas. Este proceso aumenta significativamente la sobrecarga de la red y la latencia de las transacciones.

Estrategias de mitigación arquitectónica

Para evitar fallas de TransactionTooLargeException sin degradar la experiencia del usuario mediante la eliminación completa del historial, debes mantener un equilibrio riguroso entre la retención de estado y la eficiencia de la memoria. Si implementas las siguientes estrategias de optimización, puedes administrar de forma segura el presupuesto de transacción de 1 MB mientras preservas el historial de navegación esencial y la integridad de la sesión.

Aplica límites de tamaño en la serialización de estado

En lugar de borrar por completo la pila de navegación cuando crece demasiado, un patrón más eficaz es truncar los datos históricos:

  • Política de eliminación segmentada: Usa WebViewCompat.saveState() para serializar el estado mientras aplicas un límite de bytes específico (por ejemplo, WebViewCompat.saveState(webView, outState, maxSizeBytes)). Esta API quita automáticamente las entradas de navegación más antiguas de forma secuencial hasta que la carga útil total quepa dentro de la asignación definida. Es fundamental que esto solo trunque el Bundle serializado sin modificar ni borrar el historial en vivo del WebView activo, lo que garantiza que la navegación hacia atrás inmediata permanezca completamente intacta.

  • Eliminación de entradas hacia adelante: Si la interfaz de la aplicación proporciona un botón Atrás pero no tiene un botón de navegación hacia adelante dedicado, puedes descartar todas las entradas de navegación hacia adelante configurando el parámetro includeForwardState de la API de saveState en false. Esto reduce significativamente el tamaño de la carga útil sin afectar las rutas de navegación disponibles del usuario.

Administra la latencia de recursos con la API de HTTP Cache Quota

Si bien saveState administra el límite de Bundle de 1 MB para el historial de navegación efímero, la API de HTTP Cache Quota proporciona control manual sobre los recursos web persistentes (memoria caché del disco) por perfil. Esto crea una distinción clara entre el contexto de navegación a corto plazo y los recursos almacenados en caché a largo plazo.

Elegir una cuota adecuada implica una compensación de rendimiento:

  • Las cuotas más altas mejoran la disponibilidad sin conexión y la latencia de carga de recursos, ya que mantienen más recursos en el disco.
  • Las cuotas más bajas minimizan el espacio en disco de la app y evitan el desalojo de la caché dirigida por el SO de otros datos de app críticos.

Estos parámetros de configuración persisten después de que se reinicia la app y se deben configurar desde el subproceso principal.

En la siguiente implementación, se muestra cómo configurar una cuota de memoria caché del disco para el perfil predeterminado:

Kotlin

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    val defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME)
    val httpCache = defaultProfile.httpCache

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024)
}

Java

if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
    WebViewFeature.isFeatureSupported(WebViewFeature.HTTP_CACHE)) {
    Profile defaultProfile = ProfileStore.getInstance()
        .getOrCreateProfile(Profile.DEFAULT_PROFILE_NAME);
    HttpCache httpCache = defaultProfile.getHttpCache();

    // Set explicit cache size to 50MB (50 * 1024 * 1024 bytes)
    httpCache.setQuotaBytes(50L * 1024 * 1024);
}

Para obtener más información sobre las estrategias de ajuste de tamaño de cuotas, la administración del ciclo de vida y los límites de perfil, consulta Administra la cuota de caché HTTP en WebView.

Consideraciones clave sobre el rendimiento

En los siguientes puntos, se destacan las limitaciones técnicas y los comportamientos de datos internos que rigen el comportamiento del estado de WebView:

  • Blobs PageState opacos: Aproximadamente el 70% de los datos almacenados por saveState consta de blobs PageState internos del motor de renderización. Estos datos capturan estados de sesión detallados, incluidas las entradas de formularios y las posiciones de desplazamiento de iframe. Evita intentar analizar o quitar manualmente segmentos individuales de estos blobs, ya que hacerlo plantea graves riesgos de seguridad y rompe la integridad de la restauración de la sesión.

  • Administración de historial detallada: La API estándar WebBackForwardList no admite de forma nativa la eliminación arbitraria de elementos históricos individuales. Para una administración de estado estricta, debes implementar estrategias de truncamiento con los parámetros maxSizeBytes y includeForwardState dentro de WebViewCompat.saveState() para garantizar la seguridad arquitectónica.