<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Giovani Fouz</title>
    <description>The latest articles on DEV Community by Giovani Fouz (@gfouz).</description>
    <link>https://dev.to/gfouz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F893736%2F6ca75795-4154-4233-887b-fd6f5cb23bf7.jpg</url>
      <title>DEV Community: Giovani Fouz</title>
      <link>https://dev.to/gfouz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gfouz"/>
    <language>en</language>
    <item>
      <title>I Built a Perceptual Virtualization Engine for React — Optimized for Android WebViews and Low-End Devices</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 31 May 2026 18:47:46 +0000</pubDate>
      <link>https://dev.to/gfouz/i-built-a-perceptual-virtualization-engine-for-react-optimized-for-android-webviews-and-low-end-1m45</link>
      <guid>https://dev.to/gfouz/i-built-a-perceptual-virtualization-engine-for-react-optimized-for-android-webviews-and-low-end-1m45</guid>
      <description>&lt;p&gt;I Built a Perceptual Virtualization Engine for React — Optimized for Android WebViews and Low-End Devices&lt;/p&gt;

&lt;p&gt;Most React virtualization libraries optimize for desktop browsers.&lt;/p&gt;

&lt;p&gt;Very few survive this scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android 12&lt;/li&gt;
&lt;li&gt;Low-end phone&lt;/li&gt;
&lt;li&gt;Embedded WebView&lt;/li&gt;
&lt;li&gt;Hundreds of complex interactive forms&lt;/li&gt;
&lt;li&gt;Real-time database updates&lt;/li&gt;
&lt;li&gt;Continuous touch scrolling&lt;/li&gt;
&lt;li&gt;Controlled React inputs&lt;/li&gt;
&lt;li&gt;Dynamic calculations&lt;/li&gt;
&lt;li&gt;Conditional rendering&lt;/li&gt;
&lt;li&gt;Zero visual instability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the problem I needed to solve.&lt;/p&gt;

&lt;p&gt;After months of experimentation, profiling, recycling strategies, motion analysis, predictive rendering, and WebView debugging, I ended up building something that evolved far beyond a traditional virtualized list.&lt;/p&gt;

&lt;p&gt;I call it:&lt;/p&gt;

&lt;p&gt;Perceptual Engine&lt;/p&gt;

&lt;p&gt;A perceptual rendering engine for React.&lt;/p&gt;




&lt;p&gt;The Problem&lt;/p&gt;

&lt;p&gt;Traditional virtualization libraries are excellent for static rows.&lt;/p&gt;

&lt;p&gt;But deeply interactive UI is a completely different challenge.&lt;/p&gt;

&lt;p&gt;Especially on low-end Android devices running inside embedded WebViews.&lt;/p&gt;

&lt;p&gt;I tested this extensively on a Samsung Galaxy A03 Core running Android 12 inside a WebView environment.&lt;/p&gt;

&lt;p&gt;That environment exposes problems most desktop browsers hide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transform hit-testing bugs&lt;/li&gt;
&lt;li&gt;recycled layer interaction issues&lt;/li&gt;
&lt;li&gt;GPU compositing instability&lt;/li&gt;
&lt;li&gt;focus loss during recycling&lt;/li&gt;
&lt;li&gt;touch event inconsistencies&lt;/li&gt;
&lt;li&gt;invisible layers intercepting input&lt;/li&gt;
&lt;li&gt;stacking-context rendering quirks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was ambitious:&lt;/p&gt;

&lt;p&gt;«Render 120–150 heavy interactive forms smoothly on low-end Android hardware without blinking, layout jumps, or input instability.»&lt;/p&gt;




&lt;p&gt;These Were Not Lightweight Rows&lt;/p&gt;

&lt;p&gt;Each rendered item was effectively a mini application.&lt;/p&gt;

&lt;p&gt;Every form contained:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controlled React inputs&lt;/li&gt;
&lt;li&gt;derived calculations&lt;/li&gt;
&lt;li&gt;conditional rendering&lt;/li&gt;
&lt;li&gt;dynamic sections&lt;/li&gt;
&lt;li&gt;navigation handlers&lt;/li&gt;
&lt;li&gt;validation logic&lt;/li&gt;
&lt;li&gt;live state synchronization&lt;/li&gt;
&lt;li&gt;IndexedDB persistence through Dexie&lt;/li&gt;
&lt;li&gt;multiple interactive components&lt;/li&gt;
&lt;li&gt;touch interactions&lt;/li&gt;
&lt;li&gt;real-time updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, every row behaved more like a fully interactive UI surface than a simple list item.&lt;/p&gt;

&lt;p&gt;That distinction matters enormously when building virtualization systems.&lt;/p&gt;




&lt;p&gt;The Architecture&lt;/p&gt;

&lt;p&gt;Perceptual Engine is composed of multiple specialized systems working together.&lt;/p&gt;

&lt;p&gt;Motion Analyzer&lt;/p&gt;

&lt;p&gt;Tracks scroll velocity, acceleration, deceleration, and motion patterns to dynamically adapt rendering behavior.&lt;/p&gt;

&lt;p&gt;const motionState = motionAnalyzer.update(scrollTop, performance.now());&lt;/p&gt;

&lt;p&gt;This allows predictive overscan and smarter scheduling decisions.&lt;/p&gt;




&lt;p&gt;Predictive Rendering&lt;/p&gt;

&lt;p&gt;Instead of rendering only what's visible, the engine predicts where the user is going next.&lt;/p&gt;

&lt;p&gt;predictFuturePosition(200);&lt;/p&gt;

&lt;p&gt;The result is a scrolling experience that feels significantly smoother under rapid touch movement.&lt;/p&gt;




&lt;p&gt;Recycling Pool&lt;/p&gt;

&lt;p&gt;DOM nodes are aggressively recycled instead of recreated.&lt;/p&gt;

&lt;p&gt;recyclingPool.acquireWithOrigin(virtualItem);&lt;/p&gt;

&lt;p&gt;This dramatically reduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;garbage collection&lt;/li&gt;
&lt;li&gt;layout thrashing&lt;/li&gt;
&lt;li&gt;memory pressure&lt;/li&gt;
&lt;li&gt;mount/unmount churn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially important on low-end Android devices.&lt;/p&gt;




&lt;p&gt;Adaptive Quality System&lt;/p&gt;

&lt;p&gt;The engine continuously monitors FPS and rendering pressure.&lt;/p&gt;

&lt;p&gt;If performance drops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overscan is reduced&lt;/li&gt;
&lt;li&gt;predictive rendering can be disabled&lt;/li&gt;
&lt;li&gt;GPU compositing can be downgraded&lt;/li&gt;
&lt;li&gt;rendering complexity adapts automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;private degradeQuality(): void&lt;/p&gt;

&lt;p&gt;This keeps the UI responsive even under stress.&lt;/p&gt;




&lt;p&gt;The Hardest Bug&lt;/p&gt;

&lt;p&gt;Ironically, performance was not the hardest problem.&lt;/p&gt;

&lt;p&gt;Interaction was.&lt;/p&gt;

&lt;p&gt;At one point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scrolling was perfect&lt;/li&gt;
&lt;li&gt;rendering was stable&lt;/li&gt;
&lt;li&gt;150 complex forms rendered smoothly&lt;/li&gt;
&lt;li&gt;memory usage was controlled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…but inputs and buttons stopped working.&lt;/p&gt;

&lt;p&gt;The cause turned out to be an invisible stacking-context and hit-testing issue involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;absolute positioning&lt;/li&gt;
&lt;li&gt;recycled layers&lt;/li&gt;
&lt;li&gt;touch routing&lt;/li&gt;
&lt;li&gt;compositing layers&lt;/li&gt;
&lt;li&gt;Android rendering behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final fix?&lt;/p&gt;

&lt;p&gt;A single z-index adjustment on the interactive form layer.&lt;/p&gt;



&lt;p&gt;That one line restored:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;input focus&lt;/li&gt;
&lt;li&gt;text selection&lt;/li&gt;
&lt;li&gt;button interaction&lt;/li&gt;
&lt;li&gt;touch handling&lt;/li&gt;
&lt;li&gt;keyboard interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;without sacrificing performance.&lt;/p&gt;

&lt;p&gt;This was a reminder that mobile rendering engines still behave very differently from desktop browsers.&lt;/p&gt;




&lt;p&gt;Performance Results&lt;/p&gt;

&lt;p&gt;Test device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Samsung Galaxy A03 Core&lt;/li&gt;
&lt;li&gt;Android 12&lt;/li&gt;
&lt;li&gt;Embedded WebView&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;120–150 heavy interactive forms&lt;/li&gt;
&lt;li&gt;stable scrolling&lt;/li&gt;
&lt;li&gt;no blinking&lt;/li&gt;
&lt;li&gt;no layout jumping&lt;/li&gt;
&lt;li&gt;no focus flickering&lt;/li&gt;
&lt;li&gt;minimal re-renders&lt;/li&gt;
&lt;li&gt;low memory pressure&lt;/li&gt;
&lt;li&gt;responsive touch interactions&lt;/li&gt;
&lt;li&gt;stable controlled inputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine remained stable even under rapid touch scrolling.&lt;/p&gt;




&lt;p&gt;Key Optimizations&lt;/p&gt;

&lt;p&gt;Some implementation details that made a huge difference:&lt;/p&gt;

&lt;p&gt;Deferred Recycling During Touch Scroll&lt;/p&gt;

&lt;p&gt;private isTouchScrolling: boolean = false;&lt;/p&gt;

&lt;p&gt;Recycling is temporarily deferred while touch scrolling is active.&lt;/p&gt;

&lt;p&gt;This prevents focus destruction and touch instability.&lt;/p&gt;




&lt;p&gt;requestAnimationFrame Mutation Batching&lt;/p&gt;

&lt;p&gt;requestAnimationFrame(() =&amp;gt; {&lt;br&gt;
  this.applyMutationBatch();&lt;br&gt;
});&lt;/p&gt;

&lt;p&gt;DOM mutations are batched into animation frames to reduce layout thrashing.&lt;/p&gt;




&lt;p&gt;Adaptive Overscan&lt;/p&gt;

&lt;p&gt;Overscan changes dynamically depending on motion intensity.&lt;/p&gt;

&lt;p&gt;Fast scroll → larger predictive window.&lt;/p&gt;

&lt;p&gt;Slow scroll → lower memory pressure.&lt;/p&gt;




&lt;p&gt;Pool-Based DOM Lifecycle&lt;/p&gt;

&lt;p&gt;The engine almost never destroys DOM nodes.&lt;/p&gt;

&lt;p&gt;It aggressively reuses them instead.&lt;/p&gt;

&lt;p&gt;This dramatically improves consistency in WebView environments.&lt;/p&gt;




&lt;p&gt;Why “Perceptual”?&lt;/p&gt;

&lt;p&gt;Because the engine optimizes for perceived smoothness, not just raw rendering metrics.&lt;/p&gt;

&lt;p&gt;Users don’t care how many DOM nodes exist.&lt;/p&gt;

&lt;p&gt;They care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;responsiveness&lt;/li&gt;
&lt;li&gt;touch stability&lt;/li&gt;
&lt;li&gt;visual continuity&lt;/li&gt;
&lt;li&gt;interaction reliability&lt;/li&gt;
&lt;li&gt;absence of flicker&lt;/li&gt;
&lt;li&gt;stable inputs&lt;/li&gt;
&lt;li&gt;smooth scrolling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perceptual Engine prioritizes those things first.&lt;/p&gt;




&lt;p&gt;What I Learned&lt;/p&gt;

&lt;p&gt;Building virtualization systems for desktop browsers is one thing.&lt;/p&gt;

&lt;p&gt;Building them for Android WebViews on low-end hardware is another level entirely.&lt;/p&gt;

&lt;p&gt;That environment forces you to care about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compositing behavior&lt;/li&gt;
&lt;li&gt;touch hit-testing&lt;/li&gt;
&lt;li&gt;GPU layer stability&lt;/li&gt;
&lt;li&gt;DOM recycling safety&lt;/li&gt;
&lt;li&gt;mobile rendering quirks&lt;/li&gt;
&lt;li&gt;focus preservation&lt;/li&gt;
&lt;li&gt;stacking contexts&lt;/li&gt;
&lt;li&gt;interaction consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And honestly, those constraints produced a much better architecture than I originally planned.&lt;/p&gt;




&lt;p&gt;What’s Next&lt;/p&gt;

&lt;p&gt;Planned improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grid virtualization&lt;/li&gt;
&lt;li&gt;masonry layouts&lt;/li&gt;
&lt;li&gt;sticky regions&lt;/li&gt;
&lt;li&gt;smarter predictive heuristics&lt;/li&gt;
&lt;li&gt;accessibility improvements&lt;/li&gt;
&lt;li&gt;React Native experiments&lt;/li&gt;
&lt;li&gt;worker-assisted scheduling&lt;/li&gt;
&lt;li&gt;improved measurement prediction&lt;/li&gt;
&lt;li&gt;advanced interaction preservation&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;Perceptual Engine started as a performance experiment.&lt;/p&gt;

&lt;p&gt;It evolved into a rendering system focused on one core idea:&lt;/p&gt;

&lt;p&gt;«Smoothness is not just about FPS.&lt;br&gt;
It’s about perception, interaction stability, and continuity.»&lt;/p&gt;

&lt;p&gt;And surprisingly, some of the hardest problems were not rendering problems at all.&lt;/p&gt;

&lt;p&gt;They were interaction problems.&lt;/p&gt;

&lt;p&gt;Especially inside Android WebViews.&lt;/p&gt;

&lt;p&gt;If you're interested in virtualization, rendering systems, scheduling, recycling strategies, or performance engineering, I’d love to hear your thoughts.&lt;/p&gt;

&lt;p&gt;GitHub repository coming soon.&lt;/p&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>react</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Perceptual Engine: Un motor de renderizado que predice el movimiento del usuario</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Mon, 25 May 2026 10:03:23 +0000</pubDate>
      <link>https://dev.to/gfouz/perceptual-engine-un-motor-de-renderizado-que-predice-el-movimiento-del-usuario-3em9</link>
      <guid>https://dev.to/gfouz/perceptual-engine-un-motor-de-renderizado-que-predice-el-movimiento-del-usuario-3em9</guid>
      <description>&lt;p&gt;🧠 Perceptual Engine: Un motor de renderizado que predice el movimiento del usuario&lt;/p&gt;

&lt;p&gt;La mayoría de las librerías de virtualización optimizan el número de nodos en el DOM.&lt;/p&gt;

&lt;p&gt;Yo me empecé a preguntar:&lt;/p&gt;

&lt;p&gt;¿Y si el problema real no es el tamaño del DOM… sino la percepción humana?&lt;/p&gt;

&lt;p&gt;Esa pregunta se convirtió en Perceptual Engine, un runtime de renderizado experimental para React centrado en la fluidez percibida, el desplazamiento predictivo, la calidad adaptativa y el renderizado consciente de la GPU.&lt;/p&gt;

&lt;p&gt;No se trata de "renderizar menos nodos".&lt;/p&gt;

&lt;p&gt;Se trata de renderizar lo que el usuario está a punto de necesitar.&lt;/p&gt;

&lt;p&gt;El problema que no se abordó  realmente&lt;/p&gt;

&lt;p&gt;Las librerías tradicionales de desplazamiento virtual son geométricas.&lt;/p&gt;

&lt;p&gt;Calculan la visibilidad así:&lt;/p&gt;

&lt;p&gt;scrollTop → índices visibles → renderizar&lt;/p&gt;

&lt;p&gt;Eso funciona… hasta que fuerzas la UI al límite:&lt;/p&gt;

&lt;p&gt;Formularios complejos&lt;/p&gt;

&lt;p&gt;WebViews de Android&lt;/p&gt;

&lt;p&gt;Inputs con teclados IME&lt;/p&gt;

&lt;p&gt;Cálculos en tiempo real&lt;/p&gt;

&lt;p&gt;Layouts dinámicos grandes&lt;/p&gt;

&lt;p&gt;Desplazamiento rápido con flick&lt;/p&gt;

&lt;p&gt;Ahí las cosas empiezan a fallar:&lt;/p&gt;

&lt;p&gt;Parpadeos&lt;/p&gt;

&lt;p&gt;Pérdida de foco&lt;/p&gt;

&lt;p&gt;Tirones en el scroll&lt;/p&gt;

&lt;p&gt;Caída de frames&lt;/p&gt;

&lt;p&gt;Rotación excesiva de portales&lt;/p&gt;

&lt;p&gt;Destrucción de estado&lt;/p&gt;

&lt;p&gt;Me topé con todo esto construyendo un sistema TPV que corre dentro de un WebView de Android.&lt;/p&gt;

&lt;p&gt;Y las soluciones existentes no estaban diseñadas para ese entorno.&lt;/p&gt;

&lt;p&gt;Lo que construí&lt;/p&gt;

&lt;p&gt;Perceptual Engine trata el desplazamiento como un problema de predicción de movimiento.&lt;/p&gt;

&lt;p&gt;En lugar de reaccionar solo a la posición del scroll, el motor analiza continuamente:&lt;/p&gt;

&lt;p&gt;velocidad&lt;/p&gt;

&lt;p&gt;aceleración&lt;/p&gt;

&lt;p&gt;sobreaceleración (cambio en la aceleración)&lt;/p&gt;

&lt;p&gt;distancia de frenado prevista&lt;/p&gt;

&lt;p&gt;presión de renderizado&lt;/p&gt;

&lt;p&gt;presupuesto de frames&lt;/p&gt;

&lt;p&gt;uso de capas de GPU&lt;/p&gt;

&lt;p&gt;Pipeline:&lt;/p&gt;

&lt;p&gt;ENTRADA ↓ ANÁLISIS DE MOVIMIENTO ↓ PREDICCIÓN ↓ VISIBILIDAD ↓ MEDICIÓN ↓ MUTACIÓN DEL DOM ↓ COMPOSICIÓN EN GPU&lt;/p&gt;

&lt;p&gt;Cada etapa puede:&lt;/p&gt;

&lt;p&gt;dividir el trabajo entre frames&lt;/p&gt;

&lt;p&gt;cancelarse a sí misma&lt;/p&gt;

&lt;p&gt;degradar la calidad&lt;/p&gt;

&lt;p&gt;repriorizar tareas&lt;/p&gt;

&lt;p&gt;Se comporta más como un pequeño motor de videojuegos que como una lista tradicional de React.&lt;/p&gt;

&lt;p&gt;Ideas centrales&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Overscan basado en movimiento&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En lugar de un overscan fijo:&lt;/p&gt;

&lt;p&gt;overscan={5}&lt;/p&gt;

&lt;p&gt;El motor predice hacia dónde se mueve el usuario usando la sobreaceleración y la velocidad.&lt;/p&gt;

&lt;p&gt;Los desplazamientos rápidos aumentan automáticamente el renderizado predictivo.&lt;/p&gt;

&lt;p&gt;Los movimientos lentos reducen el trabajo automáticamente.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sistema de calidad adaptativa&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si los FPS bajan:&lt;/p&gt;

&lt;p&gt;el overscan se reduce&lt;/p&gt;

&lt;p&gt;las capas de GPU disminuyen&lt;/p&gt;

&lt;p&gt;las predicciones se simplifican&lt;/p&gt;

&lt;p&gt;las mediciones costosas se posponen&lt;/p&gt;

&lt;p&gt;Muy similar a los sistemas de calidad adaptativa usados en motores de videojuegos.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pool de DOM persistente&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;En lugar de montar/desmontar constantemente:&lt;/p&gt;

&lt;p&gt;los nodos del DOM se mantienen vivos&lt;/p&gt;

&lt;p&gt;los elementos se reciclan a través de pools&lt;/p&gt;

&lt;p&gt;los portales inyectan contenido incrementalmente&lt;/p&gt;

&lt;p&gt;el estado sobrevive al desplazamiento&lt;/p&gt;

&lt;p&gt;Esto fue crítico para formularios complejos y teclados de Android.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Planificador con presupuesto de frames&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El planificador usa:&lt;/p&gt;

&lt;p&gt;prioridades&lt;/p&gt;

&lt;p&gt;división de tiempo (time slicing)&lt;/p&gt;

&lt;p&gt;lógica anti-inanición&lt;/p&gt;

&lt;p&gt;presupuestos de frames&lt;/p&gt;

&lt;p&gt;No todo el trabajo de renderizado merece la misma urgencia.&lt;/p&gt;

&lt;p&gt;API sencilla&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PerceptualList&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;perceptual-engine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductForms&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;products&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PerceptualList&lt;/span&gt;
      &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;estimatedItemSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;overscan&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"auto"&lt;/span&gt;
      &lt;span class="na"&gt;enableGPUCompositing&lt;/span&gt;
      &lt;span class="na"&gt;persistenceKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"product-forms"&lt;/span&gt;
      &lt;span class="na"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ProductForm&lt;/span&gt; &lt;span class="na"&gt;product&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El motor se encarga de:&lt;/p&gt;

&lt;p&gt;overscan predictivo&lt;/p&gt;

&lt;p&gt;reciclaje de DOM&lt;/p&gt;

&lt;p&gt;restauración del scroll&lt;/p&gt;

&lt;p&gt;degradación adaptativa&lt;/p&gt;

&lt;p&gt;composición en GPU&lt;/p&gt;

&lt;p&gt;planificación&lt;/p&gt;

&lt;p&gt;agrupación de mediciones&lt;/p&gt;

&lt;p&gt;Caso de uso real en producción&lt;/p&gt;

&lt;p&gt;Actualmente uso Perceptual Engine en producción dentro de un sistema TPV en WebView de Android.&lt;/p&gt;

&lt;p&gt;Escenario:&lt;/p&gt;

&lt;p&gt;15 formularios complejos simultáneos&lt;/p&gt;

&lt;p&gt;Cálculos en tiempo real&lt;/p&gt;

&lt;p&gt;Inputs numéricos&lt;/p&gt;

&lt;p&gt;Validación&lt;/p&gt;

&lt;p&gt;Totales dinámicos&lt;/p&gt;

&lt;p&gt;Alta interacción&lt;/p&gt;

&lt;p&gt;Hardware Android de gama baja&lt;/p&gt;

&lt;p&gt;Con virtualización tradicional:&lt;/p&gt;

&lt;p&gt;pérdida de foco&lt;/p&gt;

&lt;p&gt;parpadeo con IME&lt;/p&gt;

&lt;p&gt;estado que desaparece&lt;/p&gt;

&lt;p&gt;desplazamiento inestable&lt;/p&gt;

&lt;p&gt;Con Perceptual Engine:&lt;/p&gt;

&lt;p&gt;desplazamiento estable a 60fps&lt;/p&gt;

&lt;p&gt;interacción persistente&lt;/p&gt;

&lt;p&gt;cero parpadeos&lt;/p&gt;

&lt;p&gt;sin destrucción de foco&lt;/p&gt;

&lt;p&gt;Ese entorno moldeó toda la arquitectura.&lt;/p&gt;

&lt;p&gt;Arquitectura&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React
  ↓
PerceptualList
  ↓
usePerceptualEngine
  ↓
PerceptualEngine
  ├── MotionAnalyzer
  ├── LayoutPredictor
  ├── ViewportManager
  ├── RecyclingPool
  ├── CompositorLayer
  ├── Scheduler
  └── ScrollRestoration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qué lo hace diferente&lt;/p&gt;

&lt;p&gt;La mayoría de librerías de virtualización optimizan:&lt;/p&gt;

&lt;p&gt;memoria&lt;/p&gt;

&lt;p&gt;número de nodos en el DOM&lt;/p&gt;

&lt;p&gt;matemáticas de visibilidad&lt;/p&gt;

&lt;p&gt;Perceptual Engine optimiza:&lt;/p&gt;

&lt;p&gt;fluidez percibida&lt;/p&gt;

&lt;p&gt;continuidad de la interacción&lt;/p&gt;

&lt;p&gt;predicción de movimiento&lt;/p&gt;

&lt;p&gt;estabilidad de frames&lt;/p&gt;

&lt;p&gt;presión sobre la GPU&lt;/p&gt;

&lt;p&gt;Es una filosofía muy diferente.&lt;/p&gt;

&lt;p&gt;Estado actual&lt;/p&gt;

&lt;p&gt;El motor está actualmente en alfa avanzada.&lt;/p&gt;

&lt;p&gt;Funcionando hoy:&lt;/p&gt;

&lt;p&gt;✅ Overscan adaptativo&lt;/p&gt;

&lt;p&gt;✅ Pools de reciclaje de DOM&lt;/p&gt;

&lt;p&gt;✅ Caché incremental de portales&lt;/p&gt;

&lt;p&gt;✅ Restauración de scroll multiestrategia&lt;/p&gt;

&lt;p&gt;✅ Degradación consciente de los FPS&lt;/p&gt;

&lt;p&gt;✅ Agrupación compartida de ResizeObserver&lt;/p&gt;

&lt;p&gt;✅ Overlay de rendimiento&lt;/p&gt;

&lt;p&gt;✅ Controles de composición en GPU&lt;/p&gt;

&lt;p&gt;Hoja de ruta:&lt;/p&gt;

&lt;p&gt;🔜 Publicación en npm&lt;/p&gt;

&lt;p&gt;🔜 Benchmarks&lt;/p&gt;

&lt;p&gt;🔜 Renderizado sin portales&lt;/p&gt;

&lt;p&gt;🔜 Capa de preservación de foco&lt;/p&gt;

&lt;p&gt;🔜 Planificación basada en workers&lt;/p&gt;

&lt;p&gt;🔜 Suite de tests completa&lt;/p&gt;

&lt;p&gt;Limitaciones conocidas&lt;/p&gt;

&lt;p&gt;Los portales de React aún introducen sobrecarga a escalas extremas&lt;/p&gt;

&lt;p&gt;Los WebViews de Android antiguos pueden comportarse de forma inconsistente&lt;/p&gt;

&lt;p&gt;La API aún está evolucionando antes del lanzamiento en npm&lt;/p&gt;

&lt;p&gt;La documentación sigue en progreso&lt;/p&gt;

&lt;p&gt;Filosofía&lt;/p&gt;

&lt;p&gt;No estoy intentando construir "otra librería de virtualización".&lt;/p&gt;

&lt;p&gt;Estoy explorando cómo podría ser el renderizado si tratáramos el desplazamiento como una señal perceptiva en lugar de un problema geométrico.&lt;/p&gt;

&lt;p&gt;Los usuarios no experimentan:&lt;/p&gt;

&lt;p&gt;"índices visibles"&lt;/p&gt;

&lt;p&gt;Los usuarios experimentan:&lt;/p&gt;

&lt;p&gt;fluidez, continuidad y capacidad de respuesta.&lt;/p&gt;

&lt;p&gt;Eso cambia la arquitectura por completo.&lt;/p&gt;

&lt;p&gt;Buscando feedback&lt;/p&gt;

&lt;p&gt;Me encantaría recibir feedback de gente que trabaje con:&lt;/p&gt;

&lt;p&gt;WebViews de Android&lt;/p&gt;

&lt;p&gt;formularios complejos&lt;/p&gt;

&lt;p&gt;conjuntos de datos enormes&lt;/p&gt;

&lt;p&gt;casos límite de virtualización&lt;/p&gt;

&lt;p&gt;hardware de gama baja&lt;/p&gt;

&lt;p&gt;infraestructura de renderizado&lt;/p&gt;

&lt;p&gt;Especialmente si se han topado con limitaciones en soluciones existentes.&lt;/p&gt;

&lt;p&gt;GitHub&lt;/p&gt;

&lt;p&gt;github.com/fouzstack/perceptual-engine&lt;/p&gt;

&lt;p&gt;Lanzamiento en npm próximamente.&lt;/p&gt;

&lt;p&gt;¿Qué opinas?&lt;/p&gt;

&lt;p&gt;¿Es el renderizado predictivo el siguiente paso después de la virtualización?&lt;/p&gt;

</description>
      <category>performance</category>
      <category>react</category>
      <category>showdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Perceptual Engine — A Rendering Engine That Predicts User Movement</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Mon, 25 May 2026 09:37:27 +0000</pubDate>
      <link>https://dev.to/gfouz/perceptual-engine-a-rendering-engine-that-predicts-user-movement-21</link>
      <guid>https://dev.to/gfouz/perceptual-engine-a-rendering-engine-that-predicts-user-movement-21</guid>
      <description>&lt;p&gt;🧠 Perceptual Engine — A Rendering Engine That Predicts User Movement&lt;/p&gt;

&lt;p&gt;Most virtualization libraries optimize DOM count.&lt;/p&gt;

&lt;p&gt;I started wondering:&lt;/p&gt;

&lt;p&gt;«What if the real problem is not DOM size… but human perception?»&lt;/p&gt;

&lt;p&gt;That question became Perceptual Engine — an experimental rendering runtime for React focused on perceived smoothness, predictive scrolling, adaptive quality, and GPU-aware rendering.&lt;/p&gt;

&lt;p&gt;Not “render fewer nodes”.&lt;/p&gt;

&lt;p&gt;Render what the user is about to need.&lt;/p&gt;




&lt;p&gt;The Problem Nobody Really Solved&lt;/p&gt;

&lt;p&gt;Traditional virtual scrolling libraries are geometric.&lt;/p&gt;

&lt;p&gt;They calculate visibility like this:&lt;/p&gt;

&lt;p&gt;scrollTop → visible indexes → render&lt;/p&gt;

&lt;p&gt;That works… until you push the UI hard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex forms&lt;/li&gt;
&lt;li&gt;Android WebViews&lt;/li&gt;
&lt;li&gt;Inputs with IME keyboards&lt;/li&gt;
&lt;li&gt;Real-time calculations&lt;/li&gt;
&lt;li&gt;Large dynamic layouts&lt;/li&gt;
&lt;li&gt;Fast flick scrolling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then things start breaking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flicker&lt;/li&gt;
&lt;li&gt;Lost focus&lt;/li&gt;
&lt;li&gt;Scroll jitter&lt;/li&gt;
&lt;li&gt;Frame drops&lt;/li&gt;
&lt;li&gt;Portal churn&lt;/li&gt;
&lt;li&gt;State destruction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hit all of this while building a POS system running inside Android WebView.&lt;/p&gt;

&lt;p&gt;And existing solutions weren’t designed for that environment.&lt;/p&gt;




&lt;p&gt;What I Built&lt;/p&gt;

&lt;p&gt;Perceptual Engine treats scrolling as a motion prediction problem.&lt;/p&gt;

&lt;p&gt;Instead of reacting to scroll position only, the engine continuously analyzes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;velocity&lt;/li&gt;
&lt;li&gt;acceleration&lt;/li&gt;
&lt;li&gt;jerk (change in acceleration)&lt;/li&gt;
&lt;li&gt;predicted stopping distance&lt;/li&gt;
&lt;li&gt;rendering pressure&lt;/li&gt;
&lt;li&gt;frame budget&lt;/li&gt;
&lt;li&gt;GPU layer usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pipeline:&lt;/p&gt;

&lt;p&gt;INPUT&lt;br&gt;
  ↓&lt;br&gt;
MOTION ANALYSIS&lt;br&gt;
  ↓&lt;br&gt;
PREDICTION&lt;br&gt;
  ↓&lt;br&gt;
VISIBILITY&lt;br&gt;
  ↓&lt;br&gt;
MEASUREMENT&lt;br&gt;
  ↓&lt;br&gt;
DOM MUTATION&lt;br&gt;
  ↓&lt;br&gt;
GPU COMPOSITING&lt;/p&gt;

&lt;p&gt;Each stage can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;split work across frames&lt;/li&gt;
&lt;li&gt;cancel itself&lt;/li&gt;
&lt;li&gt;degrade quality&lt;/li&gt;
&lt;li&gt;reprioritize tasks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It behaves more like a small game engine than a traditional React list.&lt;/p&gt;




&lt;p&gt;Core Ideas&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Motion-Based Overscan&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of fixed overscan:&lt;/p&gt;

&lt;p&gt;overscan={5}&lt;/p&gt;

&lt;p&gt;The engine predicts where the user is moving next using jerk + velocity.&lt;/p&gt;

&lt;p&gt;Fast flicks increase predictive rendering automatically.&lt;/p&gt;

&lt;p&gt;Slow movement reduces work automatically.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Adaptive Quality System&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If FPS drops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;overscan shrinks&lt;/li&gt;
&lt;li&gt;GPU layers reduce&lt;/li&gt;
&lt;li&gt;predictions simplify&lt;/li&gt;
&lt;li&gt;expensive measurements defer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Very similar to adaptive quality systems used in game engines.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Persistent DOM Pool&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of constantly mounting/unmounting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM nodes stay alive&lt;/li&gt;
&lt;li&gt;elements recycle through pools&lt;/li&gt;
&lt;li&gt;portals inject content incrementally&lt;/li&gt;
&lt;li&gt;state survives scrolling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was critical for complex forms and Android keyboards.&lt;/p&gt;




&lt;ol&gt;
&lt;li&gt;Scheduler With Frame Budgeting&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scheduler uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;priorities&lt;/li&gt;
&lt;li&gt;time slicing&lt;/li&gt;
&lt;li&gt;anti-starvation logic&lt;/li&gt;
&lt;li&gt;frame budgets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all rendering work deserves the same urgency.&lt;/p&gt;




&lt;p&gt;Simple API&lt;/p&gt;

&lt;p&gt;import { PerceptualList } from 'perceptual-engine';&lt;/p&gt;

&lt;p&gt;function ProductForms({ products }) {&lt;br&gt;
  return (&lt;br&gt;
    
      items={products}&lt;br&gt;
      estimatedItemSize={600}&lt;br&gt;
      overscan="auto"&lt;br&gt;
      enableGPUCompositing&lt;br&gt;
      persistenceKey="product-forms"&lt;br&gt;
      renderItem={(product) =&amp;gt; (&lt;br&gt;
        &lt;br&gt;
      )}&lt;br&gt;
    /&amp;gt;&lt;br&gt;
  );&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The engine handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictive overscan&lt;/li&gt;
&lt;li&gt;DOM recycling&lt;/li&gt;
&lt;li&gt;scroll restoration&lt;/li&gt;
&lt;li&gt;adaptive degradation&lt;/li&gt;
&lt;li&gt;GPU compositing&lt;/li&gt;
&lt;li&gt;scheduling&lt;/li&gt;
&lt;li&gt;measurement batching&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Real Production Use Case&lt;/p&gt;

&lt;p&gt;I’m currently using Perceptual Engine in production inside an Android WebView POS system.&lt;/p&gt;

&lt;p&gt;Scenario:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15 simultaneous complex forms&lt;/li&gt;
&lt;li&gt;Real-time calculations&lt;/li&gt;
&lt;li&gt;Numeric inputs&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Dynamic totals&lt;/li&gt;
&lt;li&gt;Heavy interaction&lt;/li&gt;
&lt;li&gt;Low-end Android hardware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With traditional virtualization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;focus loss&lt;/li&gt;
&lt;li&gt;IME flicker&lt;/li&gt;
&lt;li&gt;disappearing state&lt;/li&gt;
&lt;li&gt;unstable scrolling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Perceptual Engine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stable 60fps scrolling&lt;/li&gt;
&lt;li&gt;persistent interaction&lt;/li&gt;
&lt;li&gt;zero flicker&lt;/li&gt;
&lt;li&gt;no focus destruction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That environment shaped the entire architecture.&lt;/p&gt;




&lt;p&gt;Architecture&lt;/p&gt;

&lt;p&gt;React&lt;br&gt;
  ↓&lt;br&gt;
PerceptualList&lt;br&gt;
  ↓&lt;br&gt;
usePerceptualEngine&lt;br&gt;
  ↓&lt;br&gt;
PerceptualEngine&lt;br&gt;
    ├── MotionAnalyzer&lt;br&gt;
    ├── LayoutPredictor&lt;br&gt;
    ├── ViewportManager&lt;br&gt;
    ├── RecyclingPool&lt;br&gt;
    ├── CompositorLayer&lt;br&gt;
    ├── Scheduler&lt;br&gt;
    └── ScrollRestoration&lt;/p&gt;




&lt;p&gt;What Makes It Different&lt;/p&gt;

&lt;p&gt;Most virtualization libraries optimize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;DOM count&lt;/li&gt;
&lt;li&gt;visibility math&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perceptual Engine optimizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;perceived smoothness&lt;/li&gt;
&lt;li&gt;interaction continuity&lt;/li&gt;
&lt;li&gt;motion prediction&lt;/li&gt;
&lt;li&gt;frame stability&lt;/li&gt;
&lt;li&gt;GPU pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s a very different philosophy.&lt;/p&gt;




&lt;p&gt;Current Status&lt;/p&gt;

&lt;p&gt;The engine is currently in advanced alpha.&lt;/p&gt;

&lt;p&gt;Working today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Adaptive overscan&lt;/li&gt;
&lt;li&gt;✅ DOM recycling pools&lt;/li&gt;
&lt;li&gt;✅ Incremental portal caching&lt;/li&gt;
&lt;li&gt;✅ Multi-strategy scroll restoration&lt;/li&gt;
&lt;li&gt;✅ FPS-aware degradation&lt;/li&gt;
&lt;li&gt;✅ Shared ResizeObserver batching&lt;/li&gt;
&lt;li&gt;✅ Performance overlay&lt;/li&gt;
&lt;li&gt;✅ GPU compositing controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roadmap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔜 npm release&lt;/li&gt;
&lt;li&gt;🔜 benchmarks&lt;/li&gt;
&lt;li&gt;🔜 portal-less rendering&lt;/li&gt;
&lt;li&gt;🔜 focus preservation layer&lt;/li&gt;
&lt;li&gt;🔜 worker-based scheduling&lt;/li&gt;
&lt;li&gt;🔜 full test suite&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Known Limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React portals still introduce overhead at extreme scales&lt;/li&gt;
&lt;li&gt;Legacy Android WebViews can behave inconsistently&lt;/li&gt;
&lt;li&gt;API still evolving before npm release&lt;/li&gt;
&lt;li&gt;Documentation still in progress&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Philosophy&lt;/p&gt;

&lt;p&gt;I’m not trying to build “another virtualization library”.&lt;/p&gt;

&lt;p&gt;I’m exploring what rendering could look like if we treated scrolling as a perceptual signal instead of a geometry problem.&lt;/p&gt;

&lt;p&gt;Users don’t experience:&lt;/p&gt;

&lt;p&gt;«“visible indexes”»&lt;/p&gt;

&lt;p&gt;Users experience:&lt;/p&gt;

&lt;p&gt;«smoothness, continuity, and responsiveness.»&lt;/p&gt;

&lt;p&gt;That changes the architecture completely.&lt;/p&gt;




&lt;p&gt;Looking for Feedback&lt;/p&gt;

&lt;p&gt;I’d love feedback from people working with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Android WebViews&lt;/li&gt;
&lt;li&gt;complex forms&lt;/li&gt;
&lt;li&gt;huge datasets&lt;/li&gt;
&lt;li&gt;virtualization edge cases&lt;/li&gt;
&lt;li&gt;low-end hardware&lt;/li&gt;
&lt;li&gt;rendering infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially if you’ve hit limitations with existing solutions.&lt;/p&gt;




&lt;p&gt;GitHub&lt;/p&gt;

&lt;p&gt;github.com/fouzstack/perceptual-engine&lt;/p&gt;

&lt;p&gt;npm release coming soon.&lt;/p&gt;




&lt;p&gt;What do you think?&lt;/p&gt;

&lt;p&gt;Is predictive rendering the next step after virtualization?&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>Turning Android WebView into a Real Local HTTP Runtime</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sat, 16 May 2026 04:04:42 +0000</pubDate>
      <link>https://dev.to/gfouz/turning-android-webview-into-a-real-local-http-runtime-1c6d</link>
      <guid>https://dev.to/gfouz/turning-android-webview-into-a-real-local-http-runtime-1c6d</guid>
      <description>&lt;p&gt;Turning Android WebView into a Real Local HTTP Runtime&lt;/p&gt;

&lt;p&gt;Most Android hybrid apps still load their frontend using:&lt;/p&gt;

&lt;p&gt;file:// &lt;/p&gt;

&lt;p&gt;And that comes with problems:&lt;/p&gt;

&lt;p&gt;broken SPA routing&lt;/p&gt;

&lt;p&gt;CORS limitations&lt;/p&gt;

&lt;p&gt;weak caching&lt;/p&gt;

&lt;p&gt;inconsistent offline behavior&lt;/p&gt;

&lt;p&gt;poor asset management&lt;/p&gt;

&lt;p&gt;no real HTTP semantics&lt;/p&gt;

&lt;p&gt;After fighting these issues repeatedly while building React-based Android applications, I started working on something different.&lt;/p&gt;

&lt;p&gt;Not another WebView wrapper.&lt;/p&gt;

&lt;p&gt;Not another local server dependency.&lt;/p&gt;

&lt;p&gt;A real embedded HTTP runtime for Android WebView.&lt;/p&gt;

&lt;p&gt;That project became:&lt;/p&gt;

&lt;p&gt;WebVirt v3.6.0&lt;/p&gt;

&lt;p&gt;A hybrid runtime engine that intercepts requests directly inside WebView and serves assets from the APK with:&lt;/p&gt;

&lt;p&gt;intelligent caching&lt;/p&gt;

&lt;p&gt;SPA fallback&lt;/p&gt;

&lt;p&gt;ETag support&lt;/p&gt;

&lt;p&gt;request coalescing&lt;/p&gt;

&lt;p&gt;precaching&lt;/p&gt;

&lt;p&gt;range requests&lt;/p&gt;

&lt;p&gt;CSP security headers&lt;/p&gt;

&lt;p&gt;zero configuration&lt;/p&gt;

&lt;p&gt;Why I Built It&lt;/p&gt;

&lt;p&gt;Modern SPAs expect a real HTTP environment.&lt;/p&gt;

&lt;p&gt;React, Vue, Angular, Vite, Svelte — they all assume things like:&lt;/p&gt;

&lt;p&gt;cache validation&lt;/p&gt;

&lt;p&gt;proper MIME types&lt;/p&gt;

&lt;p&gt;routing&lt;/p&gt;

&lt;p&gt;headers&lt;/p&gt;

&lt;p&gt;immutable assets&lt;/p&gt;

&lt;p&gt;partial requests&lt;/p&gt;

&lt;p&gt;But Android WebView with:&lt;/p&gt;

&lt;p&gt;file:// &lt;/p&gt;

&lt;p&gt;doesn't behave like a real runtime.&lt;/p&gt;

&lt;p&gt;So instead of forcing the frontend to adapt to WebView limitations…&lt;/p&gt;

&lt;p&gt;I decided to make WebView behave like a real web runtime.&lt;/p&gt;

&lt;p&gt;What WebVirt Actually Does&lt;/p&gt;

&lt;p&gt;WebVirt transforms Android WebView into a local virtual HTTP engine.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;WebVirt.with(context) .host("app.local") .bind(webView); webView.loadUrl("&lt;a href="https://app.local/%22" rel="noopener noreferrer"&gt;https://app.local/"&lt;/a&gt;); &lt;/p&gt;

&lt;p&gt;From WebView’s perspective:&lt;/p&gt;

&lt;p&gt;it’s loading HTTPS&lt;/p&gt;

&lt;p&gt;requests are real HTTP requests&lt;/p&gt;

&lt;p&gt;assets have cache headers&lt;/p&gt;

&lt;p&gt;ETags work&lt;/p&gt;

&lt;p&gt;SPA routing works&lt;/p&gt;

&lt;p&gt;offline works&lt;/p&gt;

&lt;p&gt;But everything is served locally from the APK.&lt;/p&gt;

&lt;p&gt;No external server.&lt;/p&gt;

&lt;p&gt;No localhost daemon.&lt;/p&gt;

&lt;p&gt;No internet required.&lt;/p&gt;

&lt;p&gt;Internal Architecture&lt;/p&gt;

&lt;p&gt;WebVirt uses a modular runtime architecture built around a central orchestrator:&lt;/p&gt;

&lt;p&gt;WebVirt └── WebVirtFileLoader ├── RequestRouter ├── AssetLoader ├── ResponseCache ├── ResponseHeaders ├── AssetPreloader ├── RequestCoalescer ├── RangeRequestHandler └── ErrorPages &lt;/p&gt;

&lt;p&gt;The goal was simple:&lt;/p&gt;

&lt;p&gt;minimal overhead&lt;/p&gt;

&lt;p&gt;predictable behavior&lt;/p&gt;

&lt;p&gt;low memory pressure&lt;/p&gt;

&lt;p&gt;high cache efficiency&lt;/p&gt;

&lt;p&gt;production-ready concurrency&lt;/p&gt;

&lt;p&gt;Performance Results&lt;/p&gt;

&lt;p&gt;One of the things that surprised me most was how efficient the runtime became after optimization.&lt;/p&gt;

&lt;p&gt;Real metrics from Android WebView:&lt;/p&gt;

&lt;p&gt;AssetSizeLoad Timevendor.js510 KB25–34msmain.js279 KB19–26msCSS bundle362 KB16–22msDexie bundle96 KB8–17ms &lt;/p&gt;

&lt;p&gt;Total initial payload:&lt;/p&gt;

&lt;p&gt;~1.4 MB &lt;/p&gt;

&lt;p&gt;Despite that, repeated cached assets frequently return in:&lt;/p&gt;

&lt;p&gt;0ms &lt;/p&gt;

&lt;p&gt;which means:&lt;/p&gt;

&lt;p&gt;cache hits are real&lt;/p&gt;

&lt;p&gt;headers are precomputed&lt;/p&gt;

&lt;p&gt;responses are already materialized&lt;/p&gt;

&lt;p&gt;the hot path is highly optimized&lt;/p&gt;

&lt;p&gt;At this point, the bottleneck is no longer the runtime itself.&lt;/p&gt;

&lt;p&gt;The heavier costs now come from:&lt;/p&gt;

&lt;p&gt;React rendering&lt;/p&gt;

&lt;p&gt;V8 parsing&lt;/p&gt;

&lt;p&gt;CSS&lt;/p&gt;

&lt;p&gt;animation libraries&lt;/p&gt;

&lt;p&gt;frontend hydration&lt;/p&gt;

&lt;p&gt;Which is exactly where I wanted the runtime to be.&lt;/p&gt;

&lt;p&gt;Features&lt;/p&gt;

&lt;p&gt;SPA Fallback&lt;/p&gt;

&lt;p&gt;React Router, Vue Router and Angular routing work automatically.&lt;/p&gt;

&lt;p&gt;/dashboard/settings → index.html &lt;/p&gt;

&lt;p&gt;No extra configuration needed.&lt;/p&gt;

&lt;p&gt;HTTP Cache Validation&lt;/p&gt;

&lt;p&gt;WebVirt supports:&lt;/p&gt;

&lt;p&gt;ETag&lt;/p&gt;

&lt;p&gt;304 Not Modified&lt;/p&gt;

&lt;p&gt;If-Modified-Since&lt;/p&gt;

&lt;p&gt;immutable cache policies&lt;/p&gt;

&lt;p&gt;This dramatically reduces unnecessary asset transfers.&lt;/p&gt;

&lt;p&gt;Intelligent Precaching&lt;/p&gt;

&lt;p&gt;Critical assets are preloaded automatically:&lt;/p&gt;

&lt;p&gt;index.html&lt;/p&gt;

&lt;p&gt;main bundles&lt;/p&gt;

&lt;p&gt;global CSS&lt;/p&gt;

&lt;p&gt;runtime assets&lt;/p&gt;

&lt;p&gt;With backpressure limits to avoid memory issues.&lt;/p&gt;

&lt;p&gt;Request Coalescing&lt;/p&gt;

&lt;p&gt;Concurrent requests to the same asset are merged into a single real load.&lt;/p&gt;

&lt;p&gt;1 disk read → N consumers &lt;/p&gt;

&lt;p&gt;This reduced redundant IO significantly during startup.&lt;/p&gt;

&lt;p&gt;Security Headers&lt;/p&gt;

&lt;p&gt;WebVirt injects:&lt;/p&gt;

&lt;p&gt;CSP&lt;/p&gt;

&lt;p&gt;X-Content-Type-Options&lt;/p&gt;

&lt;p&gt;X-Frame-Options&lt;/p&gt;

&lt;p&gt;CORS policies&lt;/p&gt;

&lt;p&gt;directly into responses.&lt;/p&gt;

&lt;p&gt;One Interesting Discovery&lt;/p&gt;

&lt;p&gt;The more optimized WebVirt became…&lt;/p&gt;

&lt;p&gt;the more obvious it became that the real bottleneck in hybrid apps is often not WebView itself.&lt;/p&gt;

&lt;p&gt;It’s:&lt;/p&gt;

&lt;p&gt;oversized frontend bundles&lt;/p&gt;

&lt;p&gt;CSS frameworks&lt;/p&gt;

&lt;p&gt;hydration cost&lt;/p&gt;

&lt;p&gt;runtime JS weight&lt;/p&gt;

&lt;p&gt;After optimizing the runtime pipeline, frontend architecture suddenly mattered much more.&lt;/p&gt;

&lt;p&gt;That was a fascinating shift.&lt;/p&gt;

&lt;p&gt;Use Cases&lt;/p&gt;

&lt;p&gt;WebVirt works especially well for:&lt;/p&gt;

&lt;p&gt;offline-first apps&lt;/p&gt;

&lt;p&gt;POS systems&lt;/p&gt;

&lt;p&gt;kiosk apps&lt;/p&gt;

&lt;p&gt;enterprise dashboards&lt;/p&gt;

&lt;p&gt;embedded admin panels&lt;/p&gt;

&lt;p&gt;local business tools&lt;/p&gt;

&lt;p&gt;React/Vite Android apps&lt;/p&gt;

&lt;p&gt;internal tools&lt;/p&gt;

&lt;p&gt;educational offline apps&lt;/p&gt;

&lt;p&gt;Current State&lt;/p&gt;

&lt;p&gt;WebVirt currently includes:&lt;/p&gt;

&lt;p&gt;intelligent HTTP cache&lt;/p&gt;

&lt;p&gt;lock-free request routing&lt;/p&gt;

&lt;p&gt;asset precaching&lt;/p&gt;

&lt;p&gt;SPA fallback&lt;/p&gt;

&lt;p&gt;range requests&lt;/p&gt;

&lt;p&gt;metrics system&lt;/p&gt;

&lt;p&gt;cache statistics&lt;/p&gt;

&lt;p&gt;memory trimming&lt;/p&gt;

&lt;p&gt;configurable security policies&lt;/p&gt;

&lt;p&gt;custom path handlers&lt;/p&gt;

&lt;p&gt;offline runtime support&lt;/p&gt;

&lt;p&gt;And it’s still evolving.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;I originally built WebVirt to solve practical problems inside Android WebView.&lt;/p&gt;

&lt;p&gt;But over time it evolved into something much bigger:&lt;/p&gt;

&lt;p&gt;a lightweight hybrid runtime layer for Android.&lt;/p&gt;

&lt;p&gt;Not a wrapper.&lt;/p&gt;

&lt;p&gt;Not a helper.&lt;/p&gt;

&lt;p&gt;A real embedded web runtime.&lt;/p&gt;

&lt;p&gt;And honestly, watching a React SPA behave like a real cached HTTP application entirely inside Android WebView is surprisingly satisfying.&lt;/p&gt;

&lt;p&gt;If you're building hybrid Android apps, I’d love to hear your thoughts.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Native-Like Android Performance Using Standard Web Technologies ⚡</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Wed, 13 May 2026 02:57:39 +0000</pubDate>
      <link>https://dev.to/gfouz/native-like-android-performance-using-standard-web-technologies-k1n</link>
      <guid>https://dev.to/gfouz/native-like-android-performance-using-standard-web-technologies-k1n</guid>
      <description>&lt;p&gt;Native-Like Android Performance Using Standard Web Technologies ⚡&lt;/p&gt;

&lt;p&gt;What if a web-based Android app could boot almost as fast as a native app — while using a fraction of the memory compared to React Native, Flutter, or Electron?&lt;/p&gt;

&lt;p&gt;After benchmarking WebVirt v3.5.1, the results were honestly surprising.&lt;/p&gt;

&lt;p&gt;🏆 Benchmark Results&lt;/p&gt;

&lt;p&gt;MetricResultRatingCold Start255ms🟢 ExcellentWarm Start38ms🟢 InstantMemory Usage14.4MB🟢 MinimalCache Hit Rate100%🟢 PerfectPreload Improvement-20.3%🟢 Significant &lt;/p&gt;

&lt;p&gt;📊 Performance Comparison&lt;/p&gt;

&lt;p&gt;TechnologyCold StartWarm StartMemoryBundle SizeWebVirt255ms38ms14.4MB1.4MBReact Native800-1200ms200-400ms40-60MB8-15MBFlutter600-900ms150-300ms35-50MB5-10MBPWA (Chrome)400-800ms100-200ms20-30MB1-3MBIonic/Capacitor1000-2000ms300-500ms45-70MB3-8MBNative Android200-400ms30-80ms10-20MB500KB-2MBElectron1500-3000ms500-1000ms80-150MB50-100MB &lt;/p&gt;

&lt;p&gt;📈 Cold Start Performance&lt;/p&gt;

&lt;p&gt;Cold Start (lower is better) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WebVirt 255ms ██████░░░░░░░░░░░░░░░░░░ Native 300ms ████████░░░░░░░░░░░░░░░░░ Flutter 750ms ██████████████████░░░░░░░ React Native 1000ms ████████████████████████░░ Ionic 1500ms ████████████████████████████████████ Electron 2250ms ██████████████████████████████████████████████████ &lt;/p&gt;

&lt;p&gt;⚡ Warm Start Performance&lt;/p&gt;

&lt;p&gt;Warm Start (lower is better) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WebVirt 38ms █░░░░░░░░░░░░░░░░░░░░░░ Native 55ms ██░░░░░░░░░░░░░░░░░░░░░ PWA 150ms █████░░░░░░░░░░░░░░░░░░ Flutter 225ms ████████░░░░░░░░░░░░░░░ React Native 300ms ██████████░░░░░░░░░░░░░ Ionic 400ms █████████████░░░░░░░░░░ Electron 750ms ████████████████████████ &lt;/p&gt;

&lt;p&gt;🔬 Detailed Resource Loading Metrics&lt;/p&gt;

&lt;p&gt;ResourceTypeSizeFirst LoadCachedindex.htmlHTML577B67msInstantindex-&lt;em&gt;.cssCSS354KB118msCachedindex-&lt;/em&gt;.jsJS1.04MB152msCachedroot-*.jsJS24KB83msCachedwww.pngPNG11KB82msCachedandroid.pngPNG40KB66msCached &lt;/p&gt;

&lt;p&gt;🎯 Smart Preloading Efficiency&lt;/p&gt;

&lt;p&gt;BEFORE preloading: Cold 320ms | Warm 40ms | Mem 15.5MB AFTER preloading: Cold 255ms | Warm 38ms | Mem 14.4MB ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ IMPROVEMENT: -20.3% -5% -7.1% &lt;/p&gt;

&lt;p&gt;💪 Why WebVirt Performs So Well&lt;/p&gt;

&lt;p&gt;vs React Native&lt;/p&gt;

&lt;p&gt;✅ 74% faster cold start&lt;/p&gt;

&lt;p&gt;✅ 87% faster warm start&lt;/p&gt;

&lt;p&gt;✅ 72% lower memory usage&lt;/p&gt;

&lt;p&gt;✅ No JavaScript bridge overhead&lt;/p&gt;

&lt;p&gt;vs Flutter&lt;/p&gt;

&lt;p&gt;✅ 66% faster cold start&lt;/p&gt;

&lt;p&gt;✅ 83% faster warm start&lt;/p&gt;

&lt;p&gt;✅ 67% lower memory usage&lt;/p&gt;

&lt;p&gt;✅ Uses standard HTML/CSS/JS&lt;/p&gt;

&lt;p&gt;vs PWA&lt;/p&gt;

&lt;p&gt;✅ 36% faster cold start&lt;/p&gt;

&lt;p&gt;✅ 75% faster warm start&lt;/p&gt;

&lt;p&gt;✅ 35% lower memory usage&lt;/p&gt;

&lt;p&gt;✅ Full native API access&lt;/p&gt;

&lt;p&gt;vs Ionic/Capacitor&lt;/p&gt;

&lt;p&gt;✅ 83% faster cold start&lt;/p&gt;

&lt;p&gt;✅ 90% faster warm start&lt;/p&gt;

&lt;p&gt;✅ 73% lower memory usage&lt;/p&gt;

&lt;p&gt;✅ No WebView bridge overhead&lt;/p&gt;

&lt;p&gt;🏗️ Architecture Overview&lt;/p&gt;

&lt;p&gt;┌─────────────────────────────────────┐ │ ANDROID APPLICATION │ ├─────────────────────────────────────┤ │ WebVirt Runtime Engine │ │ ┌───────────────────────────────┐ │ │ │ FileLoader + Precache │ │ │ │ ├─ Smart Cache (ETag) │ │ │ │ ├─ Request Coalescing │ │ │ │ ├─ GZip/Brotli Compression │ │ │ │ └─ Async Preloading │ │ │ └───────────────────────────────┘ │ │ ┌───────────────────────────────┐ │ │ │ Optimized WebView │ │ │ │ ├─ V8/JavaScriptCore │ │ │ │ ├─ Native Rendering │ │ │ │ └─ Hardware Acceleration │ │ │ └───────────────────────────────┘ │ └─────────────────────────────────────┘ &lt;/p&gt;

&lt;p&gt;📋 Production Metrics&lt;/p&gt;

&lt;p&gt;Benchmark Quick (2 iterations)&lt;/p&gt;

&lt;p&gt;metric,value coldStart_ms,255 warmStart_ms,38 memory_kb,14374 timestamp,1778640627136 &lt;/p&gt;

&lt;p&gt;FileLoader Metrics&lt;/p&gt;

&lt;p&gt;PRECACHE: 5/5 assets cached (100%) CACHE_HIT: 100% critical assets REQUEST COALESCING: No race conditions COMPRESSION: Enabled SECURITY: CSP, CORS, XSS Protection &lt;/p&gt;

&lt;p&gt;🔒 Production-Ready Security&lt;/p&gt;

&lt;p&gt;✅ Content Security Policy (CSP) ✅ CORS Headers ✅ XSS Protection ✅ ETag Cache Validation ✅ Range Requests (Streaming) ✅ Subresource Integrity ✅ Path Traversal Protection ✅ Memory Trim on Low Memory &lt;/p&gt;

&lt;p&gt;📈 Roadmap&lt;/p&gt;

&lt;p&gt;VersionFeatureExpected Impactv3.5.1Smart Preloading-20% cold startv3.6.0Automatic Code Splitting-25% bundle sizev3.7.0Offline Service WorkerFull offline supportv4.0.0Multi-thread Rendering-40% render time &lt;/p&gt;

&lt;p&gt;🎯 Best Use Cases&lt;/p&gt;

&lt;p&gt;Use CaseWebVirtReact NativeFlutterPWAEnterprise Apps🟢 Ideal🟡 Acceptable🟡 Acceptable🔴 LimitedE-commerce🟢 Ideal🟡 Acceptable🟢 Good🟡 AcceptableAdmin Dashboards🟢 Ideal🟡 Acceptable🟢 Good🟡 AcceptableSocial Apps🟡 Acceptable🟢 Ideal🟢 Ideal🔴 LimitedCasual Games🔴 Limited🟢 Good🟢 Ideal🔴 LimitedMVPs / Prototypes🟢 Ideal🟡 Acceptable🟡 Acceptable🟢 Good &lt;/p&gt;

&lt;p&gt;🏁 Final Thoughts&lt;/p&gt;

&lt;p&gt;WebVirt demonstrates that web technologies on Android do not necessarily mean slow startup times or excessive memory usage.&lt;/p&gt;

&lt;p&gt;With:&lt;/p&gt;

&lt;p&gt;🚀 255ms cold start&lt;/p&gt;

&lt;p&gt;⚡ 38ms warm start&lt;/p&gt;

&lt;p&gt;💾 14.4MB memory usage&lt;/p&gt;

&lt;p&gt;🎯 100% cache hit rate&lt;/p&gt;

&lt;p&gt;🔒 Built-in production security&lt;/p&gt;

&lt;p&gt;…it’s possible to achieve near-native performance while keeping the flexibility and developer experience of standard web technologies.&lt;/p&gt;

&lt;p&gt;This could be especially valuable for startups and teams wanting:&lt;/p&gt;

&lt;p&gt;Faster development cycles&lt;/p&gt;

&lt;p&gt;Shared web expertise&lt;/p&gt;

&lt;p&gt;Smaller APKs&lt;/p&gt;

&lt;p&gt;Lower memory consumption&lt;/p&gt;

&lt;p&gt;Native-level responsiveness&lt;/p&gt;

&lt;p&gt;📚 References&lt;/p&gt;

&lt;p&gt;Android Developer Benchmarks&lt;/p&gt;

&lt;p&gt;React Native Performance Docs&lt;/p&gt;

&lt;p&gt;Flutter Performance Best Practices&lt;/p&gt;

&lt;p&gt;Google Web Vitals&lt;/p&gt;

&lt;p&gt;PWA Benchmark Studies (2025)&lt;/p&gt;

&lt;h1&gt;
  
  
  android #webdev #performance #reactnative #flutter #webview #java #opensource #mobiledev #benchmark
&lt;/h1&gt;

</description>
      <category>android</category>
      <category>performance</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Capacitor: 8–15 ms con servidor. WebVirt: 4–5 ms con interceptor. La diferencia es arquitectónica.</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 10 May 2026 04:29:31 +0000</pubDate>
      <link>https://dev.to/gfouz/capacitor-8-15-ms-con-servidor-webvirt-4-5-ms-con-interceptor-la-diferencia-es-arquitectonica-3e5k</link>
      <guid>https://dev.to/gfouz/capacitor-8-15-ms-con-servidor-webvirt-4-5-ms-con-interceptor-la-diferencia-es-arquitectonica-3e5k</guid>
      <description>&lt;p&gt;Capacitor: 8–15 ms con servidor. WebVirt: 4–5 ms con interceptor. La diferencia es arquitectónica.&lt;/p&gt;

&lt;p&gt;WebVirt: El Runtime Web para Android que Hace que las SPAs Vuelen 🚀&lt;/p&gt;

&lt;p&gt;5ms por asset. 64.7% cache hit rate. Sin servidor HTTP. Sin plugins pesados.&lt;/p&gt;

&lt;p&gt;Mientras muchos siguen metiendo servidores embebidos dentro de WebViews… yo probé otro enfoque.&lt;/p&gt;

&lt;p&gt;Y los resultados fueron abruptamente rápidos.&lt;/p&gt;




&lt;p&gt;🤔 El Problema Real de las SPAs en Android&lt;/p&gt;

&lt;p&gt;Construir una SPA React es fácil.&lt;/p&gt;

&lt;p&gt;Hacer que funcione bien dentro de Android WebView… no tanto.&lt;/p&gt;

&lt;p&gt;Normalmente terminamos usando:&lt;/p&gt;

&lt;p&gt;· Capacitor&lt;br&gt;
· Cordova&lt;br&gt;
· NanoHTTPD&lt;br&gt;
· file://&lt;br&gt;
· o algún servidor HTTP embebido&lt;/p&gt;

&lt;p&gt;Todos funcionan.&lt;/p&gt;

&lt;p&gt;Pero casi todos añaden:&lt;/p&gt;

&lt;p&gt;· Latencia innecesaria&lt;br&gt;
· Más consumo de RAM&lt;br&gt;
· Más complejidad&lt;br&gt;
· Más superficie de ataque&lt;br&gt;
· Más APK size&lt;/p&gt;

&lt;p&gt;Entonces me hice una pregunta:&lt;/p&gt;

&lt;p&gt;¿Y si el WebView nunca necesitó un servidor HTTP real?&lt;/p&gt;

&lt;p&gt;Así comenzó WebVirt.&lt;/p&gt;



&lt;p&gt;⚡ ¿Qué es WebVirt?&lt;/p&gt;

&lt;p&gt;WebVirt es un runtime web para Android que sirve SPAs directamente desde assets/ usando interceptación nativa del WebView.&lt;/p&gt;

&lt;p&gt;👉 Código abierto en GitHub: github.com/fouzstack/webvirt-engine&lt;/p&gt;

&lt;p&gt;Sin sockets.&lt;br&gt;
Sin puertos.&lt;br&gt;
Sin threads HTTP.&lt;br&gt;
Sin dependencias externas.&lt;/p&gt;

&lt;p&gt;Solo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App → WebView → Interceptor → Asset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;📊 Resultados Reales&lt;/p&gt;

&lt;p&gt;Probé WebVirt con una SPA React real (~1.6MB).&lt;/p&gt;

&lt;p&gt;Resultados capturados en Android:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════════════════════╗
║     WEBVIRT ENGINE - PERFORMANCE REPORT         ║
╠══════════════════════════════════════════════════╣
║  Session duration:           15945 ms           ║
║  Total assets loaded:        17                 ║
║  Avg load time:              5 ms               ║
║  Min load time:              0 ms               ║
║  Max load time:              48 ms              ║
╠══════════════════════════════════════════════════╣
║  Cache hits:                 11                 ║
║  Cache misses:               6                  ║
║  Cache hit rate:             64.7%              ║
║  Total bytes loaded:         1633735 bytes      ║
╠══════════════════════════════════════════════════╣
║  HTTP errors:                0                  ║
║  SPA fallbacks:              1                  ║
║  Range requests:             0                  ║
╚══════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🔥 Lo Más Interesante&lt;/p&gt;

&lt;p&gt;✅ 5ms promedio por asset&lt;/p&gt;

&lt;p&gt;Eso es prácticamente velocidad nativa.&lt;/p&gt;

&lt;p&gt;✅ 64.7% cache hit rate en frío&lt;/p&gt;

&lt;p&gt;La mayoría de frameworks ni siquiera tienen caché LRU interna para assets.&lt;br&gt;
WebVirt sí.&lt;/p&gt;

&lt;p&gt;✅ SPA Routing Automático&lt;/p&gt;

&lt;p&gt;Esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dashboard
/settings
/profile/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;funciona automáticamente.&lt;/p&gt;

&lt;p&gt;Sin .htaccess.&lt;br&gt;
Sin rewrites.&lt;br&gt;
Sin servidor real.&lt;/p&gt;



&lt;p&gt;🆚 Comparativa Directa&lt;/p&gt;

&lt;p&gt;Solución Velocidad Caché inteligente SPA Routing Peso&lt;br&gt;
WebVirt 4-5ms ✅ LRU integrada ✅ Automático ~30KB&lt;br&gt;
Capacitor 8-15ms ❌ HTTP estándar ✅ Plugins ~2MB&lt;br&gt;
Cordova 10-20ms ❌ HTTP estándar ✅ Plugins ~1.5MB&lt;br&gt;
NanoHTTPD 10-25ms ❌ Manual ❌ Manual ~100KB&lt;br&gt;
file:// 2-5ms ❌ Ninguno ❌ Roto 0KB&lt;/p&gt;



&lt;p&gt;🧠 ¿Por Qué es Más Rápido?&lt;/p&gt;

&lt;p&gt;Porque elimina capas innecesarias.&lt;/p&gt;

&lt;p&gt;Stack tradicional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App → WebView → Plugin → HTTP Server → Socket → File
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stack de WebVirt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App → WebView → Interceptor → Asset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hay:&lt;/p&gt;

&lt;p&gt;· sockets&lt;br&gt;
· serialización HTTP real&lt;br&gt;
· threads extra&lt;br&gt;
· networking interno&lt;/p&gt;

&lt;p&gt;Solo lectura optimizada desde assets.&lt;/p&gt;



&lt;p&gt;🏗️ Integración en 3 Líneas&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y listo.&lt;/p&gt;

&lt;p&gt;Tu SPA empieza a funcionar con:&lt;/p&gt;

&lt;p&gt;· HTTPS virtual&lt;br&gt;
· caché LRU&lt;br&gt;
· métricas integradas&lt;br&gt;
· CSP&lt;br&gt;
· soporte SPA&lt;br&gt;
· range requests&lt;br&gt;
· headers automáticos&lt;/p&gt;



&lt;p&gt;📈 Métricas Integradas&lt;/p&gt;

&lt;p&gt;WebVirt incluye un sistema de métricas extensible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withMetricsCollector&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FirebaseCollector&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puedes conectar:&lt;/p&gt;

&lt;p&gt;· Firebase&lt;br&gt;
· Datadog&lt;br&gt;
· Sentry&lt;br&gt;
· tu propio collector&lt;/p&gt;

&lt;p&gt;Y obtener reportes reales de rendimiento.&lt;/p&gt;



&lt;p&gt;🔒 Seguridad por Defecto&lt;/p&gt;

&lt;p&gt;Cada respuesta incluye:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Security&lt;/span&gt;-&lt;span class="n"&gt;Policy&lt;/span&gt;
&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Frame&lt;/span&gt;-&lt;span class="n"&gt;Options&lt;/span&gt;
&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;Content&lt;/span&gt;-&lt;span class="n"&gt;Type&lt;/span&gt;-&lt;span class="n"&gt;Options&lt;/span&gt;
&lt;span class="n"&gt;X&lt;/span&gt;-&lt;span class="n"&gt;XSS&lt;/span&gt;-&lt;span class="n"&gt;Protection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin configuración manual.&lt;/p&gt;




&lt;p&gt;🎯 ¿Para Quién es Esto?&lt;/p&gt;

&lt;p&gt;WebVirt está pensado para:&lt;/p&gt;

&lt;p&gt;· React + Android WebView&lt;br&gt;
· Vue + Android&lt;br&gt;
· Angular offline-first&lt;br&gt;
· Apps híbridas ultra rápidas&lt;br&gt;
· Equipos cansados de servidores embebidos&lt;/p&gt;




&lt;p&gt;🚫 Lo Que NO Es&lt;/p&gt;

&lt;p&gt;WebVirt NO intenta reemplazar:&lt;/p&gt;

&lt;p&gt;· Capacitor plugins&lt;br&gt;
· React Native&lt;br&gt;
· Flutter&lt;br&gt;
· servidores HTTP reales&lt;/p&gt;

&lt;p&gt;No compite ahí.&lt;/p&gt;

&lt;p&gt;WebVirt resuelve otra cosa:&lt;/p&gt;

&lt;p&gt;Ejecutar SPAs dentro de Android con el menor overhead posible.&lt;/p&gt;




&lt;p&gt;🧩 La Idea Más Importante&lt;/p&gt;

&lt;p&gt;Capacitor y Cordova tratan al WebView como "un navegador conectado a un servidor".&lt;/p&gt;

&lt;p&gt;WebVirt trata al WebView como un runtime local.&lt;/p&gt;

&lt;p&gt;Ese cambio arquitectónico cambia todo.&lt;/p&gt;




&lt;p&gt;🏆 Conclusión&lt;/p&gt;

&lt;p&gt;WebVirt probablemente no sea para todos.&lt;/p&gt;

&lt;p&gt;Pero si estás construyendo:&lt;/p&gt;

&lt;p&gt;· SPAs offline-first&lt;br&gt;
· dashboards locales&lt;br&gt;
· apps empresariales híbridas&lt;br&gt;
· runtimes React embebidos&lt;/p&gt;

&lt;p&gt;…el modelo tradicional empieza a sentirse innecesariamente pesado.&lt;/p&gt;

&lt;p&gt;Y honestamente:&lt;/p&gt;

&lt;p&gt;Ver una SPA React cargar assets en ~4ms dentro de Android se siente estupendamente bien.&lt;/p&gt;

&lt;p&gt;🔗 Repo en GitHub: github.com/fouzstack/webvirt-engine&lt;/p&gt;




&lt;p&gt;¿Qué opinas?&lt;br&gt;
¿Seguimos necesitando servidores HTTP embebidos dentro de WebViews en 2026?&lt;/p&gt;




&lt;h1&gt;
  
  
  Android #WebView #React #SPA #Performance #OpenSource #Capacitor #Cordova #MobileDev #Java
&lt;/h1&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>performance</category>
      <category>spanish</category>
    </item>
    <item>
      <title>I Served My React SPA from Android Assets Like a Professional Web Server</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 03 May 2026 18:50:28 +0000</pubDate>
      <link>https://dev.to/gfouz/i-served-my-react-spa-from-android-assets-like-a-professional-web-server-bl6</link>
      <guid>https://dev.to/gfouz/i-served-my-react-spa-from-android-assets-like-a-professional-web-server-bl6</guid>
      <description>&lt;p&gt;🚀 I Served My React SPA from Android Assets Like a Professional Web Server — Here's What Happened&lt;/p&gt;

&lt;p&gt;First load: 77ms. Reload: 2ms. 38x faster with LRU cache. No server, no permissions, no dependencies.&lt;/p&gt;




&lt;p&gt;🤔 The Problem Every React Dev Faces&lt;/p&gt;

&lt;p&gt;You've got your SPA running perfectly on localhost:5173. React, TypeScript, TailwindCSS, React Router, lazy loading... everything works beautifully.&lt;/p&gt;

&lt;p&gt;Now you need to take it to Android.&lt;/p&gt;

&lt;p&gt;Your traditional options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Option 1: Capacitor — 30MB runtime, complex config&lt;/span&gt;
&lt;span class="c1"&gt;// Option 2: Cordova — 15MB, outdated plugins&lt;/span&gt;
&lt;span class="c1"&gt;// Option 3: file:// protocol — broken CORS, SPA routes don't work&lt;/span&gt;
&lt;span class="c1"&gt;// Option 4: 50-line homemade script — fragile, no cache, no security&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of them feel right. You want something lightweight, fast, secure, and respectful of your architecture.&lt;/p&gt;




&lt;p&gt;✨ The Solution: WebVirt Engine&lt;/p&gt;

&lt;p&gt;An Android library of ~600 lines that simulates a virtual web server inside the WebView. Your SPA thinks it's at &lt;a href="https://app.local" rel="noopener noreferrer"&gt;https://app.local&lt;/a&gt;, but everything comes from assets/.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 5 lines. That's it.&lt;/span&gt;
&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all. Your React app running. SPA routes intact. No weird configuration.&lt;/p&gt;




&lt;p&gt;🔬 But Don't Take My Word for It. Look at the Real Data.&lt;/p&gt;

&lt;p&gt;To validate that WebVirt Engine was as fast as promised, I needed real metrics. Not synthetic benchmarks. Not "it feels fast." Cold, hard data.&lt;/p&gt;

&lt;p&gt;The Secret Weapon: WebVirtMetrics&lt;/p&gt;

&lt;p&gt;WebVirt Engine includes an optional metrics module that captures every asset load in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Enable only in debug. Zero overhead in production.&lt;/span&gt;
&lt;span class="nc"&gt;WebVirtMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEBUG&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;WebVirtMetrics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startSession&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Every asset WebVirt loads gets recorded:&lt;/span&gt;
&lt;span class="c1"&gt;// - File path&lt;/span&gt;
&lt;span class="c1"&gt;// - Load time in milliseconds&lt;/span&gt;
&lt;span class="c1"&gt;// - Whether it came from cache or disk&lt;/span&gt;
&lt;span class="c1"&gt;// - Size in bytes&lt;/span&gt;
&lt;span class="c1"&gt;// - MIME type&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metrics are automatically persisted using LoggingUtil, which writes a log file to the device storage without requiring any permissions.&lt;/p&gt;




&lt;p&gt;📊 The Results (Real Financial App)&lt;/p&gt;

&lt;p&gt;Stack: React 18 + TypeScript + TailwindCSS + Vite + React Router&lt;br&gt;
Assets: 1.4MB (3 main files + 13 lazy chunks)&lt;br&gt;
Device: Physical Android, mid-range&lt;/p&gt;

&lt;p&gt;First Load (Assets from Disk)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════════════════════╗
║     WEBVIRT ENGINE - PERFORMANCE REPORT          ║
╠══════════════════════════════════════════════════╣
║ Session duration:         4214 ms              ║
║ Total assets loaded:         3                  ║
║ Total load time:            77 ms              ║
║ Avg load time:              25 ms              ║
║ Min load time:              10 ms              ║
║ Max load time:              49 ms              ║
╠══════════════════════════════════════════════════╣
║ Cache hits:                  0                  ║
║ Cache misses:                3                  ║
║ Cache hit rate:           0.0%                 ║
║ Bytes from cache:            0 bytes           ║
║ Total bytes loaded:    1426251 bytes           ║
╠══════════════════════════════════════════════════╣
║ HTTP errors:                 0                  ║
║ SPA fallbacks:               1                  ║
║ Range requests:              0                  ║
╠══════════════════════════════════════════════════╣
║ BY MIME TYPE:                                    ║
║   HTML                 x1    avg   10ms           ║
║   CSS                  x1    avg   18ms           ║
║   JavaScript           x1    avg   49ms           ║
╠══════════════════════════════════════════════════╣
║ RECENT LOADS (last 5):                           ║
║   📄 /index.html                      10ms║
║   📄 /assets/index-DGe01YXs.css       18ms║
║   📄 /assets/index-B3g6t1vt.js        49ms║
╚══════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3 assets. 77ms total. Zero errors.&lt;/p&gt;

&lt;p&gt;The 4214ms "session" includes: app startup, welcome animation, and the user tapping the "Start" button. WebVirt only took 77ms.&lt;/p&gt;




&lt;p&gt;Second Load (LRU Cache in RAM)&lt;/p&gt;

&lt;p&gt;By long-pressing the WebView (a hidden debug gesture), I forced a reload to measure cache performance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════════════════════╗
║     WEBVIRT ENGINE - PERFORMANCE REPORT          ║
╠══════════════════════════════════════════════════╣
║ Session duration:          513 ms              ║
║ Total assets loaded:         3                  ║
║ Total load time:             2 ms              ║
║ Avg load time:               0 ms              ║
║ Min load time:               0 ms              ║
║ Max load time:               1 ms              ║
╠══════════════════════════════════════════════════╣
║ Cache hits:                  3                  ║
║ Cache misses:                0                  ║
║ Cache hit rate:         100.0%                 ║
║ Bytes from cache:      1426251 bytes           ║
║ Total bytes loaded:    1426251 bytes           ║
╠══════════════════════════════════════════════════╣
║ RECENT LOADS (last 5):                           ║
║   💾 /index.html                      1ms║
║   💾 /assets/index-B3g6t1vt.js        0ms║
║   💾 /assets/index-DGe01YXs.css       1ms║
╚══════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3 assets. 2ms total. 100% cache hit rate.&lt;/p&gt;

&lt;p&gt;Notice the emoji: 💾 = served from cache. The JS bundle took 0ms (less than 1ms, rounded down). HTML took 1ms. CSS took 1ms.&lt;/p&gt;




&lt;p&gt;📈 The Side-by-Side Comparison&lt;/p&gt;

&lt;p&gt;Metric First Load Reload (Cache) Improvement&lt;br&gt;
Total load time 77ms 2ms 38.5x faster&lt;br&gt;
Average time 25ms 0ms Instant&lt;br&gt;
Slowest asset 49ms (JS) 1ms (CSS) 49x faster&lt;br&gt;
Cache hit rate 0% 100% Perfect&lt;br&gt;
Bytes transferred 1.4MB 0 All from RAM&lt;br&gt;
HTTP errors 0 0 Perfect&lt;/p&gt;



&lt;p&gt;🧠 Why Is It So Fast?&lt;/p&gt;

&lt;p&gt;WebVirt Engine uses an in-memory LRU cache with SHA-1 ETags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First load:
  assets/index-B3g6t1vt.js → read from APK → cached in RAM → ETag generated

Second load:
  assets/index-B3g6t1vt.js → ETag match? → Yes → 304 Not Modified → 0ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;· No asset decoding (Android stores them compressed in the APK)&lt;br&gt;
· No disk I/O on reloads (everything in RAM)&lt;br&gt;
· No real HTTP header parsing (everything is local)&lt;br&gt;
· LruCache with memory awareness that cleans up on onTrimMemory()&lt;/p&gt;



&lt;p&gt;🔒 Security That Doesn't Sacrifice Speed&lt;/p&gt;

&lt;p&gt;Every response includes automatic security headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: default-src 'self'; script-src 'self'...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Access-Control-Allow-Origin: *
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And CSP is fully configurable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cspPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"default-src 'self'; script-src 'self' https://api.external.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🤝 Plays Beautifully with Nexus&lt;/p&gt;

&lt;p&gt;Need native APIs? Nexus is a JavaScript ↔ Android bridge that doesn't interfere with WebVirt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WebVirt: serves the SPA&lt;/span&gt;
&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Nexus: export, import, PDF, camera, whatever you need&lt;/span&gt;
&lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExportAdapter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImportAdapter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pdf"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PdfAdapter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFilePicker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachToWebViewLifecycle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Doesn't break WebVirt&lt;/span&gt;

&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebVirt doesn't know Nexus exists. Nexus doesn't know WebVirt exists. They collaborate without coupling. This is real architecture.&lt;/p&gt;




&lt;p&gt;🏗️ The Architecture That Makes This Possible&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebView
  ├── WebViewClient → WebVirt (owner)
  │     └── shouldInterceptRequest() → assets/
  │
  ├── WebViewLifecycleObserver → Nexus (decorator)
  │     └── Wraps WebVirt's client without breaking it
  │
  └── JavascriptInterface → Nexus (parallel channel)
        └── window.__nexus.call("export", data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three layers that don't compete. Decorator Pattern for lifecycle. Builder Pattern for fluent configuration. Strategy Pattern for PathHandlers.&lt;/p&gt;




&lt;p&gt;📦 Production Proven&lt;/p&gt;

&lt;p&gt;This isn't a "hello world" library. It's running in production in a real financial app with:&lt;/p&gt;

&lt;p&gt;· ⚛️ React 18 + TypeScript + TailwindCSS&lt;br&gt;
· 📦 5MB of assets (1.4MB main bundle)&lt;br&gt;
· 🔀 React Router with lazy loading&lt;br&gt;
· 📤 Native JSON export&lt;br&gt;
· 📥 Native JSON import with FilePicker (no permissions required)&lt;br&gt;
· 📄 Native PDF export&lt;br&gt;
· 🔒 Restrictive CSP&lt;br&gt;
· ⚡ 77ms first load, 2ms reloads&lt;/p&gt;



&lt;p&gt;🚀 Coming Soon to GitHub &amp;amp; JitPack&lt;/p&gt;

&lt;p&gt;WebVirt Engine v3.1.1&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="k"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;maven&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="s1"&gt;'https://jitpack.io'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.github.fouzstack:webvirt-engine:3.1.1'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nexus v2.0.0&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.github.fouzstack:nexus:2.0.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🎯 Is This for You?&lt;/p&gt;

&lt;p&gt;✅ Use WebVirt Engine if you:&lt;/p&gt;

&lt;p&gt;· Have an SPA in React/Vue/Svelte&lt;br&gt;
· Want full control without heavy dependencies&lt;br&gt;
· Need maximum offline performance&lt;br&gt;
· Value clean architecture and real decoupling&lt;/p&gt;

&lt;p&gt;❌ Not for you if you:&lt;/p&gt;

&lt;p&gt;· Need hot reload during development (for now)&lt;br&gt;
· Your company is already committed to Capacitor/Cordova&lt;br&gt;
· Your app is purely native with no web content&lt;/p&gt;




&lt;p&gt;🙏 Acknowledgments&lt;/p&gt;

&lt;p&gt;To Fouzstack for creating and maintaining both WebVirt and Nexus.&lt;br&gt;
To the GoF design patterns that still hold up 30 years later.&lt;br&gt;
To WebVirtMetrics and LoggingUtil for making it possible to collect this data without extra permissions.&lt;br&gt;
And to you, for reading this far.&lt;/p&gt;




&lt;p&gt;Questions? Ideas? Want to contribute? The repos will be open for issues and PRs as soon as they go live.&lt;/p&gt;

&lt;p&gt;Drop a comment: Which metric surprised you most? The 77ms first load or the 2ms cached reload?&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>performance</category>
      <category>react</category>
    </item>
    <item>
      <title>WebVirt + Nexus: Run Your React/Vue/Svelte SPA Inside an Android WebView</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 03 May 2026 15:40:34 +0000</pubDate>
      <link>https://dev.to/gfouz/webvirt-nexus-run-your-reactvuesvelte-spa-inside-an-android-webview-409i</link>
      <guid>https://dev.to/gfouz/webvirt-nexus-run-your-reactvuesvelte-spa-inside-an-android-webview-409i</guid>
      <description>&lt;h1&gt;
  
  
  WebVirt + Nexus: Run Your React/Vue/Svelte SPA Inside an Android WebView — No Capacitor, No Server, No Permissions
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Two decoupled Android libraries: one serves your SPA from APK assets like a real web server, the other bridges JavaScript to native code. Used together in a production app serving local businesses. Open source, JitPack-ready. Engine v3 and Nexus v2 coming soon.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;You built a great SPA in React + Vite + TypeScript. It runs beautifully at &lt;code&gt;localhost:5173&lt;/code&gt;. Now your client wants it packaged as an Android app.&lt;/p&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Capacitor / Cordova&lt;/td&gt;
&lt;td&gt;+25MB runtime, opinionated structure, black-box WebView&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;file://&lt;/code&gt; protocol&lt;/td&gt;
&lt;td&gt;CORS broken, SPA routes 404, APIs blocked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Embedded HTTP server (NanoHTTPD)&lt;/td&gt;
&lt;td&gt;Threads, ports, network permissions, overkill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw WebView&lt;/td&gt;
&lt;td&gt;You write everything from scratch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What you actually want: your SPA thinks it's on &lt;code&gt;https://app.local&lt;/code&gt;, everything comes from &lt;code&gt;assets/&lt;/code&gt;, zero server, zero permissions, zero overhead.&lt;/p&gt;

&lt;p&gt;That's exactly what WebVirt does.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is WebVirt?
&lt;/h2&gt;

&lt;p&gt;WebVirt is a Java Android library that intercepts &lt;code&gt;WebViewClient.shouldInterceptRequest()&lt;/code&gt; and serves your SPA assets directly from the APK — no actual server involved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your React app loads. Routes work. CORS works. No permissions needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Already published:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/fouzstack/fouzstack-webvirt" rel="noopener noreferrer"&gt;github.com/fouzstack/fouzstack-webvirt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JitPack: &lt;code&gt;implementation 'com.github.fouzstack:fouzstack-webvirt:1.0.0'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real Production Use Case
&lt;/h2&gt;

&lt;p&gt;WebVirt v1 + Nexus are currently running in a financial management app used by small local businesses. The tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚛️ React + Vite + TypeScript + Tailwind CSS v4&lt;/li&gt;
&lt;li&gt;📦 ~5MB of assets served entirely from APK&lt;/li&gt;
&lt;li&gt;📋 Product lists of 90+ items — fast scrolling, no lag&lt;/li&gt;
&lt;li&gt;📤 JSON export (native)&lt;/li&gt;
&lt;li&gt;📥 JSON import with file picker (no permissions)&lt;/li&gt;
&lt;li&gt;📄 PDF export (native)&lt;/li&gt;
&lt;li&gt;🔒 Restrictive CSP&lt;/li&gt;
&lt;li&gt;⚡ Near-instant load time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No internet required. No server running. Just assets + native bridges.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Coming: WebVirt Engine v3 + Nexus v2
&lt;/h2&gt;

&lt;p&gt;The production experience revealed what was missing. Two new libraries are in the works:&lt;/p&gt;

&lt;h3&gt;
  
  
  WebVirt Engine — The Production-Grade Upgrade
&lt;/h3&gt;

&lt;p&gt;Built on the same &lt;code&gt;shouldInterceptRequest()&lt;/code&gt; approach but adds what real apps need:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart caching with LRU + ETags&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First load:  index.html → read → ETag generated → cached (LRU)
Second load: index.html → ETag match → 304 Not Modified → 0 bytes transferred
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Real Range Requests for video streaming&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Client:  GET /video.mp4   Range: bytes=1048576-2097151
Server:  206 Partial Content
         Content-Range: bytes 1048576-2097151/52428800
         [only the requested bytes — file never fully loaded into RAM]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security headers on every response&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: default-src 'self'; script-src 'self'...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Virtual API routes&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/config"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{\"version\": \"1.0\"}"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cspPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"default-src 'self'; connect-src https://api.example.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The architecture uses Chain of Responsibility for request routing and Strategy for path handlers — the same GoF patterns you'd use in a backend framework, applied to Android's WebView layer.&lt;/p&gt;




&lt;h3&gt;
  
  
  Nexus — The Native Bridge
&lt;/h3&gt;

&lt;p&gt;Nexus is a completely separate library for JavaScript-to-native communication. It doesn't know WebVirt exists. WebVirt doesn't know Nexus exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Nexus&lt;/span&gt; &lt;span class="n"&gt;nexus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDebugMode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withGlobalTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExportHandlerAdapter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImportHandlerAdapter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFilePicker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachToWebViewLifecycle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__nexus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;export&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What makes Nexus interesting:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart re-injection&lt;/strong&gt; — The JS runtime re-injects only on real page navigations, not on React Router hash changes (&lt;code&gt;#/products&lt;/code&gt;, &lt;code&gt;#/settings&lt;/code&gt;). This was a real pain point discovered in production.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From the actual source&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; 
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previousBaseUrl&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;previousBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;baseUrl&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;notifyPageLoaded&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Only re-inject here&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;FilePicker without permissions&lt;/strong&gt; — Uses Android's Storage Access Framework (SAF). No &lt;code&gt;READ_EXTERNAL_STORAGE&lt;/code&gt; in your manifest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFilePicker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// That's it. No permissions.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Interceptor chain&lt;/strong&gt; — Add cross-cutting concerns (logging, auth, rate limiting) without touching your handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuthInterceptor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerInterceptor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;LoggingInterceptor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExportHandler&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Event emission from native to JS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;emitEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"syncComplete"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"records"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__nexus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;syncComplete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Synced &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; records`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Architecture: Two Libraries That Don't Know Each Other
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                      WebView                        │
│                                                     │
│  ┌─────────────────────────────────────────────┐   │
│  │         WebVirt Engine                       │   │
│  │  shouldInterceptRequest() → assets/          │   │
│  │  "app.local/index.html" → real index.html    │   │
│  │  CSP, ETags, Range Requests, LRU Cache      │   │
│  └─────────────────────────────────────────────┘   │
│                    ▲                                │
│                    │ Decorates (doesn't override)   │
│  ┌─────────────────────────────────────────────┐   │
│  │         Nexus WebViewLifecycleObserver       │   │
│  │  Observes onPageFinished, onPageStarted      │   │
│  │  Delegates everything else to WebVirt        │   │
│  └─────────────────────────────────────────────┘   │
│                                                     │
│  ┌─────────────────────────────────────────────┐   │
│  │         Nexus JSInterface (parallel channel) │   │
│  │  window.__nexus.call("export", data)         │   │
│  │  Does NOT go through shouldInterceptRequest  │   │
│  └─────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key design decision: &lt;code&gt;WebViewLifecycleObserver&lt;/code&gt; wraps WebVirt's client using the &lt;strong&gt;Decorator pattern&lt;/strong&gt;. It adds lifecycle hooks without overriding or replacing WebVirt's request handling.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;WebVirt doesn't know...&lt;/th&gt;
&lt;th&gt;Nexus doesn't know...&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;That Nexus exists&lt;/td&gt;
&lt;td&gt;That WebVirt exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;There's a JS bridge&lt;/td&gt;
&lt;td&gt;How assets are served&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What handlers are registered&lt;/td&gt;
&lt;td&gt;What virtual host is used&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is actual decoupling, not just marketing copy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;WebVirt Engine&lt;/th&gt;
&lt;th&gt;Capacitor&lt;/th&gt;
&lt;th&gt;Raw WebView&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime size&lt;/td&gt;
&lt;td&gt;0KB overhead&lt;/td&gt;
&lt;td&gt;~25MB&lt;/td&gt;
&lt;td&gt;0KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Smart cache (LRU + ETags)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range Requests (video)&lt;/td&gt;
&lt;td&gt;✅ Real&lt;/td&gt;
&lt;td&gt;Delegates to system&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configurable CSP&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPA route fallback&lt;/td&gt;
&lt;td&gt;✅ Automatic&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline mode&lt;/td&gt;
&lt;td&gt;✅ Built-in&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot reload (dev)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native JS bridge&lt;/td&gt;
&lt;td&gt;With Nexus&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No permissions needed&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interceptor chain&lt;/td&gt;
&lt;td&gt;✅ (Nexus)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;~5 min&lt;/td&gt;
&lt;td&gt;~2 hours&lt;/td&gt;
&lt;td&gt;Variable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Who Should Use This
&lt;/h2&gt;

&lt;p&gt;✅ React/Vue/Svelte developers who need Android without Capacitor's complexity&lt;br&gt;&lt;br&gt;
✅ Small teams who want full control with minimal dependencies&lt;br&gt;&lt;br&gt;
✅ Offline-first apps (no internet required after install)&lt;br&gt;&lt;br&gt;
✅ Apps where clean architecture matters  &lt;/p&gt;

&lt;p&gt;❌ If you need live hot reload during development&lt;br&gt;&lt;br&gt;
❌ If your team is already deep in the Capacitor ecosystem  &lt;/p&gt;




&lt;h2&gt;
  
  
  What's Available Now vs. Coming Soon
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebVirt v1.0.0&lt;/td&gt;
&lt;td&gt;✅ GitHub + JitPack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nexus (production)&lt;/td&gt;
&lt;td&gt;✅ Running in production&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebVirt Engine v3.1.1&lt;/td&gt;
&lt;td&gt;🔜 Coming soon — github.com/fouzstack/webvirt-engine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nexus v2.0.0&lt;/td&gt;
&lt;td&gt;🔜 Coming soon — github.com/fouzstack/nexus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kotlin DSL (-ktx modules)&lt;/td&gt;
&lt;td&gt;🔜 Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit tests + benchmarks&lt;/td&gt;
&lt;td&gt;🔜 Planned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When Engine and Nexus launch, I'll post a follow-up with concrete benchmarks: cold start times, cache hit rates, memory footprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try WebVirt Now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="c1"&gt;// settings.gradle&lt;/span&gt;
&lt;span class="n"&gt;dependencyResolutionManagement&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;repositories&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;maven&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="s1"&gt;'https://jitpack.io'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// build.gradle (app)&lt;/span&gt;
&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt; &lt;span class="s1"&gt;'com.github.fouzstack:fouzstack-webvirt:1.0.0'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put your React/Vite build output in &lt;code&gt;assets/&lt;/code&gt; and you're done.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Questions, feedback, or contributions welcome.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What native handler would you find most useful with Nexus — camera access, biometrics, Bluetooth, local database? Let me know in the comments, it'll help prioritize what gets documented first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://github.com/fouzstack" rel="noopener noreferrer"&gt;@fouzstack&lt;/a&gt; — open source Android libraries for the WebView ecosystem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>WebVirt Engine + Nexus: La dupla perfecta para SPAs en Android</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 03 May 2026 15:12:15 +0000</pubDate>
      <link>https://dev.to/gfouz/webvirt-engine-nexus-la-dupla-perfecta-para-spas-en-android-3ajf</link>
      <guid>https://dev.to/gfouz/webvirt-engine-nexus-la-dupla-perfecta-para-spas-en-android-3ajf</guid>
      <description>&lt;p&gt;🚀 WebVirt Engine + Nexus: La dupla perfecta para SPAs en Android que sí respeta tu arquitectura&lt;/p&gt;

&lt;p&gt;Sirve tu SPA desde assets como un servidor web profesional + puente nativo JavaScript sin fricción. La arquitectura que Capacitor no puede ofrecerte.&lt;/p&gt;




&lt;p&gt;🤔 El problema que todos enfrentamos&lt;/p&gt;

&lt;p&gt;Tienes una SPA hermosa en React, Vue o Svelte. Corre perfecto en localhost:3000. Ahora necesitas llevarla a Android.&lt;/p&gt;

&lt;p&gt;Tus opciones tradicionales:&lt;/p&gt;

&lt;p&gt;Solución Resultado&lt;br&gt;
Capacitor / Cordova +30MB de runtime, configuración compleja, WebView propio&lt;br&gt;
file:// protocol CORS roto, rutas SPA no funcionan, APIs bloqueadas&lt;br&gt;
WebView + servidor embebido Overkill, hilos, puertos, problemas de red&lt;br&gt;
Script casero de 50 líneas Funciona... hasta que agregas video, PDF, o edge cases&lt;/p&gt;

&lt;p&gt;Lo que realmente quieres: Que tu SPA crea que está en &lt;a href="https://app.local" rel="noopener noreferrer"&gt;https://app.local&lt;/a&gt;, pero todo venga de assets/. Sin servidor. Sin permisos. Sin overhead.&lt;/p&gt;



&lt;p&gt;✨ La solución: WebVirt Engine + Nexus&lt;/p&gt;

&lt;p&gt;Dos librerías diseñadas para coexistir sin conocerse. Como hermanos que comparten cuarto sin pelearse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                    WebView                          │
│                                                     │
│  ┌───────────────────────────────────────────────┐ │
│  │         WebVirt Engine (dueño del cliente)    │ │
│  │  shouldInterceptRequest() → assets/           │ │
│  │  "app.local/index.html" → index.html real     │ │
│  │  CSP, ETags, Range Requests, Caché LRU       │ │
│  └───────────────────────────────────────────────┘ │
│                    ▲                                │
│                    │ Decora (no sobrescribe)         │
│  ┌───────────────────────────────────────────────┐ │
│  │         Nexus WebViewLifecycleObserver        │ │
│  │  Observa onPageFinished, onPageStarted        │ │
│  │  Delega todo lo demás a WebVirt               │ │
│  └───────────────────────────────────────────────┘ │
│                                                     │
│  ┌───────────────────────────────────────────────┐ │
│  │         Nexus JSInterface (canal paralelo)    │ │
│  │  window.__nexus.call("export", data)          │ │
│  │  NO pasa por shouldInterceptRequest           │ │
│  └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🎯 5 líneas y tu SPA está viva&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. WebVirt: Sirve tu SPA desde assets&lt;/span&gt;
&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Nexus: Puente nativo&lt;/span&gt;
&lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExportAdapter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ImportAdapter&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFilePicker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Nexus observa sin romper&lt;/span&gt;
&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachToWebViewLifecycle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 4. Tu SPA carga como si estuviera en un servidor real&lt;/span&gt;
&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://app.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo. Tu React app funcionando, rutas SPA intactas, APIs nativas disponibles. Sin configuración extraña.&lt;/p&gt;




&lt;p&gt;🏗️ Arquitectura: El secreto está en el desacoplamiento&lt;/p&gt;

&lt;p&gt;A diferencia de frameworks monolíticos, WebVirt Engine y Nexus usan capas que no compiten:&lt;/p&gt;

&lt;p&gt;Capa 1: Red Virtual → WebVirt Engine&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebViewClient.shouldInterceptRequest()
  ├── ¿Es app.local? → Sirve desde assets/
  ├── ¿Ruta SPA? → index.html (fallback)
  ├── ¿API virtual? → RouteHandler (respuesta JSON falsa)
  └── ¿Externo permitido? → Deja pasar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patrones: Builder, Chain of Responsibility, Strategy (PathHandlers)&lt;/p&gt;

&lt;p&gt;Capa 2: Puente Nativo → Nexus&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebView.addJavascriptInterface(nexusInterface, "__nexus")
  └── window.__nexus.call("export", params, callbackId)
        ├── Thread pool para handlers
        ├── Timeout automático
        ├── Serialización JSON
        └── Callback al WebView vía evaluateJavascript()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patrones: Observer, Command, Mediator, Callback&lt;/p&gt;

&lt;p&gt;Capa 3: Ciclo de Vida → Nexus (Decorator)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebViewLifecycleObserver envuelve al cliente de WebVirt
  ├── shouldInterceptRequest() → delega a WebVirt ✅
  ├── onPageFinished() → WebVirt + notifica a Nexus ✅
  └── Todos los demás métodos → delega a WebVirt ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patrón: Decorator — El más elegante del sistema&lt;/p&gt;

&lt;p&gt;¿Por qué funciona tan bien?&lt;/p&gt;

&lt;p&gt;WebVirt NO sabe Nexus NO sabe&lt;br&gt;
Que Nexus existe Que WebVirt existe&lt;br&gt;
Que hay un bridge JS Cómo se sirven los assets&lt;br&gt;
Qué handlers hay registrados Qué host virtual se usa&lt;/p&gt;

&lt;p&gt;Esto es desacoplamiento real. No es marketing.&lt;/p&gt;



&lt;p&gt;🔒 Seguridad de grado producción&lt;/p&gt;

&lt;p&gt;WebVirt Engine hereda el enfoque de WebVirt original pero lo lleva a nivel empresarial:&lt;/p&gt;

&lt;p&gt;Defensa en profundidad&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Capa 1: Path Traversal Protection
  ├── Normalización de rutas
  ├── Bloqueo de ".."
  └── Validación de esquema

Capa 2: Content Security Policy
  ├── CSP configurable por el desarrollador
  ├── Default restrictivo
  └── Headers de seguridad automáticos

Capa 3: Offline Mode
  ├── Bloquea TODAS las peticiones externas
  └── Whitelist de dominios explícita

Capa 4: Extensiones permitidas
  └── Solo sirve archivos con extensiones seguras
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers de seguridad automáticos&lt;/p&gt;

&lt;p&gt;Cada respuesta de WebVirt Engine incluye:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: default-src 'self'; script-src 'self'...
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Access-Control-Allow-Origin: *
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y tú puedes personalizar el CSP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cspPolicy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"default-src 'self'; script-src 'self' https://api.externa.com"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🎬 Streaming de video sin cargar en RAM&lt;/p&gt;

&lt;p&gt;¿Tu SPA tiene videos? WebVirt Engine soporta Range Requests reales:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cliente: GET /video.mp4
         Range: bytes=1048576-2097151

Servidor: 206 Partial Content
          Content-Range: bytes 1048576-2097151/52428800
          [solo envía los bytes solicitados]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El archivo nunca se carga completo en memoria. Streaming real desde InputStream.&lt;/p&gt;




&lt;p&gt;⚡ Rendimiento: Caché que aprende&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Primera carga:
  index.html → leído → ETag generado → cacheado (LRU)

Segunda carga:
  index.html → ETag coincide → 304 Not Modified → 0 bytes transferidos
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;· LruCache con control de memoria real&lt;br&gt;
· ETags SHA-1 para validación&lt;br&gt;
· If-Modified-Since para archivos&lt;br&gt;
· Se limpia automáticamente con onTrimMemory()&lt;/p&gt;



&lt;p&gt;🧩 ¿Juntos o separados?&lt;/p&gt;

&lt;p&gt;Solo WebVirt Engine&lt;/p&gt;

&lt;p&gt;Perfecto para SPAs que no necesitan APIs nativas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/config"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{...}"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// API falsa&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solo Nexus&lt;/p&gt;

&lt;p&gt;Perfecto para WebViews tradicionales que necesitan bridge nativo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"camera"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CameraHandler&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebVirt Engine + Nexus&lt;/p&gt;

&lt;p&gt;La combinación completa. SPA servida profesionalmente + APIs nativas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"app.local"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;Nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;installOn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"export"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exportAdapter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;importAdapter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registerHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"camera"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cameraAdapter&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withFilePicker&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;nexus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachToWebViewLifecycle&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;📊 Comparación honesta&lt;/p&gt;

&lt;p&gt;Característica WebVirt Engine Capacitor Cordova WebView puro&lt;br&gt;
Tamaño runtime 0KB (solo tu código) ~25MB ~15MB 0KB&lt;br&gt;
Caché inteligente ✅ LRU + ETags ❌ ❌ ❌&lt;br&gt;
Range Requests ✅ Streaming real ✅ ✅ ❌&lt;br&gt;
CSP personalizable ✅ ❌ ✅ ❌&lt;br&gt;
Rutas SPA ✅ Automático ✅ ✅ ❌&lt;br&gt;
Offline mode ✅ Built-in ✅ ✅ ✅&lt;br&gt;
Hot reload ❌ ✅ ✅ ❌&lt;br&gt;
JS Bridge nativo Con Nexus ✅ ✅ Manual&lt;br&gt;
Sin permisos ✅ ✅ ✅ ✅&lt;br&gt;
Curva aprendizaje 5 minutos 2 horas 3 horas Variable&lt;br&gt;
Desacoplamiento ✅ Total ❌ ❌ N/A&lt;/p&gt;




&lt;p&gt;🚀 Producción probada&lt;/p&gt;

&lt;p&gt;No es teoría. WebVirt (versión lite) + Nexus ya funcionan en producción en una app financiera real:&lt;/p&gt;

&lt;p&gt;· ⚛️ React + TypeScript + TailwindCSS&lt;br&gt;
· 📦 5MB de assets servidos sin conexión&lt;br&gt;
· 📤 Export JSON nativo&lt;br&gt;
· 📥 Import JSON con FilePicker (sin permisos)&lt;br&gt;
· 📄 Export PDF nativo&lt;br&gt;
· 🔒 CSP restrictivo&lt;br&gt;
· ⚡ Carga instantánea&lt;/p&gt;




&lt;p&gt;📦 Próximamente&lt;/p&gt;

&lt;p&gt;WebVirt Engine v3.1.1&lt;/p&gt;

&lt;p&gt;· Repositorio: github.com/fouzstack/webvirt-engine&lt;br&gt;
· JitPack: implementation 'com.github.fouzstack:webvirt-engine:3.1.1'&lt;br&gt;
· Documentación completa&lt;br&gt;
· Módulo Kotlin -ktx&lt;/p&gt;

&lt;p&gt;Nexus v2.0.0&lt;/p&gt;

&lt;p&gt;· Repositorio: github.com/fouzstack/nexus&lt;br&gt;
· JitPack: implementation 'com.github.fouzstack:nexus:2.0.0'&lt;br&gt;
· Documentación completa&lt;br&gt;
· Templates de proyecto&lt;/p&gt;




&lt;p&gt;🎯 ¿Quién debería usar esto?&lt;/p&gt;

&lt;p&gt;✅ Desarrolladores React/Vue/Svelte que necesitan Android sin Capacitor&lt;/p&gt;

&lt;p&gt;✅ Equipos pequeños que quieren control total sin dependencias pesadas&lt;/p&gt;

&lt;p&gt;✅ Apps offline-first que necesitan rendimiento máximo&lt;/p&gt;

&lt;p&gt;✅ Cualquiera que valore arquitecturas limpias y desacoplamiento real&lt;/p&gt;

&lt;p&gt;❌ Si necesitas hot reload en desarrollo (por ahora)&lt;/p&gt;

&lt;p&gt;❌ Si tu empresa ya está comprometida con Capacitor/Cordova&lt;/p&gt;




&lt;p&gt;💬 Lo que dicen los patrones de diseño&lt;/p&gt;

&lt;p&gt;"Programa contra interfaces, no implementaciones" — GoF&lt;/p&gt;

&lt;p&gt;WebVirt Engine y Nexus no se conocen. Se respetan. Colaboran sin acoplarse.&lt;/p&gt;

&lt;p&gt;"Separa lo que cambia de lo que permanece" — Clean Architecture&lt;/p&gt;

&lt;p&gt;WebVirt cambia (assets, CSP, rutas). Nexus cambia (handlers, timeouts).&lt;br&gt;
Ninguno rompe al otro.&lt;/p&gt;

&lt;p&gt;"Una clase debería tener solo una razón para cambiar" — SRP&lt;/p&gt;

&lt;p&gt;WebVirt: servir assets. Nexus: bridge nativo. Una responsabilidad cada uno.&lt;/p&gt;




&lt;p&gt;🔗 Enlaces&lt;/p&gt;

&lt;p&gt;· WebVirt (lite): github.com/fouzstack/fouzstack-webvirt&lt;br&gt;
· WebVirt Engine: Próximamente en github.com/fouzstack/webvirt-engine&lt;br&gt;
· Nexus: Próximamente en github.com/fouzstack/nexus&lt;/p&gt;




&lt;p&gt;🙏 Agradecimientos&lt;/p&gt;

&lt;p&gt;A la comunidad de Android que sigue buscando formas más limpias de integrar WebViews.&lt;br&gt;
A los patrones de diseño del GoF que 30 años después siguen vigentes.&lt;br&gt;
Y a ti, por leer hasta aquí.&lt;/p&gt;




&lt;p&gt;¿Preguntas? ¿Ideas? ¿Quieres contribuir? Los repositorios estarán abiertos para issues y PRs.&lt;/p&gt;

&lt;p&gt;Comenta abajo: ¿Qué handler nativo te gustaría ver implementado con Nexus?&lt;/p&gt;

</description>
      <category>android</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>spanish</category>
    </item>
    <item>
      <title>WebVirt : Load your SPA in Android WebView with 3 lines of code</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Mon, 27 Apr 2026 14:02:38 +0000</pubDate>
      <link>https://dev.to/gfouz/webvirt-load-your-spa-in-android-webview-with-3-lines-of-code-2d9l</link>
      <guid>https://dev.to/gfouz/webvirt-load-your-spa-in-android-webview-with-3-lines-of-code-2d9l</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# 🚀 WebVirt: Load your SPA in Android WebView with just 3 lines of code&lt;/span&gt;

Tired of writing 100+ lines of boilerplate just to display a SPA in a WebView?

&lt;span class="gs"&gt;**WebVirt reduces it to 3 lines.**&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## 😩 The problem (we've all been there)&lt;/span&gt;

Packaging a SPA (React, Vue, Angular, Svelte, SolidJs...) into an Android WebView should be trivial… but it's not:
&lt;span class="p"&gt;
-&lt;/span&gt; Manually configuring &lt;span class="sb"&gt;`WebViewClient`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Intercepting requests (&lt;span class="sb"&gt;`shouldInterceptRequest`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Resolving SPA routes (&lt;span class="sb"&gt;`/products/42`&lt;/span&gt; → &lt;span class="sb"&gt;`index.html`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Handling MIME types correctly
&lt;span class="p"&gt;-&lt;/span&gt; Implementing asset caching
&lt;span class="p"&gt;-&lt;/span&gt; Preventing directory traversal attacks
&lt;span class="p"&gt;-&lt;/span&gt; Controlling external traffic

All that… before you even start building your app.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## ✨ The solution: WebVirt&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
java&lt;br&gt;
WebVirt.with(this)&lt;br&gt;
    .host("myapp.local")&lt;br&gt;
    .bind(webView);&lt;/p&gt;

&lt;p&gt;webView.loadUrl("&lt;a href="https://myapp.local/%22" rel="noopener noreferrer"&gt;https://myapp.local/"&lt;/a&gt;);&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
That's it.

WebVirt turns your APK into an internal virtual web server.

Your SPA lives in /assets and is served exactly like it would be in production.

---

🔒 Offline-first security by default

WebVirt follows a simple rule:

If you don't explicitly allow it, it's blocked.

· 🚫 External traffic blocked by default
· ✅ Whitelist with allowExternalDomains()
· 🔌 Offline mode with offlineOnly(true)
· 🛡 Directory traversal protection

📦 Automatic HTTP headers

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
java&lt;br&gt;
WebVirt.with(this)&lt;br&gt;
    .host("myapp.local")&lt;br&gt;
    .allowExternalDomains("api.myapp.com", "cdn.myapp.com")&lt;br&gt;
    .bind(webView);&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

🧠 Automatic SPA routing

Using React Router, Vue Router, or Angular?

WebVirt detects extensionless routes and serves index.html automatically:

· /about → index.html ✅
· /products/42 → index.html ✅
· /app.js → app.js ✅
· /style.css → style.css ✅

No configuration. No hacks. No 404s.

---

⚡ Smart in-memory caching

Optimized for real performance:

· 🧠 index.html → memory-cached (never stale)
· 📦 Assets (JS, CSS, JSON, WASM, fonts) → immutable cache (max-age=31536000)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
java&lt;br&gt;
webVirt.clearCache(); // Ideal in onDestroy()&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

📦 Installation

settings.gradle

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
gradle&lt;br&gt;
dependencyResolutionManagement {&lt;br&gt;
    repositories {&lt;br&gt;
        maven { url '&lt;a href="https://jitpack.io" rel="noopener noreferrer"&gt;https://jitpack.io&lt;/a&gt;' }&lt;br&gt;
    }&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
build.gradle

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
gradle&lt;br&gt;
dependencies {&lt;br&gt;
    implementation 'com.github.fouzstack:fouzstack-webvirt:v1.0.0'&lt;br&gt;
}&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
---

🎨 Advanced configuration (optional)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
java&lt;br&gt;
WebVirt.with(this)&lt;br&gt;
    .host("myapp.local")&lt;br&gt;
    .subfolder("dist")&lt;br&gt;
    .allowExternalDomains("api.example.com")&lt;br&gt;
    .config(cfg -&amp;gt; {&lt;br&gt;
        cfg.setCacheEnabled(true);&lt;br&gt;
        cfg.setJavaScriptEnabled(true);&lt;br&gt;
        cfg.setDomStorageEnabled(true);&lt;br&gt;
    })&lt;br&gt;
    .bind(webView);&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
Full access to WebViewSettings without breaking the fluent API.

---

📊 Before vs After

Without WebVirt With WebVirt
~100 lines of code 3 lines
Manual WebViewClient Automatic
Broken SPA routing Works out-of-the-box
Manual caching Included
MIME type hell Included
Manual security Offline-first

---

🌐 Repository

👉 github.com/fouzstack/fouzstack-webvirt

---

🤔 Why "WebVirt"?

Because your APK becomes a:

Virtual web server without a real server

· No ports
· No network
· No real localhost

Just your SPA running like in production.

---

🧪 Requirements

· Android 4.4+ (API 19)
· AndroidX
· Compile SDK 34+

---

📄 License

MIT — use it without fear.

---

💭 Final thought

The best tools aren't the biggest.

They're the ones you set up in seconds… and then forget.

WebVirt doesn't want the spotlight.
It wants you to focus on your product.

---

⭐ Did it help you?

· Star it on GitHub
· Open an issue if you find something
· PRs are welcome

Happy coding 🚀

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>android</category>
      <category>java</category>
      <category>mobile</category>
      <category>showdev</category>
    </item>
    <item>
      <title>🚀 WebVirt: Carga tu SPA en Android WebView con solo 3 líneas de código</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Fri, 24 Apr 2026 05:16:55 +0000</pubDate>
      <link>https://dev.to/gfouz/webvirt-carga-tu-spa-en-android-webview-con-solo-3-lineas-de-codigo-5j7</link>
      <guid>https://dev.to/gfouz/webvirt-carga-tu-spa-en-android-webview-con-solo-3-lineas-de-codigo-5j7</guid>
      <description>&lt;p&gt;La primera vez que intenté cargar una SPA de React en un WebView pensé que sería trivial. No lo fue. Terminé escribiendo más de 150 líneas solo para:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;servir archivos desde &lt;code&gt;assets&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;interceptar requests manualmente&lt;/li&gt;
&lt;li&gt;arreglar rutas de React Router&lt;/li&gt;
&lt;li&gt;y evitar errores 404 en producción&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Después de repetir ese proceso varias veces… decidí simplificarlo. Así nació &lt;strong&gt;WebVirt&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 El problema
&lt;/h2&gt;

&lt;p&gt;Cargar una SPA (React, Vue, Angular, Svelte) dentro de un &lt;code&gt;WebView&lt;/code&gt; en Android implica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WebViewAssetLoader&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;shouldInterceptRequest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;manejo manual de rutas&lt;/li&gt;
&lt;li&gt;configuración de headers y MIME types&lt;/li&gt;
&lt;li&gt;cache&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es difícil… pero sí repetitivo, verboso y propenso a errores.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ La solución
&lt;/h2&gt;

&lt;p&gt;Con WebVirt, todo se reduce a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"miapp.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es todo.&lt;/p&gt;

&lt;p&gt;Tu WebView ya puede cargar una SPA completa desde assets sin configuración adicional.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✨ ¿Qué hace WebVirt?
&lt;/h3&gt;

&lt;p&gt;WebVirt actúa como un servidor web virtual dentro de tu APK.&lt;/p&gt;

&lt;p&gt;Intercepta las peticiones del WebView y sirve los archivos directamente desde assets, resolviendo automáticamente los problemas más comunes.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔁 SPA routing automático
&lt;/h3&gt;

&lt;p&gt;Si usas React Router (o similar), no necesitas configuración extra.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/dashboard&lt;/code&gt; → WebVirt sirve &lt;code&gt;index.html&lt;/code&gt; → tu router toma el control&lt;/p&gt;

&lt;h3&gt;
  
  
  💾 Caché inteligente
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;HTML → no cacheado (para reflejar cambios)&lt;/li&gt;
&lt;li&gt;JS/CSS → cacheado agresivamente (tipo CDN)&lt;/li&gt;
&lt;li&gt;Otros archivos → streaming directo&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔒 Seguridad básica integrada
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prevención de directory traversal (&lt;code&gt;../&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Validación de rutas&lt;/li&gt;
&lt;li&gt;Headers seguros por defecto&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📁 MIME types listos
&lt;/h3&gt;

&lt;p&gt;Soporte para HTML, JS, CSS, JSON, imágenes, fuentes, WASM y más.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Ejemplo real
&lt;/h3&gt;

&lt;p&gt;Supongamos que tienes un build de React (Vite, CRA, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/src/main/assets/
├── index.html
└── assets/
    ├── index.js
    └── index.css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Solo haces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://miapp.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¡Y listo!&lt;/p&gt;

&lt;p&gt;✅ React Router funciona&lt;br&gt;
✅ No hay 404 en rutas internas&lt;br&gt;
✅ No necesitas lógica extra&lt;/p&gt;
&lt;h3&gt;
  
  
  🧠 ¿Cómo funciona?
&lt;/h3&gt;

&lt;p&gt;A alto nivel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WebView → intercept request → WebVirt → assets → response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Detecta si la URL pertenece a tu host virtual.&lt;/li&gt;
&lt;li&gt;Resuelve si es archivo o ruta SPA.&lt;/li&gt;
&lt;li&gt;Sirve desde memoria o desde assets.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sin magia, solo encapsulación de lo que normalmente harías a mano.&lt;/p&gt;

&lt;h3&gt;
  
  
  🤔 ¿Por qué usarlo?
&lt;/h3&gt;

&lt;p&gt;Porque evita repetir lo mismo en cada proyecto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;menos código boilerplate&lt;/li&gt;
&lt;li&gt;menos errores en routing&lt;/li&gt;
&lt;li&gt;mejor experiencia de desarrollo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No reemplaza las herramientas existentes,&lt;br&gt;
simplemente las abstrae en una API más simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 Uso rápido en tu MainActivity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;WebVirt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"miapp.local"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bind&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;webView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://miapp.local/"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🔮 Próximamente
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Publicación en Maven Central / JitPack&lt;/li&gt;
&lt;li&gt;Kotlin DSL&lt;/li&gt;
&lt;li&gt;LRU cache configurable&lt;/li&gt;
&lt;li&gt;Hooks para personalización&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📦 Código y documentación
&lt;/h3&gt;

&lt;p&gt;Estoy preparando el repo público con README completo y ejemplos.&lt;/p&gt;

&lt;p&gt;Si te interesa el proyecto o quieres probarlo antes:&lt;/p&gt;

&lt;p&gt;👉 Déjame un comentario&lt;br&gt;
👉 o sígueme para actualizaciones&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Conclusión
&lt;/h3&gt;

&lt;p&gt;Cargar una SPA en un WebView no debería tomar 150 líneas.&lt;/p&gt;

&lt;p&gt;Con WebVirt:&lt;/p&gt;

&lt;p&gt;✅ es simple&lt;br&gt;
✅ es rápido&lt;br&gt;
✅ funciona&lt;/p&gt;

&lt;p&gt;¿Te ha pasado lo mismo con WebView?&lt;br&gt;
Me interesa saber cómo lo estás resolviendo tú 👇&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>showdev</category>
      <category>spanish</category>
    </item>
    <item>
      <title>Tu WebView no necesita un servidor HTTP. Necesita un Virtual Host.</title>
      <dc:creator>Giovani Fouz</dc:creator>
      <pubDate>Sun, 19 Apr 2026 09:01:38 +0000</pubDate>
      <link>https://dev.to/gfouz/tu-webview-no-necesita-un-servidor-http-necesita-un-virtual-host-3jca</link>
      <guid>https://dev.to/gfouz/tu-webview-no-necesita-un-servidor-http-necesita-un-virtual-host-3jca</guid>
      <description>&lt;h3&gt;
  
  
  Cómo un patrón de los años 90 resolvió uno de los problemas más frustrantes del desarrollo móvil moderno
&lt;/h3&gt;




&lt;p&gt;Hay una idea en ingeniería de software que reaparece una y otra vez, con décadas de distancia y en plataformas completamente distintas. Una idea tan elegante que la industria la redescubre cada cierto tiempo, la viste con nueva ropa, y la presenta como si fuera nueva.&lt;/p&gt;

&lt;p&gt;Esta es la historia de esa idea. Y de cómo terminó viviendo dentro de una app Android.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema que nadie quiere admitir
&lt;/h2&gt;

&lt;p&gt;Construir una app móvil con React, Vue o cualquier framework web moderno es tentador. Tienes un equipo que ya sabe web, una base de código compartida, y la promesa de "escribe una vez, corre en todas partes."&lt;/p&gt;

&lt;p&gt;Hasta que llega el momento de cargar tu app en Android sin conexión a internet.&lt;/p&gt;

&lt;p&gt;Android no tiene un servidor local. WebView no sabe hablar con tu carpeta &lt;code&gt;assets/&lt;/code&gt; como si fuera un dominio real. Y React Router — el corazón de la navegación en casi toda SPA moderna — necesita que las rutas como &lt;code&gt;/dashboard&lt;/code&gt; o &lt;code&gt;/perfil/123&lt;/code&gt; devuelvan siempre el mismo &lt;code&gt;index.html&lt;/code&gt;, sin importar qué tan profunda sea la URL.&lt;/p&gt;

&lt;p&gt;Sin eso, tu app se rompe. El usuario navega, presiona atrás, recarga — y ve una pantalla en blanco o un error 404 que no debería existir.&lt;/p&gt;

&lt;p&gt;La solución obvia sería levantar un servidor HTTP local. Pero eso consume batería, requiere permisos, complica el ciclo de vida de la app, y abre vectores de seguridad innecesarios. No es la respuesta correcta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La respuesta correcta tiene 30 años de historia detrás.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  1991: Cuando los servidores aprendieron a mentir (bien)
&lt;/h2&gt;

&lt;p&gt;En los primeros días de la web, cada URL correspondía a un archivo físico en el disco del servidor. &lt;code&gt;/pagina.html&lt;/code&gt; era literalmente un archivo llamado &lt;code&gt;pagina.html&lt;/code&gt;. Simple, predecible, rígido.&lt;/p&gt;

&lt;p&gt;El problema llegó cuando los sitios crecieron. Un servidor físico, múltiples dominios. ¿Cómo diferenciaba el servidor a cuál de ellos le hablaba el visitante?&lt;/p&gt;

&lt;p&gt;La respuesta fue el &lt;strong&gt;Virtual Hosting&lt;/strong&gt;: la capacidad de un solo servidor de hacerse pasar por muchos, respondiendo diferente según el dominio que el cliente pedía. Apache HTTP Server lo popularizó a mediados de los 90. Un servidor físico podía alojar &lt;code&gt;empresa-a.com&lt;/code&gt;, &lt;code&gt;empresa-b.com&lt;/code&gt; y &lt;code&gt;empresa-c.com&lt;/code&gt; simultáneamente, cada uno con su propio espacio de archivos, sus propias reglas, su propia identidad.&lt;/p&gt;

&lt;p&gt;Era, en esencia, enseñarle a un servidor a interceptar una petición y decidir: &lt;em&gt;¿quién eres tú y qué te mereces recibir?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nginx lo refinó. Los CDNs lo escalaron a millones de dominios. Y el patrón quedó grabado en el ADN de la infraestructura web moderna.&lt;/p&gt;




&lt;h2&gt;
  
  
  2010: Las SPAs rompen todo (de nuevo)
&lt;/h2&gt;

&lt;p&gt;Cuando llegaron las Single Page Applications, el Virtual Hosting tuvo que evolucionar.&lt;/p&gt;

&lt;p&gt;Una SPA tiene una sola entrada: &lt;code&gt;index.html&lt;/code&gt;. Todo lo demás — rutas, vistas, estados — es JavaScript puro que corre en el navegador. El servidor no sabe nada de &lt;code&gt;/dashboard&lt;/code&gt; o &lt;code&gt;/usuario/perfil&lt;/code&gt;. Cuando alguien recarga esa URL, el servidor busca un archivo llamado &lt;code&gt;dashboard&lt;/code&gt; en el disco. No existe. 404.&lt;/p&gt;

&lt;p&gt;La solución fue tan simple como poderosa: &lt;strong&gt;el fallback a index.html&lt;/strong&gt;. Si el servidor no encuentra el archivo estático solicitado, en lugar de responder con un error, devuelve siempre &lt;code&gt;index.html&lt;/code&gt;. React Router toma el control desde ahí y reconstruye la vista correcta.&lt;/p&gt;

&lt;p&gt;En Nginx, son dos líneas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;En ASP.NET Core, Microsoft lo formalizó como middleware oficial con MapFallbackToFile("index.html"). El patrón tenía nombre, documentación, y respaldo institucional.&lt;/p&gt;

&lt;p&gt;Pero todo eso asume que tienes un servidor. ¿Qué pasa cuando no lo tienes?&lt;/p&gt;



&lt;p&gt;2024: El mismo patrón, sin servidor&lt;/p&gt;

&lt;p&gt;Android tiene un mecanismo poco conocido pero extraordinariamente poderoso: shouldInterceptRequest. Cada vez que el WebView está a punto de hacer una petición de red, se detiene y pregunta: ¿alguien quiere manejar esto antes que yo?&lt;/p&gt;

&lt;p&gt;La mayoría de los desarrolladores lo ignora. Algunos lo usan para bloquear anuncios o inyectar headers. Pero hay una tercera posibilidad: usarlo para construir un servidor virtual completo que nunca hace una petición de red real.&lt;/p&gt;

&lt;p&gt;Eso es exactamente lo que hace VirtualHostManager.&lt;/p&gt;

&lt;p&gt;Cuando el WebView intenta cargar &lt;a href="https://app.gfouz.com/assets/main.js" rel="noopener noreferrer"&gt;https://app.gfouz.com/assets/main.js&lt;/a&gt;, la clase intercepta esa petición antes de que salga al mundo. Busca el archivo en la carpeta assets/ del APK. Lo sirve directamente, con el MIME type correcto, con los headers de caché apropiados, sin tocar internet.&lt;/p&gt;

&lt;p&gt;Y cuando el WebView pide &lt;a href="https://app.gfouz.com/dashboard" rel="noopener noreferrer"&gt;https://app.gfouz.com/dashboard&lt;/a&gt; — una ruta de React Router que no existe como archivo — la clase reconoce que no tiene extensión, asume que es una ruta SPA, y devuelve index.html. React Router hace el resto.&lt;/p&gt;

&lt;p&gt;El servidor está dentro de la app. El dominio es ficticio. Las peticiones nunca salen del dispositivo. Y React Router nunca se entera de la diferencia.&lt;/p&gt;



&lt;p&gt;Por qué esto importa más de lo que parece&lt;/p&gt;

&lt;p&gt;La mayoría de soluciones para este problema siguen uno de dos caminos: usar file:// (que rompe CORS y tiene restricciones de seguridad severas) o levantar un servidor HTTP local con librerías como NanoHTTPD (que consume recursos y complica el ciclo de vida de la app).&lt;/p&gt;

&lt;p&gt;VirtualHostManager toma un tercer camino: habitar la infraestructura existente del WebView en lugar de luchar contra ella o rodearla.&lt;/p&gt;

&lt;p&gt;El resultado es una clase que:&lt;/p&gt;

&lt;p&gt;· No abre puertos de red.&lt;br&gt;
· No requiere permisos adicionales.&lt;br&gt;
· Cachea solo lo que vale cachear (index.html, que se pide en cada navegación).&lt;br&gt;
· Transmite archivos grandes como streams, sin cargarlos completos en RAM.&lt;br&gt;
· Valida rutas para prevenir path traversal attacks.&lt;br&gt;
· Soporta 25 tipos de archivo con sus MIME types correctos.&lt;br&gt;
· Funciona en Android 4.1 en adelante.&lt;/p&gt;

&lt;p&gt;Y hace todo eso en menos de 200 líneas de Java.&lt;/p&gt;



&lt;p&gt;El patrón que no envejece&lt;/p&gt;

&lt;p&gt;Lo fascinante de esta historia no es la clase en sí. Es que el problema — ¿cómo sirvo contenido estático con inteligencia? — ha tenido exactamente la misma solución en cada era de la computación:&lt;/p&gt;

&lt;p&gt;1995 → Apache Virtual Hosting&lt;br&gt;
2005 → Nginx try_files&lt;br&gt;
2016 → ASP.NET Core MapFallbackToFile&lt;br&gt;
2024 → Android shouldInterceptRequest + SPA fallback&lt;/p&gt;

&lt;p&gt;El contexto cambia. El hardware cambia. Los frameworks cambian. El patrón permanece.&lt;/p&gt;

&lt;p&gt;Eso, en ingeniería de software, es la señal más confiable de que algo es fundamentalmente correcto.&lt;/p&gt;

&lt;p&gt;VirtualHostManager es parte de FouzStack — una colección de soluciones de ingeniería para desarrollo móvil con tecnologías web.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/gfouz" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F82347049%3Fv%3D4%3Fs%3D400" height="300" class="m-0" width="300"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/gfouz" rel="noopener noreferrer" class="c-link"&gt;
            gfouz (Giovani Fouz) · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Welcome,  I'm excited to share my journey,
skills, and projects with you. Dive in, explore
my projects, and get to know the developer behind
the screen. - gfouz
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg" width="32" height="32"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>android</category>
      <category>architecture</category>
      <category>mobile</category>
      <category>spanish</category>
    </item>
  </channel>
</rss>
