<?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: Gabriel Maglia</title>
    <description>The latest articles on DEV Community by Gabriel Maglia (@gabimaglia).</description>
    <link>https://dev.to/gabimaglia</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%2F1336685%2Fcc80e76a-c98f-48f9-98ab-0bf66f840d4e.jpeg</url>
      <title>DEV Community: Gabriel Maglia</title>
      <link>https://dev.to/gabimaglia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gabimaglia"/>
    <language>en</language>
    <item>
      <title>Cómo un modelo de IA de 115 MB tumbaba Safari en el iPhone</title>
      <dc:creator>Gabriel Maglia</dc:creator>
      <pubDate>Sat, 24 Oct 2026 22:41:23 +0000</pubDate>
      <link>https://dev.to/gabimaglia/como-un-modelo-de-ia-de-115-mb-tumbaba-safari-en-el-iphone-9lg</link>
      <guid>https://dev.to/gabimaglia/como-un-modelo-de-ia-de-115-mb-tumbaba-safari-en-el-iphone-9lg</guid>
      <description>&lt;p&gt;Hace unos meses empecé un proyecto personal: &lt;strong&gt;PhotoCut&lt;/strong&gt;, un editor de fotos que quita el fondo con IA &lt;strong&gt;sin subir la imagen a ningún servidor&lt;/strong&gt;. Todo corre en el navegador: la IA, el recorte y la exportación.&lt;/p&gt;

&lt;p&gt;La idea salió de una molestia concreta: los removedores de fondo gratis que encontraba suben tu foto a sus servidores y después te cobran la descarga en buena calidad. Quería uno que no hiciera ninguna de las dos cosas.&lt;/p&gt;

&lt;p&gt;Les cuento cómo está armado y, sobre todo, los problemas que no vi venir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo funciona por adentro
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El recorte con IA&lt;/strong&gt; usa &lt;a href="https://onnxruntime.ai/" rel="noopener noreferrer"&gt;onnxruntime-web&lt;/a&gt; dentro de un Web Worker, para no congelar la interfaz.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dos modelos según el equipo:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;En computadoras con una GPU que el navegador puede usar (WebGPU), corre &lt;strong&gt;BiRefNet-lite&lt;/strong&gt; en fp16. Pesa unos 115 MB y recorta con mucho detalle.&lt;/li&gt;
&lt;li&gt;En celulares y equipos sin GPU, corre &lt;strong&gt;u2netp&lt;/strong&gt; sobre WebAssembly. Pesa 4,6 MB.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El refinado de bordes&lt;/strong&gt; es un motor GrabCut que escribí en &lt;strong&gt;Rust&lt;/strong&gt; y compilé a &lt;strong&gt;WebAssembly&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La primera vez&lt;/strong&gt; se descarga el modelo y queda en caché. Después del primer recorte, la app funciona hasta sin internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para comprobar que la foto no sale del navegador, medí una sesión completa: 93 peticiones de red y ninguna lleva la imagen. Cualquiera lo puede verificar abriendo la pestaña Red (Network) mientras recorta.&lt;/p&gt;

&lt;h2&gt;
  
  
  El bug: Safari en iPhone recargaba la página
&lt;/h2&gt;

&lt;p&gt;Todo andaba perfecto en mi Mac. Hasta que lo probé en un iPhone 13: tocaba «Recorte IA», bajaba el modelo... y &lt;strong&gt;la página se recargaba sola&lt;/strong&gt;. Sin ningún error en la consola, sin excepción, nada.&lt;/p&gt;

&lt;h3&gt;
  
  
  La hipótesis
&lt;/h3&gt;

&lt;p&gt;iOS 26 trae WebGPU activado en Safari, y la GPU del iPhone cumple las condiciones que yo chequeaba (adapter de hardware, &lt;code&gt;shader-f16&lt;/code&gt;, suficientes storage buffers). Entonces el código decidía: «hay GPU apta → uso BiRefNet».&lt;/p&gt;

&lt;p&gt;El problema es la memoria. El modelo de 115 MB se carga más de una vez: el buffer descargado, la copia dentro del runtime de ONNX y los buffers en la GPU, que para imágenes de 1024×1024 suman cientos de MB. iOS tiene un límite de memoria por pestaña, y cuando lo pasás &lt;strong&gt;mata el proceso&lt;/strong&gt; (lo que en iOS se llama &lt;em&gt;jetsam&lt;/em&gt;). Safari simplemente recarga la página.&lt;/p&gt;

&lt;p&gt;Y como el proceso muere, &lt;strong&gt;no hay ningún error que el código pueda atrapar&lt;/strong&gt;. Mi lógica de respaldo («si BiRefNet falla, uso u2netp») nunca llegaba a ejecutarse. Peor: al recargar no quedaba registro del intento, así que se reiniciaba &lt;strong&gt;cada vez&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  La prueba
&lt;/h3&gt;

&lt;p&gt;Antes de tocar código, una prueba de dos minutos: en el iPhone, &lt;em&gt;Ajustes → Safari → Avanzado → Feature Flags&lt;/em&gt;, desactivar WebGPU. Sin WebGPU el código elige u2netp... y el recorte anduvo perfecto. Hipótesis confirmada.&lt;/p&gt;

&lt;h3&gt;
  
  
  La solución, en dos capas
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Regla de dispositivo:&lt;/strong&gt; en celulares y tablets se usa siempre el modelo chico, aunque haya WebGPU. Ojo con el iPad, que se presenta como Mac en el user agent: la pista es &lt;code&gt;navigator.maxTouchPoints &amp;gt; 1&lt;/code&gt;. Y ojo con los Web Workers, que &lt;strong&gt;no tienen&lt;/strong&gt; &lt;code&gt;maxTouchPoints&lt;/code&gt; ni &lt;code&gt;screen&lt;/code&gt;: el hilo principal calcula «es móvil» y se lo pasa al worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marca anti-bucle:&lt;/strong&gt; antes de abrir el modelo grande, guardo en IndexedDB una marca de «intento en curso» (IndexedDB sí existe dentro del worker; &lt;code&gt;localStorage&lt;/code&gt; no). Si el recorte termina bien, la borro. Si la página se recarga y la marca sigue ahí, el siguiente intento va directo al modelo chico. La marca vence a los 7 días, por si fue un cierre casual de la pestaña.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La segunda capa existe porque la primera puede fallar con algún dispositivo que no anticipé. Ningún equipo debería quedar recargándose para siempre.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La lección:&lt;/strong&gt; los tests en el WebKit de Playwright pasaban todos, porque en la Mac no hay WebGPU en ese contexto. Un iPhone real encontró en dos minutos lo que ningún test veía.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Safari y el WebP que en realidad es PNG
&lt;/h2&gt;

&lt;p&gt;Uno de los presets exporta stickers de WhatsApp: WebP de 512×512, de menos de 100 KB.&lt;/p&gt;

&lt;p&gt;En Chrome y Firefox, &lt;code&gt;canvas.toBlob(cb, 'image/webp', calidad)&lt;/code&gt; funciona. En Safari &lt;strong&gt;no tira error: te devuelve un PNG&lt;/strong&gt;. Si nombrás el archivo según el formato que pediste, terminás entregando un PNG llamado &lt;code&gt;.webp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lo que hice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecto una vez si hay WebP nativo: codifico un canvas chiquito y miro &lt;code&gt;blob.type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si no hay, cargo &lt;strong&gt;libwebp compilado a WebAssembly&lt;/strong&gt; (unos 110 KB) en un worker dedicado, solo en ese caso. Los usuarios de Chrome nunca lo descargan.&lt;/li&gt;
&lt;li&gt;El nombre del archivo sale del tipo real del blob, nunca del que pedí.&lt;/li&gt;
&lt;li&gt;En los tests con WebKit verifico que los bytes empiecen con &lt;code&gt;RIFF....WEBP&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lo que aprendí
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correr IA en el navegador ya es viable&lt;/strong&gt;, pero el límite real no es la velocidad, es la &lt;strong&gt;memoria&lt;/strong&gt; en celulares.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un proceso que muere no avisa.&lt;/strong&gt; Si tu lógica de respaldo depende de atrapar un error, no te va a salvar de un &lt;em&gt;jetsam&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probá en dispositivos reales.&lt;/strong&gt; Los emuladores y los navegadores de testing no tienen los mismos límites.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Probalo
&lt;/h2&gt;

&lt;p&gt;PhotoCut es gratis, sin cuenta y sin marca de agua, y el código es abierto (MIT). El sitio tiene publicidad de Google para pagar el hosting; esos scripts hacen sus propias peticiones, pero ninguna lleva la foto.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;App:&lt;/strong&gt; &lt;a href="https://www.photocutapp.com/editor/" rel="noopener noreferrer"&gt;https://www.photocutapp.com/editor/&lt;/a&gt;&lt;br&gt;
👉 &lt;strong&gt;Código:&lt;/strong&gt; &lt;a href="https://github.com/gabiMaglia/PhotoCutOnline" rel="noopener noreferrer"&gt;https://github.com/gabiMaglia/PhotoCutOnline&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Me encantaría saber cómo anda en sus celulares, sobre todo en equipos de gama baja. ¡Cualquier crítica es bienvenida!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webassembly</category>
      <category>ai</category>
    </item>
    <item>
      <title>Lo que hice:

- Detecto una vez si hay WebP nativo: codifico un canvas chiquito y miro `blob.type`.
- Si no hay, cargo **libwebp compilado a WebAssembly** (unos 110 KB) en un worker dedicado, solo en ese caso. Los usuarios de Chrome nunca lo descargan.
- E</title>
      <dc:creator>Gabriel Maglia</dc:creator>
      <pubDate>Sat, 24 Oct 2026 22:34:27 +0000</pubDate>
      <link>https://dev.to/gabimaglia/lo-que-hice-detecto-una-vez-si-hay-webp-nativo-codifico-un-canvas-chiquito-y-miro-3fd9</link>
      <guid>https://dev.to/gabimaglia/lo-que-hice-detecto-una-vez-si-hay-webp-nativo-codifico-un-canvas-chiquito-y-miro-3fd9</guid>
      <description></description>
    </item>
  </channel>
</rss>
