Un WebView se vende como «la web, dentro de tu app»: actualizaciones instantáneas, sin pasar por la tienda. En producción es una caché que no controlas, dividida entre dos plataformas, que decide en silencio qué versión de tu página cuenta como «actual». En iOS no hay básicamente offline ni Service Worker. En Android la caché respeta tus encabezados solo a veces. El resultado previsible es una página que renderiza el JavaScript de la semana pasada bajo el HTML de esta: una pantalla en blanco sin error, sin stack y sin causa obvia.
La caché que decide por su cuenta
Cuando publicas una página web firmas un contrato: el HTML referencia un bundle concreto, y ese bundle espera el DOM del HTML. Un navegador mantiene el contrato revalidando en un calendario que tú fijas. Un WebView lo mantiene según el ánimo de la plataforma. La caché está por debajo de tu código, por debajo de tu CDN y por debajo de tu control. Puedes enviar los encabezados correctos; no puedes obligar al motor a obedecerlos.
iOS: sin offline, sin Service Worker
En iOS, WKWebView se construye sobre URLSession y respeta los encabezados HTTP de cache-control estándar, pero esa caché es en memoria y por sesión, así que se evapora en cuanto el proceso muere. No hay caché offline persistente. El respaldo oficial para offline es un Service Worker, y los Service Workers no están disponibles en WKWebView por defecto. Solo funcionan con el permiso privado de Web Browser que los desarrolladores de terceros no pueden obtener, o activando limitsNavigationsToAppBoundDomains y declarando cada dominio en WKAppBoundDomains: tope de diez entradas, sin comodines y sin esquemas de URL personalizados. El antiguo issue de WebKit que rastrea esto (bug 206741) sigue abierto. Para una página embebida, offline simplemente no es algo que la plataforma te dé.
Android: una caché que ignora tus encabezados
Android WebView sí soporta Service Workers, pero su caché HTTP es la trampa mayor. Sin Cache-Control, el motor aplica una heurística: guarda una respuesta durante aproximadamente (Date − Last-Modified) × 10% de su antigüedad, y durante esa ventana ni siquiera toca la red. Peor aún, en dispositivos con el modo LOAD_DEFAULT por defecto, la caché puede ignorar un no-cache del servidor y servir la copia local de todos modos. El mismo equipo que puso max-age=31536000 a un asset estático —porque es «rápido»— construyó una página que no se actualizará en un año en esos dispositivos.
El modo de fallo de pantalla en blanco
Aquí es donde las dos plataformas se encuentran de la peor manera. Publicas un nuevo HTML que apunta a un bundle renombrado o con hash de contenido. El HTML se obtiene fresco, pero el JavaScript o CSS viejo sigue en la opaca caché de disco del WebView. El nuevo DOM carga, el script viejo corre contra él, y el resultado es un layout roto o —a menudo— una pantalla en blanco silenciosa. Los usuarios deben borrar los datos de la app para recuperarse. La conocida historia de guerra de los devs chinos «H5 页面发版后内容未更新» no es un bug de una sola app; es el modelo de caché de la plataforma combatiendo tu release.
La solución del runtime
Un runtime propio del host no apuesta por la caché del navegador. Sirve recursos versionados y deterministas desde la capa del host: el runtime sabe qué HTML pertenece a qué bundle y no dejará que un fragmento obsoleto cargue contra un nuevo shell. El offline se vuelve una capacidad de primera clase gestionada por el host —una caché persistente que el runtime controla—, no una esperanza de que el motor respete tus encabezados. La página que pretendes es la página que renderiza, en ambas plataformas, cada vez.
| Dimensión | WebView | Runtime propio del host |
|---|---|---|
| Offline iOS | Sin Service Worker; caché en memoria muere con el proceso | Caché persistente gestionada por el host |
| Caché Android | Heurística (Date−LM)×10%; ignora no-cache en algunos dispositivos | Fetch determinista, con versión fijada |
| Visibilidad de actualización | JS obsoleto bajo HTML nuevo → pantalla en blanco | Shell y bundle avanzan juntos |
| Recuperación | El usuario debe borrar datos de la app | El runtime controla la invalidación |
| Multiplataforma | Dos bugs de caché distintos | Un contrato consistente |
El hilo conductor de esta serie es el control. Un WebView entrega a tu pipeline de releases una caché que no posees ni puedes confiar. Un runtime te deja fijar la versión. Para un catálogo de páginas de terceros que transan dentro de tu app, esa diferencia es la brecha entre una página que se actualiza cuando tú lo dices y una que se actualiza cuando el motor se digna.
Fuentes
- WKWebView no tiene caché offline persistente — depende de la caché HTTP en memoria y por sesión de URLSession — análisis de causa raíz: la caché se evapora al morir el proceso y los Service Workers no son una opción dentro de la app.
- Los Service Workers no se soportan en WKWebView sin App-Bound Domains (tope 10, sin comodines, sin esquemas propios) — las trampas de WKWebView de takazudo: offline y sync en segundo plano no disponibles por defecto.
- WebKit bug 206741 — WKWebView no implementa Service Workers — el antiguo issue de seguimiento de WebKit; SW queda tras un permiso no disponible para terceros.
- La heurística de caché por defecto de Android WebView sirve assets obsoletos cuando falta o se ignora Cache-Control — heurística (Date − Last-Modified) × 10%; algunos dispositivos ignoran
no-cachebajoLOAD_DEFAULT. - AppCache eliminado: Chrome 90 (Android WebView) e iOS 15 — los Service Workers son la única salida offline — y SW no funciona dentro de WKWebView, dejando a las apps iOS sin caché offline.