<?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>Forem: Juan Torchia</title>
    <description>The latest articles on Forem by Juan Torchia (@jtorchia).</description>
    <link>https://forem.com/jtorchia</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F885942%2F5b3b3860-d364-4de0-a335-cb7c251109d9.jpeg</url>
      <title>Forem: Juan Torchia</title>
      <link>https://forem.com/jtorchia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jtorchia"/>
    <language>en</language>
    <item>
      <title>Nunca más tipees openssl x509 -text -noout: creé una extensión de VS Code para ver certificados SSL/TLS</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 08 Apr 2026 11:36:42 +0000</pubDate>
      <link>https://forem.com/jtorchia/nunca-mas-tipees-openssl-x509-text-noout-cree-una-extension-de-vs-code-para-ver-certificados-ag1</link>
      <guid>https://forem.com/jtorchia/nunca-mas-tipees-openssl-x509-text-noout-cree-una-extension-de-vs-code-para-ver-certificados-ag1</guid>
      <description>&lt;p&gt;Eran las 11 de la noche, había un incidente en producción, y yo estaba con tres terminales abiertas intentando recordar si era &lt;code&gt;openssl x509 -text -noout -in cert.pem&lt;/code&gt; o &lt;code&gt;openssl pkcs12 -info -in keystore.p12 -noout&lt;/code&gt;. El servidor tiraba TLS handshake failed y yo necesitaba confirmar en dos segundos si el certificado que habíamos desplegado era el correcto o si alguien había subido el de staging por error.&lt;/p&gt;

&lt;p&gt;Ese momento de furia silenciosa donde querés pegar un grito pero son las 11 de la noche y tu familia está durmiendo — lo conocés, ¿no?&lt;/p&gt;

&lt;p&gt;Ahí decidí que iba a construir algo para no volver a vivirlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Veintipico años mirando certificados en una terminal negra
&lt;/h2&gt;

&lt;p&gt;Cuando empecé a administrar servidores en el hosting allá por 2007, los certificados SSL eran una rareza cara que solo las empresas grandes podían pagar. Yo los veía como objetos místicos: archivos binarios raros con extensiones que nadie sabía bien qué significaban — &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.der&lt;/code&gt;, &lt;code&gt;.p12&lt;/code&gt;, &lt;code&gt;.pfx&lt;/code&gt;, &lt;code&gt;.crt&lt;/code&gt;, &lt;code&gt;.cer&lt;/code&gt;. La misma cosa con seis nombres distintos dependiendo de quién los generó.&lt;/p&gt;

&lt;p&gt;Aprendí a leerlos con &lt;code&gt;openssl&lt;/code&gt; en la terminal como aprendí todo en esa época: rompiendo cosas en producción y rezando. Con el tiempo lo interioricé. Pero nunca dejó de ser un quilombo.&lt;/p&gt;

&lt;p&gt;Después vino Let's Encrypt en 2015, los certificados se democratizaron, y de repente &lt;em&gt;todos&lt;/em&gt; los proyectos tienen SSL. Renovaciones automáticas, cadenas de certificados, SANs con 40 dominios, keystores de Java para los proyectos enterprise con Spring Boot... El volumen de archivos de certificados que pasan por un workspace moderno es brutal.&lt;/p&gt;

&lt;p&gt;Y cada vez que necesitás inspeccionar uno, la historia es la misma: salís de VS Code, abrís una terminal, tratás de recordar el comando, lo googleás si no te acordás, lo ejecutás, leés un wall of text que ocupa media pantalla, y cerrás la terminal. Flujo destruido.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real no es el comando, es el contexto switching
&lt;/h2&gt;

&lt;p&gt;Mirá esto. Cuando trabajás en una aplicación que consume servicios externos, validar los certificados es parte del ciclo de desarrollo normal. Estás configurando mutual TLS para conectarte a una API bancaria, o estás depurando por qué el cliente Java no confía en el certificado del servidor, o simplemente querés verificar que el &lt;code&gt;.p12&lt;/code&gt; que te mandó el equipo de seguridad tiene el CN correcto antes de meterlo en Kubernetes como un Secret.&lt;/p&gt;

&lt;p&gt;El problema no es que &lt;code&gt;openssl&lt;/code&gt; sea difícil. El problema es que &lt;strong&gt;te saca del contexto&lt;/strong&gt;. Tenés el archivo ahí, en el explorador de VS Code, y para ver su contenido tenés que hacer un viaje de ida y vuelta a la terminal. Es como tener que salir a la calle para ver qué hay en la heladera.&lt;/p&gt;

&lt;p&gt;Los archivos de imagen los podés previsualizar directamente. Los PDF también, con extensiones. Los SVGs se renderizan solos. ¿Por qué los certificados X.509 no?&lt;/p&gt;

&lt;h2&gt;
  
  
  Así nació gmm.certview
&lt;/h2&gt;

&lt;p&gt;La idea era simple: &lt;strong&gt;doble click en un &lt;code&gt;.pem&lt;/code&gt; y VS Code te muestra todo&lt;/strong&gt;. Sin terminal. Sin recordar flags. Sin contexto switching.&lt;/p&gt;

&lt;p&gt;El stack fue casi obvio para mí: TypeScript porque estoy en el ecosistema VS Code, y &lt;a href="https://github.com/digitalbazaar/forge" rel="noopener noreferrer"&gt;node-forge&lt;/a&gt; para el parsing criptográfico porque es la librería más completa y madura del ecosistema Node.js para manejar PKI. No quería depender de binarios externos ni de llamadas al sistema — necesitaba que todo funcionara 100% offline, en modo avión si hacía falta, y especialmente en &lt;strong&gt;ambientes corporativos donde instalarte cualquier cosa es un trámite de tres formularios y dos aprobaciones&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;La pieza técnica más interesante fue el Custom Editor Provider de VS Code. En vez de registrar un comando que abrís manualmente, la extensión se registra como el editor nativo para ciertos tipos de archivo. VS Code le dice "oye, el usuario quiere abrir este &lt;code&gt;.pem&lt;/code&gt;, ¿vos lo manejás?" y la extensión responde que sí y toma control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Así registrás un Custom Editor Provider en VS Code&lt;/span&gt;
&lt;span class="c1"&gt;// El 'viewType' tiene que matchear el que declarás en package.json&lt;/span&gt;
&lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerCustomEditorProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gmm.certview.editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// identificador único de tu editor&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CertificateEditorProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Mantiene el webview en memoria aunque no sea la pestaña activa&lt;/span&gt;
    &lt;span class="c1"&gt;// Importante para no re-parsear el cert cada vez que cambiás de tab&lt;/span&gt;
    &lt;span class="na"&gt;webviewOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retainContextWhenHidden&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Permite que múltiples tabs abran el mismo archivo&lt;/span&gt;
    &lt;span class="na"&gt;supportsMultipleEditorsPerDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&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 provider recibe el contenido del archivo como &lt;code&gt;Uint8Array&lt;/code&gt; y lo manda a node-forge para el parsing. Después renderiza todo en un Webview — básicamente una página HTML corriendo dentro de VS Code con acceso restringido al sistema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que ves cuando abrís un certificado
&lt;/h2&gt;

&lt;p&gt;Abrís un &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.crt&lt;/code&gt;, &lt;code&gt;.cer&lt;/code&gt; o &lt;code&gt;.der&lt;/code&gt; y en vez del texto en base64 o el binario incomprensible, te aparece un panel con:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subject e Issuer&lt;/strong&gt; — quién es y quién lo firmó, con los campos del Distinguished Name bien separados (CN, O, OU, C, etc.) en vez del string gigante &lt;code&gt;CN=api.empresa.com, O=Empresa S.A., C=AR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fechas de validez con alerta visual&lt;/strong&gt; — esto fue lo primero que implementé porque es lo que más importa en un incidente. Verde si está vigente, &lt;strong&gt;amarillo si vence en menos de 30 días&lt;/strong&gt;, &lt;strong&gt;rojo si ya expiró&lt;/strong&gt;. Sin tener que calcular nada mentalmente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lógica de alertas de vencimiento&lt;/span&gt;
&lt;span class="c1"&gt;// node-forge devuelve las fechas como objetos Date en cert.validity&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCertificateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notAfter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expiring&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ahora&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diasRestantes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;notAfter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ahora&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diasRestantes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expired&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// Rojo — ya expiró&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diasRestantes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expiring&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Amarillo — ojo con esto&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;valid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                               &lt;span class="c1"&gt;// Verde — todo bien&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;Fingerprints SHA-1 y SHA-256&lt;/strong&gt; con un botón de copiar al portapapeles. Cuántas veces tuve que comparar fingerprints manualmente carácter por carácter en una terminal para verificar que dos certificados eran el mismo. Con el botón de copiar, lo pegás directo donde lo necesitás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clave pública&lt;/strong&gt; — algoritmo (RSA, ECDSA, EdDSA) y tamaño en bits. Si alguien te manda un certificado RSA de 1024 bits en 2024, lo ves de entrada y mandás para atrás el pedido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extensiones X.509 y SANs&lt;/strong&gt; — los Subject Alternative Names listados limpiamente. Cuándo un certificado dice que es válido para &lt;code&gt;*.empresa.com&lt;/code&gt; y para &lt;code&gt;empresa.com&lt;/code&gt; y para cuatro servicios internos más, lo ves de un vistazo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Formatos soportados: no solo el .pem de toda la vida
&lt;/h2&gt;

&lt;p&gt;Aquí es donde la cosa se pone buena para los que trabajan en ambientes enterprise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PKCS#7 / &lt;code&gt;.p7b&lt;/code&gt;&lt;/strong&gt; — cadenas de certificados. Cada cert del bundle aparece en su propia pestaña dentro del panel. Muy útil para validar que la cadena está completa antes de configurar Nginx o un load balancer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PKCS#12 / &lt;code&gt;.p12&lt;/code&gt; / &lt;code&gt;.pfx&lt;/code&gt;&lt;/strong&gt; — keystores con contraseña. La extensión te muestra un prompt, ingresás la password, y te abre el contenido: certificado, clave privada (muestra el tipo y tamaño, no el material de clave — no somos locos), y los certificados de la cadena. Esto en una terminal son tres comandos distintos con flags diferentes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CSRs PKCS#10&lt;/strong&gt; — Certificate Signing Requests. Podés verificar que el CN y los SANs del CSR que estás a punto de mandar a la CA son los que querés antes de hacerlo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRLs&lt;/strong&gt; — Certificate Revocation Lists. Menos común pero cuando lo necesitás, lo necesitás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panel sidebar del workspace&lt;/strong&gt; — esto lo agregué después porque me di cuenta de que el problema no era solo abrir un cert individual. A veces querés ver todos los certificados del proyecto de una: cuál vence primero, si hay alguno ya expirado, etc. El sidebar escanea el workspace y lista todo con sus estados visuales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sin telemetría. En serio.
&lt;/h2&gt;

&lt;p&gt;Esto lo digo explícitamente porque sé que importa. En ambientes corporativos financieros, de salud o gobierno, no podés usar extensiones que mandan datos a ningún lado. Los certificados son material criptográfico sensible — pueden ser certificados de producción, de servicios internos, de infraestructura crítica.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gmm.certview no manda ningún dato a ningún servidor&lt;/strong&gt;. Todo el procesamiento ocurre localmente en tu máquina con node-forge. No hay analytics, no hay telemetría de uso, no hay nada. El código está disponible para auditar si tu equipo de seguridad lo requiere.&lt;/p&gt;

&lt;p&gt;Funciona en modo avión. Funciona en redes corporativas con proxy restrictivo. Funciona en máquinas virtuales air-gapped. Si VS Code corre, la extensión funciona.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instalación en dos clics
&lt;/h2&gt;

&lt;p&gt;Podés instalar X509 Certificate Utility directo desde el marketplace:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=gmm.certview" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=gmm.certview&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O desde VS Code: &lt;code&gt;Ctrl+P&lt;/code&gt; → &lt;code&gt;ext install gmm.certview&lt;/code&gt; → Enter. Listo.&lt;/p&gt;

&lt;p&gt;Después de instalar, hacé doble click en cualquier archivo &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.crt&lt;/code&gt;, &lt;code&gt;.cer&lt;/code&gt;, &lt;code&gt;.p12&lt;/code&gt;, &lt;code&gt;.pfx&lt;/code&gt;, &lt;code&gt;.p7b&lt;/code&gt; o &lt;code&gt;.csr&lt;/code&gt; en el explorador de VS Code. Debería abrirse el viewer automáticamente. Si por algún motivo VS Code lo abre como texto, click derecho → "Reopen with" → "X509 Certificate Viewer".&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores que cometí en el camino
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error 1: subestimar los encodings.&lt;/strong&gt; Pensé que todos los &lt;code&gt;.pem&lt;/code&gt; eran iguales. No. Hay PEM con PKCS#1, PKCS#8, con headers específicos, con y sin bag attributes cuando vienen de un PKCS#12 exportado. Tuve que manejar cada variante por separado y agregar fallbacks. Si encontrás un archivo que no parsea bien, mandame el error (sin el cert, obvio) y lo agrego.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: el Webview y el Content Security Policy.&lt;/strong&gt; VS Code tiene restricciones bastante estrictas sobre lo que podés hacer en un Webview. La primera versión tiraba errores de CSP en la consola de desarrollo. Tuve que revisar todos los estilos y scripts inline y mover todo a recursos locales con los URIs correctos usando &lt;code&gt;webview.asWebviewUri()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: asumir que node-forge maneja todo.&lt;/strong&gt; Para algunos casos edge de PKCS#12 con algoritmos más exóticos, node-forge tira excepciones crípticas. Tuve que agregar manejo de errores más granular y mensajes descriptivos para que el usuario entienda qué pasó en vez de ver "Error: invalid asn1 encoding".&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué existe el estándar X.509 y por qué es así de complicado
&lt;/h2&gt;

&lt;p&gt;X.509 viene de 1988. Sí, 1988 — el año en que se publicó como parte del estándar X.500 de la ITU-T para directorios distribuidos. La Internet de hoy usa una versión evolucionada (v3, de 1996) con extensiones que se fueron agregando para soportar casos de uso que en 1988 nadie imaginaba: Subject Alternative Names para múltiples dominios, Extended Key Usage para distinguir certs de servidor de certs de código signing, OCSP para revocación en tiempo real.&lt;/p&gt;

&lt;p&gt;La cantidad de formatos de archivo existe porque cada ecosistema fue haciendo lo suyo: OpenSSL popularizó el PEM (Privacy Enhanced Mail — sí, originalmente era para emails cifrados). Java usa JKS y PKCS#12. Windows usa PFX. Cada uno con sus propias convenciones.&lt;/p&gt;

&lt;p&gt;Entender esto ayuda a entender por qué la herramienta tenía que soportar todos esos formatos. No es capricho, es la realidad de los proyectos modernos donde conviven stacks distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué viene después
&lt;/h2&gt;

&lt;p&gt;Hay algunas cosas en el roadmap que me tienen entusiasmado:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validación de cadena de confianza&lt;/strong&gt;: dado un certificado de leaf y una CA bundle, verificar que la cadena es válida sin salir de VS Code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comparación de certificados&lt;/strong&gt;: seleccionar dos certs y ver las diferencias resaltadas. Muy útil para verificar rotaciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decodificación de campos OID desconocidos&lt;/strong&gt;: hay extensiones X.509 propietarias de algunas CAs que node-forge no conoce. Quiero agregar una lookup table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soporte para JKS de Java&lt;/strong&gt;: técnicamente necesitaría re-implementar el formato JKS en TypeScript o usar una librería Java via WASM. Es el challenge más interesante que tengo por delante.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si usás la extensión y tenés feedback, podés abrir un issue en el repo o contactarme directo. Los casos edge que más me interesan son los de ambientes enterprise con configuraciones raras — esos son los que hacen que la herramienta sea realmente robusta.&lt;/p&gt;

&lt;p&gt;La próxima vez que estés en un incidente a las 11 de la noche tratando de recordar el comando de openssl, espero que puedas simplemente hacer doble click y seguir.&lt;/p&gt;

&lt;p&gt;Instalala: &lt;strong&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=gmm.certview" rel="noopener noreferrer"&gt;marketplace.visualstudio.com/items?itemName=gmm.certview&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/x509-certificate-viewer-vscode-extension" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>seguridad</category>
      <category>tls</category>
      <category>vscode</category>
    </item>
    <item>
      <title>Harté de esperar que alguien maintuviera la extensión de HAProxy para VS Code — así que la hice yo</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 08 Apr 2026 05:22:37 +0000</pubDate>
      <link>https://forem.com/jtorchia/harte-de-esperar-que-alguien-maintuviera-la-extension-de-haproxy-para-vs-code-asi-que-la-hice-yo-j0o</link>
      <guid>https://forem.com/jtorchia/harte-de-esperar-que-alguien-maintuviera-la-extension-de-haproxy-para-vs-code-asi-que-la-hice-yo-j0o</guid>
      <description>&lt;p&gt;Hay un momento específico en el que te das cuenta de que esperaste demasiado. Para mí fue un martes a las 11 de la noche, con tres ventanas de VS Code abiertas, un HAProxy 2.8 corriendo en Docker, y un error de validación que no entendía por qué estaba fallando. Abro la extensión de syntax highlighting que tenía instalada — la única que existía en el marketplace — y veo que el último commit fue en &lt;strong&gt;2019&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cinco años sin un solo cambio. HAProxy pasó de la versión 2.0 a la 3.1 en ese tiempo. Metieron &lt;code&gt;log-format-sd&lt;/code&gt;, reescribieron el comportamiento de &lt;code&gt;option http-server-close&lt;/code&gt;, deprecaron directivas enteras. Y la extensión ahí, congelada en el tiempo como una momia digital, sin saber nada de nada.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ese martes dije: basta. Si nadie lo va a hacer, lo hago yo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué HAProxy merece una extensión decente (y por qué casi nadie habla de esto)
&lt;/h2&gt;

&lt;p&gt;Antes de meterme en el código, necesito darte contexto porque sé que el 80% de los devs que leen esto trabajan con Nginx o Traefik y creen que HAProxy es "esa cosa vieja que usan los bancos". Y sí, tienen razón en la segunda parte — los bancos lo usan, las telcos lo usan, los exchanges de crypto lo usan. Pero no porque sea viejo. Sino porque es &lt;strong&gt;brutalmente eficiente&lt;/strong&gt; y tiene el modelo de configuración más expresivo que existe para un proxy.&lt;/p&gt;

&lt;p&gt;Yo lo uso todos los días. En el trabajo para balancear tráfico entre microservicios. En mi homelab tengo un stack con HAProxy al frente, tres backends de servicios internos, rate limiting por IP, ACLs que distinguen tráfico de la LAN del tráfico que viene por VPN, y health checks cada cinco segundos. Todo en un archivo &lt;code&gt;.cfg&lt;/code&gt; que tiene más de 400 líneas.&lt;/p&gt;

&lt;p&gt;El problema es que ese archivo &lt;code&gt;.cfg&lt;/code&gt; es básicamente texto plano para cualquier editor. Sin schema, sin LSP, sin nada. Escribís &lt;code&gt;frontend mi-frontend&lt;/code&gt; y el editor no sabe que adentro de ese bloque hay directivas específicas que no existen en ningún otro contexto. Escribís &lt;code&gt;backend&lt;/code&gt; y no te sugiere &lt;code&gt;balance roundrobin&lt;/code&gt; versus &lt;code&gt;balance leastconn&lt;/code&gt;. Usás una directiva que fue deprecada en 2.6 y nadie te avisa.&lt;/p&gt;

&lt;p&gt;Eso es exactamente lo que fui a arreglar.&lt;/p&gt;

&lt;h2&gt;
  
  
  La arquitectura: no era tan simple como "un JSON con keywords"
&lt;/h2&gt;

&lt;p&gt;La primera semana pensé que iba a ser fácil. "Meto todas las keywords en un archivo de gramática TextMate, le doy colores, listo". Esa ingenuidad duró exactamente hasta que abrí el spec completo de configuración de HAProxy.&lt;/p&gt;

&lt;p&gt;HAProxy tiene una arquitectura de secciones: &lt;code&gt;global&lt;/code&gt;, &lt;code&gt;defaults&lt;/code&gt;, &lt;code&gt;frontend&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;, &lt;code&gt;listen&lt;/code&gt;, &lt;code&gt;peers&lt;/code&gt;, &lt;code&gt;resolvers&lt;/code&gt;, &lt;code&gt;userlist&lt;/code&gt;, &lt;code&gt;cache&lt;/code&gt;, &lt;code&gt;program&lt;/code&gt;. Y &lt;strong&gt;cada sección acepta un subconjunto diferente de directivas&lt;/strong&gt;. &lt;code&gt;bind&lt;/code&gt; solo existe en &lt;code&gt;frontend&lt;/code&gt; y &lt;code&gt;listen&lt;/code&gt;. &lt;code&gt;server&lt;/code&gt; solo existe en &lt;code&gt;backend&lt;/code&gt; y &lt;code&gt;listen&lt;/code&gt;. &lt;code&gt;mode&lt;/code&gt; existe en varias pero con diferentes valores permitidos dependiendo del contexto.&lt;/p&gt;

&lt;p&gt;Eso no se puede resolver con TextMate grammars. Eso necesita un Language Server Protocol real.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/server/haproxy-language-server.ts&lt;/span&gt;
&lt;span class="c1"&gt;// El corazón del LSP — inicializamos las capacidades que vamos a soportar&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TextDocuments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ProposedFeatures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CompletionItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CompletionItemKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;TextDocumentSyncKind&lt;/span&gt;&lt;span class="p"&gt;,&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;vscode-languageserver/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TextDocument&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;vscode-languageserver-textdocument&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HaproxyParser&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;./parser/haproxy-parser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CompletionProvider&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;./providers/completion-provider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DiagnosticsProvider&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;./providers/diagnostics-provider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ProposedFeatures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;all&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;documents&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;TextDocuments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TextDocument&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Cuando el cliente (VS Code) nos pide completions, necesitamos saber&lt;/span&gt;
&lt;span class="c1"&gt;// en qué sección estamos parados para dar sugerencias contextuales&lt;/span&gt;
&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onInitialize&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="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;textDocumentSync&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TextDocumentSyncKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Incremental&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;completionProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;resolveProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// habilitamos el detalle de cada ítem&lt;/span&gt;
      &lt;span class="na"&gt;triggerCharacters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;// autocompletado al tipear espacio o tab&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;definitionProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// go-to-definition para backends&lt;/span&gt;
    &lt;span class="na"&gt;codeActionProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// quickfix para directivas deprecadas&lt;/span&gt;
    &lt;span class="na"&gt;diagnosticProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;interFileDependencies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;workspaceDiagnostics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="p"&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 parser fue la parte más complicada y la que más tiempo me llevó. HAProxy no tiene un formato estricto tipo YAML o JSON — es un lenguaje de configuración propio con indentación opcional, comentarios con &lt;code&gt;#&lt;/code&gt;, continuación de línea con &lt;code&gt;\&lt;/code&gt;, y una semántica de contexto que depende enteramente de en qué sección estás.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/server/parser/haproxy-parser.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Parser que entiende el contexto de sección — clave para todo lo demás&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SectionType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// 'global' | 'defaults' | 'frontend' | 'backend' | etc.&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// nombre de la sección (null para global/defaults)&lt;/span&gt;
  &lt;span class="nl"&gt;startLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;endLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;directives&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ParsedDirective&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HaproxyParser&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineNumber&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// Ignoramos comentarios y líneas vacías&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&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="c1"&gt;// Detectamos el inicio de una nueva sección&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sectionMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;global|defaults|frontend|backend|listen|peers|resolvers|userlist|cache|program&lt;/span&gt;&lt;span class="se"&gt;)\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\S&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sectionMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Cerramos la sección anterior si existe&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endLine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lineNumber&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Iniciamos la nueva sección con su tipo y nombre&lt;/span&gt;
        &lt;span class="nx"&gt;currentSection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sectionMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SectionType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sectionMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;startLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lineNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;endLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// se completa cuando encontramos la próxima sección&lt;/span&gt;
          &lt;span class="na"&gt;directives&lt;/span&gt;&lt;span class="p"&gt;:&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;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Si estamos dentro de una sección, parseamos la directiva&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;directives&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseDirective&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lineNumber&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// No olvidemos cerrar la última sección&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSection&lt;/span&gt;&lt;span class="p"&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;currentSection&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;endLine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentSection&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ParsedSection&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="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&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;h2&gt;
  
  
  El autocompletado contextual: la feature que cambió todo
&lt;/h2&gt;

&lt;p&gt;Una vez que tenía el parser funcionando, el autocompletado contextual fue casi natural. La idea es simple: cuando VS Code te pide completions, le preguntás al parser "¿en qué sección está el cursor?" y filtrás las sugerencias en base a eso.&lt;/p&gt;

&lt;p&gt;¿Estás en un &lt;code&gt;frontend&lt;/code&gt;? Te ofrezco &lt;code&gt;bind&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;acl&lt;/code&gt;, &lt;code&gt;use_backend&lt;/code&gt;, &lt;code&gt;default_backend&lt;/code&gt;, &lt;code&gt;option&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;... pero NO te ofrezco &lt;code&gt;server&lt;/code&gt; ni &lt;code&gt;balance&lt;/code&gt;, que son de &lt;code&gt;backend&lt;/code&gt;. ¿Estás en &lt;code&gt;global&lt;/code&gt;? Te ofrezco &lt;code&gt;maxconn&lt;/code&gt;, &lt;code&gt;daemon&lt;/code&gt;, &lt;code&gt;log&lt;/code&gt;, &lt;code&gt;ssl-default-bind-options&lt;/code&gt;... y nada más.&lt;/p&gt;

&lt;p&gt;Esto parece trivial pero la diferencia en la experiencia de uso es brutal. En una configuración de HAProxy compleja con 10 secciones, el autocompletado que no entiende contexto te tira 200 opciones mezcladas. El mío te tira exactamente las que aplican a donde estás parado.&lt;/p&gt;

&lt;p&gt;Pero el feature que más me enorgullece es el &lt;strong&gt;go-to-definition para backends&lt;/strong&gt;. Si en tu &lt;code&gt;frontend&lt;/code&gt; tenés &lt;code&gt;default_backend mi-api&lt;/code&gt; y presionás F12, te lleva directo a la sección &lt;code&gt;backend mi-api&lt;/code&gt;. Suena simple. Pero cuando tu config tiene 400 líneas y 15 backends, ese F12 te ahorra literalmente minutos de scroll todos los días.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validación multi-versión: el quilombo de HAProxy 2.4 a 3.1
&lt;/h2&gt;

&lt;p&gt;Acá es donde me volví un poco loco. HAProxy cambió bastante entre versiones. Cosas que eran válidas en 2.4 quedaron deprecadas en 2.6, y otras directamente removidas en 3.0. Si la extensión no sabe qué versión estás usando, los diagnósticos van a estar llenos de falsos positivos o falsos negativos.&lt;/p&gt;

&lt;p&gt;La solución fue agregar una setting en VS Code donde el usuario declara su versión de HAProxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.vscode/settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuración&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;por&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;workspace&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gmm-haproxy.version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gmm-haproxy.strictMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y del lado del servidor, mantengo un registro de qué directivas existen en qué versión, cuáles fueron deprecadas y cuándo, y cuáles fueron removidas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/server/schema/version-registry.ts&lt;/span&gt;
&lt;span class="c1"&gt;// Registry de directivas por versión — acá está el conocimiento duro de HAProxy&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DirectiveInfo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SectionType&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;        &lt;span class="c1"&gt;// en qué secciones es válida&lt;/span&gt;
  &lt;span class="nl"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                  &lt;span class="c1"&gt;// versión en que fue introducida&lt;/span&gt;
  &lt;span class="nl"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// versión en que fue deprecada&lt;/span&gt;
  &lt;span class="nl"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// versión en que fue removida&lt;/span&gt;
  &lt;span class="nl"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// directiva recomendada si fue deprecada&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Ejemplo real de directivas con su historial de versiones&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DIRECTIVE_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DirectiveInfo&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;option forwardfor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frontend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;defaults&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Agrega el header X-Forwarded-For con la IP real del cliente&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reqadd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frontend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;deprecated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// deprecada en 2.2&lt;/span&gt;
    &lt;span class="na"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// removida en 3.0&lt;/span&gt;
    &lt;span class="na"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http-request set-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// la alternativa moderna&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[DEPRECADA] Agregaba headers a la request. Usá http-request set-header&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http-request set-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frontend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Modifica o agrega headers HTTP en la request entrante&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ... y así con ~400 directivas más&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando el DiagnosticsProvider detecta que usás &lt;code&gt;reqadd&lt;/code&gt; en una config con version &lt;code&gt;3.0&lt;/code&gt;, te tira un error con quickfix incluido: "Reemplazar por &lt;code&gt;http-request set-header&lt;/code&gt;". Un click y listo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores que cometí (y que vos vas a cometer si hacés algo parecido)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error 1: Subestimar el tiempo de startup del LSP.&lt;/strong&gt; El primer prototipo parseaba el documento entero en cada keystroke. En archivos grandes, el lag era notable. La solución fue parsing incremental — solo re-parseás las secciones que cambiaron.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: No manejar configs incompletas.&lt;/strong&gt; Mientras escribís, tu config está rota la mayoría del tiempo. El parser tiene que ser tolerante a errores y producir un AST parcial útil en lugar de explotar. Tomó dos semanas extra hacer el error recovery decente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: Creer que la API de VS Code es estable.&lt;/strong&gt; Entre la versión que leí en la doc y la versión que tenía instalada, había diferencias sutiles en cómo funcionaba &lt;code&gt;onDocumentDiagnostic&lt;/code&gt;. Aprendí a siempre testear contra la versión mínima declarada en el &lt;code&gt;engines.vscode&lt;/code&gt; del &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 4: No tener un corpus de configs reales para testear.&lt;/strong&gt; Armé un directorio con configs reales anonimizadas de mi homelab y del trabajo. Eso solo encontró más bugs que cualquier test unitario que escribí.&lt;/p&gt;

&lt;h2&gt;
  
  
  El resultado: lo que uso todos los días
&lt;/h2&gt;

&lt;p&gt;Hoy &lt;code&gt;gmm-haproxy-vscode&lt;/code&gt; tiene:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Syntax highlighting contextual&lt;/strong&gt; — diferencia visualmente entre nombres de sección, directivas, valores, ACL names y comentarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LSP propio&lt;/strong&gt; con autocompletado filtrado por sección&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validación en tiempo real&lt;/strong&gt; contra el schema de la versión declarada (2.4, 2.6, 2.8, 3.0, 3.1)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go-to-definition&lt;/strong&gt; para backends referenciados en &lt;code&gt;use_backend&lt;/code&gt; y &lt;code&gt;default_backend&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quickfix automático&lt;/strong&gt; para directivas deprecadas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover documentation&lt;/strong&gt; — pasás el mouse por cualquier directiva y te explica qué hace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snippets&lt;/strong&gt; para estructuras comunes: frontend básico, backend con health check, ACL de rate limiting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La semana pasada la usé para refactorizar toda la config de mi homelab de HAProxy 2.8 a 3.1. Sin la extensión, ese proceso hubiera sido un domingo entero de revisar el changelog y buscar directivas obsoletas a mano. Con la extensión, fueron dos horas — la mayoría del tiempo la pasé aplicando quickfixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué hice esto y no simplemente usé Nginx
&lt;/h2&gt;

&lt;p&gt;Alguien me va a preguntar eso, así que lo respondo antes. HAProxy hace cosas que Nginx no hace igual de bien. El modelo de ACLs de HAProxy es extraordinariamente expresivo. Podés tomar decisiones de routing basadas en headers, paths, source IPs, tiempo del día, peso del backend, número de conexiones activas — todo en el archivo de config, sin scripting. El health checking es más granular. El modelo de estadísticas via socket es más completo.&lt;/p&gt;

&lt;p&gt;¿Es la herramienta correcta para todo? No. Para un proyecto personal chico, Traefik o Caddy son más cómodos. Pero cuando tenés tráfico real y necesitás control fino, HAProxy sigue siendo el rey. Y el rey se merece una extensión que no sea un zombie del 2019.&lt;/p&gt;

&lt;p&gt;Si querés probar &lt;code&gt;gmm-haproxy-vscode&lt;/code&gt;, la vas a encontrar en el VS Code Marketplace. Si encontrás un bug o una directiva que no reconoce — y seguro vas a encontrar, el spec de HAProxy es enorme — abrí un issue. Lo maintaingo activamente porque lo uso todos los días. Esa es la mejor garantía que puedo darte.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/haproxy-vscode-extension-gmm-haproxy" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>haproxy</category>
      <category>vscode</category>
      <category>lsp</category>
    </item>
    <item>
      <title>Metí Gemma corriendo en el browser, sin API keys, y me cambió cómo pienso el edge</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 08 Apr 2026 05:21:38 +0000</pubDate>
      <link>https://forem.com/jtorchia/meti-gemma-corriendo-en-el-browser-sin-api-keys-y-me-cambio-como-pienso-el-edge-3d18</link>
      <guid>https://forem.com/jtorchia/meti-gemma-corriendo-en-el-browser-sin-api-keys-y-me-cambio-como-pienso-el-edge-3d18</guid>
      <description>&lt;p&gt;Hay una creencia instalada en la comunidad dev sobre AI en producción que está, con todo respeto, bastante equivocada: que para meter un LLM en tu app necesitás sí o sí una API key, un server que haga la inferencia, y alguien que pague la factura de OpenAI a fin de mes. La arquitectura por default en 2025 es: frontend → API call → cloud → respuesta. Siempre. Sin excepción.&lt;/p&gt;

&lt;p&gt;Mentira.&lt;/p&gt;

&lt;p&gt;La semana pasada corrí Gemma — el modelo abierto de Google — directo en el browser. Sin API keys. Sin servidor. Sin latencia de red. El modelo bajó, se cargó en memoria del cliente, y la inferencia corrió ahí mismo, en el dispositivo del usuario. Y en el momento en que vi la primera respuesta generarse sin que ningún request saliera a la red... pará. Esto cambia todo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemma LLM browser sin API keys: qué es y por qué importa
&lt;/h2&gt;

&lt;p&gt;Antes de entrar al código, contexto rápido para los que no siguieron el post anterior sobre &lt;a href="https://dev.to/blog/llm-pequeno-browser-edge-inferencia-nextjs"&gt;meter un LLM chico en Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Gemma es la familia de modelos open-weights de Google DeepMind. Los modelos chicos — Gemma 2B, Gemma 3 1B — tienen un tamaño razonable para correr en hardware de consumo. Lo nuevo en 2025 es que con WebGPU y las librerías correctas, ese "hardware de consumo" incluye el browser del usuario.&lt;/p&gt;

&lt;p&gt;Las herramientas que hacen posible esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebGPU API&lt;/strong&gt;: acceso directo a la GPU desde el browser, sin plugins&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;@huggingface/transformers.js&lt;/strong&gt;: port de Transformers para el browser, WebAssembly + WebGPU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MediaPipe LLM Inference API&lt;/strong&gt;: el approach de Google, optimizado para Gemma específicamente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yo probé con Transformers.js porque ya tenía experiencia con el ecosistema Hugging Face y porque el modelo de distribución — cargar pesos desde CDN con cache del browser — me pareció el más práctico para un contexto de app real.&lt;/p&gt;

&lt;h2&gt;
  
  
  El experimento: código real, sin magia
&lt;/h2&gt;

&lt;p&gt;Empecé simple. Componente de React, sin server, inferencia en el cliente. Este es el código que realmente corrí:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/GemmaLocal.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// Inferencia completamente en el browser — sin API calls&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Importamos pipeline de transformers.js — corre en el browser&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;TextGenerationPipeline&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;@huggingface/transformers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EstadoCarga&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cargando&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GemmaLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;estado&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEstado&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EstadoCarga&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;progreso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setProgreso&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;respuesta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRespuesta&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipelineRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TextGenerationPipeline&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cargarModelo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="nf"&gt;setEstado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cargando&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Gemma 2B instruct — ~1.5GB en el primer load, cacheado después&lt;/span&gt;
      &lt;span class="c1"&gt;// El modelo se descarga una vez y queda en Cache Storage del browser&lt;/span&gt;
      &lt;span class="nx"&gt;pipelineRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text-generation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Xenova/gemma-2b-it&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// versión cuantizada, más liviana&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Usa WebGPU si está disponible, fallback a WASM&lt;/span&gt;
          &lt;span class="na"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgpu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;progress_callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nf"&gt;setProgreso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nf"&gt;setEstado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listo&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error cargando Gemma:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setEstado&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generarRespuesta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pipelineRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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="nf"&gt;setRespuesta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Template de Gemma instruct — importante para que responda bien&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;start_of_turn&amp;gt;user\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;end_of_turn&amp;gt;\n&amp;lt;start_of_turn&amp;gt;model\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pipelineRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;max_new_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Streaming: cada token se emite apenas se genera&lt;/span&gt;
      &lt;span class="c1"&gt;// La respuesta aparece progresivamente sin esperar al servidor&lt;/span&gt;
      &lt;span class="na"&gt;callback_function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;generated_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;texto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;generated_text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// Extraemos solo la parte del modelo, sin el prompt&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;respuestaPura&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;start_of_turn&amp;gt;model&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;setRespuesta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;respuestaPura&lt;/span&gt;&lt;span class="p"&gt;);&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="nx"&gt;resultado&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-6 max-w-2xl mx-auto&lt;/span&gt;&lt;span class="dl"&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;estado&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
          &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cargarModelo&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-4 py-2 bg-blue-600 text-white rounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;Cargar&lt;/span&gt; &lt;span class="nc"&gt;Gemma &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;primera&lt;/span&gt; &lt;span class="na"&gt;vez&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;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;estado&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cargando&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Descargando&lt;/span&gt; &lt;span class="nx"&gt;modelo&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;progreso&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;%&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Después del primer load esto no aparece — el browser lo cachea */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-sm text-gray-500&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;Solo&lt;/span&gt; &lt;span class="nx"&gt;la&lt;/span&gt; &lt;span class="nx"&gt;primera&lt;/span&gt; &lt;span class="nx"&gt;vez&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;Después&lt;/span&gt; &lt;span class="nx"&gt;va&lt;/span&gt; &lt;span class="nx"&gt;al&lt;/span&gt; &lt;span class="nx"&gt;instante&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;estado&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;space-y-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt;
            &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
            &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w-full p-3 border rounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tu pregunta...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
            &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;
            &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;generarRespuesta&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;px-4 py-2 bg-green-600 text-white rounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nc"&gt;Generar &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sin&lt;/span&gt; &lt;span class="nx"&gt;internet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;respuesta&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-4 bg-gray-50 rounded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;whitespace-pre-wrap&lt;/span&gt;&lt;span class="dl"&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;respuesta&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/demo-local/page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// Página standalone — zero server components necesarios para la inferencia&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GemmaLocal&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;@/components/GemmaLocal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DemoLocalPage&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Gemma&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;inferencia&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Este componente no hace ningún fetch a ningún server nuestro */&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GemmaLocal&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&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;Lo que pasó: primera carga, ~1.5GB de descarga (modelo cuantizado en 4-bit). Lento. Pero después del primer load, el browser lo cachea en Cache Storage. Segunda visita: el modelo está ahí, se carga en segundos.&lt;/p&gt;

&lt;p&gt;Y la inferencia: en una máquina con GPU discreta, entre 5-15 tokens por segundo. En la mía, con una RTX 3060, llegué a 20 tokens/seg. No es GPT-4 Turbo, pero para tasks específicos — clasificación, resumen corto, extracción de datos — funciona.&lt;/p&gt;

&lt;h2&gt;
  
  
  El momento de "pará, esto cambia todo"
&lt;/h2&gt;

&lt;p&gt;Después de que funcionó, apagué el WiFi. Escribí una pregunta. La respuesta llegó igual.&lt;/p&gt;

&lt;p&gt;Yo vengo de &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;33 años viendo cómo el cómputo migra&lt;/a&gt;. El patrón siempre fue el mismo: el poder empieza centralizado, se democratiza hacia el edge, y en algún punto llega al dispositivo. La Amiga hacía en el cliente lo que antes necesitaba un mainframe. El cyber café donde laburé a los 14 tenía más poder de cómputo que instituciones enteras de diez años antes. Cada generación, el cliente se come un pedazo del servidor.&lt;/p&gt;

&lt;p&gt;Lo que acaba de pasar con los LLMs es exactamente ese mismo movimiento, pero en cámara rápida.&lt;/p&gt;

&lt;p&gt;Las implicaciones concretas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sin billing por inferencia.&lt;/strong&gt; Cero costo de API. El usuario trae su propia GPU. Si tu app tiene 100.000 usuarios activos haciendo 50 queries por día, con GPT-4 eso son números que duelen. Con inferencia en el cliente, son literalmente cero dólares de inferencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sin latencia de red.&lt;/strong&gt; El round-trip a un servidor en us-east-1 desde Argentina son 200-300ms antes de que empiece a llegar el primer token. Local: 0ms. Para UX esto es brutal — la diferencia entre "espero que cargue" y "responde al instante".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sin datos que salen del dispositivo.&lt;/strong&gt; Para casos de uso con datos sensibles — documentos legales, notas médicas, código propietario — inferencia local cambia el juego. El dato no viaja a ningún lado.&lt;/p&gt;

&lt;p&gt;Conectando con lo que escribí sobre &lt;a href="https://dev.to/blog/sandboxes-coding-agents-freestyle"&gt;sandboxes para agentes de código&lt;/a&gt;: parte del problema de darle autonomía a un agente es el costo y la latencia de cada LLM call. Si el modelo corre local, la economía del problema cambia completamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores y gotchas que me comí
&lt;/h2&gt;

&lt;p&gt;No todo fue bonito. Los problemas reales:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebGPU no está en todos lados.&lt;/strong&gt; Firefox lo tiene detrás de un flag. Safari lo agregó en versiones recientes. El fallback a WebAssembly funciona, pero es 3-5x más lento. Necesitás feature detection y manejar el degraded experience.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Detectar soporte antes de intentar cargar&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkWebGPU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adapter&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;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gpu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestAdapter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Elegir device según soporte&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkWebGPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webgpu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wasm&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;El primer load es un problema de UX real.&lt;/strong&gt; 1.5GB en la primera visita es mucho. Tuve que agregar una pantalla de "instalación" explícita con progreso claro. Tratarlo como una PWA que se instala, no como una página que carga.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memoria RAM.&lt;/strong&gt; El modelo cuantizado necesita ~1-2GB de RAM. En dispositivos con 4GB totales, esto puede freezar el tab. Necesitás setear expectativas y ofrecer fallback a API cloud para dispositivos que no den el ancho.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El modelo es chico — actúa como tal.&lt;/strong&gt; Gemma 2B no es GPT-4. Para summarization corta, clasificación, y tareas con mucho contexto en el prompt, anda bien. Para razonamiento complejo o generación larga, los resultados son notoriamente peores. Yo calibré mis expectativas después de una hora de pruebas. El truco es diseñar la task para el modelo, no al revés.&lt;/p&gt;

&lt;p&gt;Esto me conectó con algo que aprendí optimizando la app de Next.js que &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;bajé de 3 segundos a 300ms&lt;/a&gt;: la performance no viene de apretar un botón mágico, viene de entender qué está pasando realmente y diseñar en función de eso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context window limitada.&lt;/strong&gt; El modelo cuantizado que usé tiene 2048 tokens de contexto efectivo. Si mandás un documento largo, lo trunca sin avisarte. Tuve que implementar chunking explícito.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Chunking básico para no superar el context window&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_TOKENS_APROX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// margen de seguridad&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CHARS_POR_TOKEN_APROX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_CHARS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_TOKENS_APROX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;CHARS_POR_TOKEN_APROX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;truncarContexto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_CHARS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// Truncamos desde el principio, preservamos el final (suele ser más relevante)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;MAX_CHARS&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;Esto también lo sentí cuando estuve trabajando con &lt;a href="https://dev.to/blog/claude-code-updates-febrero-2025"&gt;Claude Code en febrero&lt;/a&gt; — el context management es el problema que nadie resuelve del todo bien todavía.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Gemma LLM en el browser sin API keys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Qué navegadores soportan WebGPU para correr Gemma?&lt;/strong&gt;&lt;br&gt;
Chrome 113+ y Edge tienen soporte estable. Safari 18+ lo soporta. Firefox lo tiene detrás de &lt;code&gt;dom.webgpu.enabled&lt;/code&gt; en about:config, no está en producción todavía. Para producción real hoy, Chrome/Edge son el target seguro. Siempre implementá fallback a WebAssembly para los demás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuánto pesa el modelo y cómo manejo la primera descarga?&lt;/strong&gt;&lt;br&gt;
Gemma 2B cuantizado en 4-bit pesa ~1.4-1.6GB. La primera descarga es real y tarda — en conexiones lentas puede ser 5-10 minutos. La clave es tratarlo como instalación de PWA: pantalla de progreso explícita, explicación de que es una sola vez, y que después el browser lo cachea en Cache Storage. Visits siguientes: carga en segundos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué tan rápida es la inferencia comparada con una API en la nube?&lt;/strong&gt;&lt;br&gt;
Depende mucho del hardware. En una GPU discreta moderna (RTX 3060+): 15-25 tokens/segundo con WebGPU. En hardware integrado (Apple Silicon M1): 8-15 tokens/seg. En CPU via WASM: 1-3 tokens/seg, notoriamente lento. La API de OpenAI/Anthropic entrega 50-100 tokens/seg con mejor calidad. La ventaja local no es velocidad bruta, es latencia cero de red y costo cero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Funciona offline completamente?&lt;/strong&gt;&lt;br&gt;
Sí, esa es la parte que me cambió el esquema mental. Una vez que el modelo está cacheado, la inferencia corre sin ningún request de red. Lo probé apagando el WiFi. Funciona. Esto abre casos de uso que antes eran imposibles: apps para zonas con conectividad intermitente, herramientas que manejan datos sensibles que no pueden salir del dispositivo, features que funcionan en aviones/subtes/donde sea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tiene sentido para producción o es un experimento?&lt;/strong&gt;&lt;br&gt;
Hoy está en algún punto entre experimento avanzado y producción early-adopter. Los casos donde ya tiene sentido: apps con datos sensibles (legal, médico, notas personales), features nice-to-have donde el fallback es simplemente no tenerlas, usuarios tech-savvy con hardware bueno. Los casos donde todavía no escala: experiencia de usuario masivo en mobile con hardware variado, tareas que requieren el nivel de razonamiento de modelos grandes, apps donde 1.5GB de primera descarga rompe el funnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué pasa con móviles?&lt;/strong&gt;&lt;br&gt;
WebGPU en mobile está en desarrollo pero limitado. Chrome en Android está avanzando, iOS Safari tiene soporte parcial. El problema gordo es RAM — los phones con 4-6GB no tienen margen para cargar 1.5GB de modelo. Gemma 1B (la versión más chica, ~700MB cuantizado) es más viable para mobile. La realidad honesta: mobile-first con inferencia local todavía tiene 1-2 años por delante para ser confiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión: el cómputo siempre migra hacia el edge
&lt;/h2&gt;

&lt;p&gt;Lo que viví con Gemma en el browser es el mismo patrón que vi cuando el cyber café donde laburé empezó a tener más poder que servers de empresas de cinco años antes. El cómputo siempre migra hacia el edge. Siempre.&lt;/p&gt;

&lt;p&gt;No estoy diciendo que las APIs de cloud van a desaparecer. GPT-4, Claude, Gemini Pro — para los casos que necesitan el mayor nivel de capacidad, van a seguir siendo la respuesta. Pero hay toda una categoría de features — clasificación, summarización, extracción, asistencia contextual — donde un modelo chico corriendo en el cliente resuelve el problema igual de bien, sin costo de API, sin latencia de red, sin datos que salen del dispositivo.&lt;/p&gt;

&lt;p&gt;El cambio más grande para mí no fue técnico. Fue conceptual: dejé de pensar en "LLM en mi app" como sinónimo de "API call a un cloud endpoint". Ahora es una decisión arquitectural real: ¿este modelo va en el servidor, en el edge, o en el cliente?&lt;/p&gt;

&lt;p&gt;Y una vez que hacés esa pregunta, no podés dejar de hacerla.&lt;/p&gt;

&lt;p&gt;Si ya leíste el post sobre &lt;a href="https://dev.to/blog/llm-pequeno-browser-edge-inferencia-nextjs"&gt;LLMs chicos en Next.js&lt;/a&gt; y te quedaste con ganas de ir un paso más allá, este es el paso. Bajate Transformers.js, cargá Gemma, apagá el WiFi, y preguntale algo. La primera vez que responde sin que ningún paquete salga a la red, vas a tener el mismo momento que tuve yo.&lt;/p&gt;

&lt;p&gt;Vale la pena.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/gemma-llm-browser-sin-api-keys-local" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>llm</category>
      <category>webgpu</category>
    </item>
    <item>
      <title>Sandboxes para agentes de código: qué es Freestyle y por qué me importa</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Wed, 08 Apr 2026 05:21:29 +0000</pubDate>
      <link>https://forem.com/jtorchia/sandboxes-para-agentes-de-codigo-que-es-freestyle-y-por-que-me-importa-p22</link>
      <guid>https://forem.com/jtorchia/sandboxes-para-agentes-de-codigo-que-es-freestyle-y-por-que-me-importa-p22</guid>
      <description>&lt;p&gt;En 2005, cuando administraba el cyber café a los 14 años, tuve mi primera lección sobre procesos que corren sin supervisión. Un cliente había dejado un script ejecutándose — algo que descargaba archivos, decía — y cuando lo encontré media hora después había consumido todo el ancho de banda del local. Diez máquinas inutilizadas, gente enojada, yo sin saber ni por dónde empezar. Aprendí esa noche que &lt;em&gt;lo que no podés ver ejecutarse, te puede romper todo&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Hoy pienso en eso cada vez que le doy permiso a Claude Code para que haga cambios en un proyecto real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxes para coding agents: el problema que nadie nombra bien
&lt;/h2&gt;

&lt;p&gt;Hay algo que los posts sobre agentes de código evitan decir directamente: &lt;strong&gt;el mayor riesgo no es que escriban código malo&lt;/strong&gt;. El código malo lo revisás, lo revertís, lo arreglás. El riesgo real es la &lt;em&gt;ejecución&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Un agente que escribe código incorrecto es un problema de calidad. Un agente que ejecuta &lt;code&gt;rm -rf&lt;/code&gt; en el directorio equivocado, o hace un &lt;code&gt;npm publish&lt;/code&gt; sin que vos lo pidas, o llama a una API con tus credenciales cacheadas — eso es un problema de seguridad. Y es un problema que muy poca gente está nombrando con la precisión que merece.&lt;/p&gt;

&lt;p&gt;Cuando empecé a integrar agentes de código en mi workflow — hablo de proyectos reales, no de demos — lo primero que hice fue leer los logs de lo que se ejecutaba. No porque desconfíe de Claude específicamente. Porque soy el mismo tipo que &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;tiró un servidor de producción en su primera semana con un rm -rf&lt;/a&gt;. Sé exactamente cuánto daño puede hacer un comando ejecutado en el contexto equivocado.&lt;/p&gt;

&lt;p&gt;Eso me llevó a Freestyle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es Freestyle y qué resuelve concretamente
&lt;/h2&gt;

&lt;p&gt;Freestyle llegó a Hacker News hace poco con 188 puntos — número que para mí es señal de que tocó un nervio real, no hype. La propuesta es directa: &lt;strong&gt;un sandbox de ejecución para agentes de código&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No es un concepto nuevo. Los sandboxes existen desde siempre en seguridad. Lo nuevo es aplicarlo específicamente al problema de los coding agents que necesitan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Correr código arbitrario&lt;/li&gt;
&lt;li&gt;Instalar dependencias&lt;/li&gt;
&lt;li&gt;Ejecutar tests&lt;/li&gt;
&lt;li&gt;Posiblemente hacer requests HTTP&lt;/li&gt;
&lt;li&gt;Todo eso sin tocar tu sistema real&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Freestyle te da un entorno de ejecución aislado donde el agente puede hacer todas esas cosas. Si rompe algo, rompe el sandbox. Tu máquina, tu base de datos, tus credenciales — siguen intactas.&lt;/p&gt;

&lt;p&gt;La arquitectura, en términos simples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lo que pasa SIN sandbox (tu situación actual, probablemente)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="c1"&gt;// El agente ejecuta directamente en tu proceso Node&lt;/span&gt;
  &lt;span class="c1"&gt;// Tiene acceso a process.env (tus secrets!)&lt;/span&gt;
  &lt;span class="c1"&gt;// Tiene acceso al filesystem real&lt;/span&gt;
  &lt;span class="c1"&gt;// Un npm install modifica tu node_modules real&lt;/span&gt;
  &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// oversimplificado, pero conceptualmente esto&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que propone Freestyle&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agentRunSandboxed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="c1"&gt;// Cada ejecución corre en un ambiente aislado&lt;/span&gt;
  &lt;span class="c1"&gt;// Filesystem efímero — muere con el sandbox&lt;/span&gt;
  &lt;span class="c1"&gt;// Variables de entorno controladas explícitamente&lt;/span&gt;
  &lt;span class="c1"&gt;// Network access configurable (podés bloquearlo)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sandbox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Freestyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSandbox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Solo los secrets que querés exponer, nada más&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SANDBOX_DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Podés allowlist dominios específicos&lt;/span&gt;
      &lt;span class="na"&gt;allowedHosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api.openai.com&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="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sandbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&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;Eso, en términos prácticos, es enorme.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo mapea contra mi workflow actual
&lt;/h2&gt;

&lt;p&gt;Mi stack hoy es Next.js, TypeScript, PostgreSQL en Railway, y Claude Code como asistente principal. Si querés el detalle completo, está en &lt;a href="https://dev.to/blog/como-construi-juanchi-dev"&gt;cómo construí juanchi.dev&lt;/a&gt; y en el post sobre &lt;a href="https://dev.to/blog/stack-tecnologico-perfecto-2025"&gt;el stack que elegiría en 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cuando uso Claude Code en modo interactivo — el que sugiere cambios y los aplica — hay una tensión constante. Le doy suficiente contexto para que sea útil, lo que incluye acceso al proyecto. Pero ese acceso, por definición, incluye cosas que no quiero que toque automáticamente.&lt;/p&gt;

&lt;p&gt;Mi solución actual es básicamente manual: reviso cada cambio antes de confirmar, tengo git en cada paso, y nunca corro sugerencias directamente en el proyecto conectado a la base de datos de producción. Funciona. Pero es fricción.&lt;/p&gt;

&lt;p&gt;Un sandbox como Freestyle cambia esa ecuación. En lugar de &lt;em&gt;yo supervisando cada micro-acción&lt;/em&gt;, el sandbox define los límites estructuralmente. El agente puede correr lo que quiera dentro del sandbox. Afuera del sandbox, no existe.&lt;/p&gt;

&lt;p&gt;Para alguien que está &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;optimizando performance en producción&lt;/a&gt; con agentes ayudando a generar benchmarks y tests — esto es la diferencia entre "dejo que el agente pruebe" y "tengo miedo de que el agente pruebe".&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores comunes cuando integrás agentes sin sandbox
&lt;/h2&gt;

&lt;p&gt;Voy a ser específico porque lo viví.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 1: Credentials en el contexto del agente&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si tu agente corre en el mismo proceso que tu app, tiene acceso a &lt;code&gt;process.env&lt;/code&gt;. Todo. DATABASE_URL, API keys, tokens. Si el agente hace un request HTTP — por cualquier razón — puede estar exfiltrando esas credenciales. No porque sea malicioso. Porque el contexto no está delimitado.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Esto parece inofensivo pero no lo es&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agente&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;ClaudeAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// Tiene acceso a todo el proyecto&lt;/span&gt;
  &lt;span class="c1"&gt;// process.env está disponible implícitamente&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Explicitá qué tiene y qué no tiene&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;agente&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;ClaudeAgent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp/sandbox-workspace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Directorio aislado&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Solo lo que necesita para la tarea específica&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;&lt;strong&gt;Error 2: npm install sin control&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Un agente que puede instalar paquetes puede instalar cualquier cosa. Hay paquetes npm con código malicioso que se ejecuta en el install. Si el agente corre en tu máquina, ese código también corre en tu máquina.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 3: Confiar en que el agente "va a pedir permiso"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Algunos agentes tienen mecanismos de confirmación. Bien. Pero eso es UI, no seguridad. La seguridad tiene que estar en el aislamiento del sistema, no en la buena voluntad del modelo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 4: Mezclar el database de desarrollo con el de producción&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esto aplica siempre, pero con agentes se vuelve crítico. Si el agente tiene acceso a tu connection string de producción — aunque sea "para leer" — estás un paso de un error costoso. Mis &lt;a href="https://dev.to/blog/typescript-patrones-avanzados-que-uso"&gt;patrones de TypeScript&lt;/a&gt; incluyen helpers específicos para separar estos contextos, pero un sandbox lo resuelve a nivel infraestructura.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que me genera ruido de Freestyle (la parte crítica)
&lt;/h2&gt;

&lt;p&gt;Dicho todo lo anterior — y lo digo genuinamente porque creo en el problema que resuelve — hay cosas que me generan preguntas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El cold start es real.&lt;/strong&gt; Crear un sandbox por ejecución tiene latencia. Para flujos interactivos donde el agente hace muchas iteraciones pequeñas, esa latencia acumula. Freestyle menciona optimizaciones de startup, pero todavía no lo medí en un workflow real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El pricing en producción está por verse.&lt;/strong&gt; Sandboxes como servicio tiene costos de infraestructura no triviales. Si tu agente hace 50 ejecuciones por tarea, ese modelo de pricing escala diferente que una ejecución local.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La integración con Claude Code específicamente no está documentada claramente.&lt;/strong&gt; O al menos no la encontré cuando lo exploré. Para mi workflow principal, necesito saber cómo conecta esto con el agente que ya estoy usando, no con un agente nuevo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No resuelve el problema de output.&lt;/strong&gt; El sandbox aísla la &lt;em&gt;ejecución&lt;/em&gt;, pero el código que el agente &lt;em&gt;produce&lt;/em&gt; igual termina en tu codebase. El sandbox te salva del daño durante el proceso; el code review te salva del daño en el resultado. Los dos son necesarios.&lt;/p&gt;

&lt;p&gt;Lo que haría diferente: antes de adoptar Freestyle como dependencia principal, construiría primero con Docker un sandbox básico propio — algo que ya cubrí en el post de &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;Docker para Node.js&lt;/a&gt; — para entender los trade-offs antes de delegar esa responsabilidad a un servicio externo.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Sandboxes para coding agents
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es un sandbox para coding agents exactamente?&lt;/strong&gt;&lt;br&gt;
Es un entorno de ejecución aislado donde un agente de código puede correr comandos, instalar dependencias y ejecutar scripts sin acceder al sistema host. Piensalo como una VM efímera: todo lo que pasa adentro, muere adentro. Tu filesystem real, tus variables de entorno y tu base de datos no son accesibles a menos que vos explícitamente lo permitas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué no alcanza con correr el agente en Docker localmente?&lt;/strong&gt;&lt;br&gt;
Docker es una opción válida y es lo que muchos hacemos hoy. El valor de Freestyle específicamente es que abstrae la infraestructura del sandbox y la expone como API, con lifecycle management, networking configurable y soporte para múltiples runtimes. Docker local funciona, pero tiene fricción de setup y no tiene las mismas garantías de aislamiento de red out-of-the-box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Freestyle es open source o es un servicio?&lt;/strong&gt;&lt;br&gt;
Es un servicio con SDK. El SDK es open source, la infraestructura que corre los sandboxes es managed. Modelo similar a Vercel con Next.js: podés correrlo vos mismo, pero el servicio managed es el punto de entrada natural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Funciona con cualquier agente de código o solo con algunos específicos?&lt;/strong&gt;&lt;br&gt;
Conceptualmente funciona con cualquier agente que necesite ejecutar código — Claude Code, GPT Engineer, Devin, o tu propio agente custom. La integración concreta depende de cómo tu agente maneja la ejecución. Si el agente llama a un subprocess o a una API de ejecución, podés redirigir esas llamadas a Freestyle. Si el agente tiene un modelo de ejecución muy acoplado, la integración es más compleja.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Un sandbox resuelve todos los problemas de seguridad de los coding agents?&lt;/strong&gt;&lt;br&gt;
No. El sandbox resuelve el problema de &lt;em&gt;ejecución no supervisada&lt;/em&gt;. No resuelve: código malicioso que el agente produce y vos deployás, prompt injection si el agente procesa input externo, o el problema de qué hacés con el output del sandbox una vez que lo tenés. Es una capa de seguridad, no la seguridad completa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena para proyectos personales o solo para equipos?&lt;/strong&gt;&lt;br&gt;
Depende de cuánto usás agentes de código. Si usás Claude Code o similar ocasionalmente para sugerencias de código que vos aplicás manualmente, probablemente no necesitás un sandbox formal. Si tenés agentes corriendo de forma autónoma — haciendo commits, corriendo tests, instalando deps — un sandbox deja de ser nice-to-have y se vuelve necesario. Para mi uso actual está en el límite. Cuando pase a workflows más autónomos, voy directo al sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión: el miedo correcto
&lt;/h2&gt;

&lt;p&gt;El cyber café me enseñó que el problema no es lo que ves correr — es lo que corre sin que lo estés mirando.&lt;/p&gt;

&lt;p&gt;Los coding agents son increíblemente útiles. Los uso todos los días y no volvería atrás. Pero hay una diferencia entre usarlos como autocomplete inteligente y usarlos como agentes autónomos con acceso a tu entorno real. Esa diferencia importa.&lt;/p&gt;

&lt;p&gt;Freestyle está apuntando al problema correcto. El sandbox no es paranoia — es el mismo principio que me llevó a tener bases de datos separadas por ambiente, a usar secrets managers en lugar de .env en producción, y a nunca correr código de terceros con más permisos de los necesarios. Principios que cualquiera que haya trabajado en infraestructura real &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;entiende visceralmente&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Lo que haría hoy: si ya estás usando agentes en proyectos reales, revisá qué acceso tienen a tu entorno. Si tienen acceso a process.env completo, a tu database real, o corren en el mismo proceso que tu app — eso es técnicamente un sandbox cero. Empezá por ahí, con Docker o con Freestyle, antes de que el problema sea más que teórico.&lt;/p&gt;

&lt;p&gt;El App Router de Next.js me enseñó que a veces te enojás dos semanas con la abstracción correcta. No quiero repetir ese error con los sandboxes para agentes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/sandboxes-coding-agents-freestyle" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>claudecode</category>
      <category>codingagents</category>
      <category>freestyle</category>
    </item>
    <item>
      <title>Vibe-coding vs stress-coding: cómo trabajo yo realmente con IA en proyectos que importan</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 23:31:50 +0000</pubDate>
      <link>https://forem.com/jtorchia/vibe-coding-vs-stress-coding-como-trabajo-yo-realmente-con-ia-en-proyectos-que-importan-15k6</link>
      <guid>https://forem.com/jtorchia/vibe-coding-vs-stress-coding-como-trabajo-yo-realmente-con-ia-en-proyectos-que-importan-15k6</guid>
      <description>&lt;p&gt;El 87% de los bugs que encontré en código generado por IA aparecieron en edge cases que el prompt no mencionaba. No en la funcionalidad principal. En los bordes.&lt;/p&gt;

&lt;p&gt;Cuando vi ese número en mi propio historial de PRs, tuve que releer dos veces. Porque llevaba semanas hablando de cuánto me ayudaba Cursor y Claude, y resulta que el 87% de mis correcciones eran exactamente en los lugares donde el contexto del negocio importa más que la sintaxis.&lt;/p&gt;

&lt;p&gt;Eso me hizo pensar diferente sobre el vibe-coding. Y hoy lo quiero decir directo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe coding productividad real con IA — la diferencia que nadie te explica
&lt;/h2&gt;

&lt;p&gt;Existe una corriente (legítima, interesante) que dice que el futuro del desarrollo es el vibe-coding: describís lo que querés, la IA lo genera, vos lo guiás con prompts, y el código aparece casi solo. Vi el artículo que circuló en Dev.to hace poco sobre "stress-coding" — la contracara ansiosa — y aunque el post en sí era bastante básico, el concepto me cerró algo que venía dando vueltas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El vibe-coding no es malo. Es contextual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yo vibe-codeo. Todos los días. Pero no en todos los proyectos de la misma manera. La distinción que me tomó tiempo articular es esta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cuando experimento&lt;/strong&gt;: la IA es copiloto con las riendas sueltas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando hay usuarios reales&lt;/strong&gt;: la IA es una herramienta poderosa que yo audito con criterio propio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parece obvio. No lo es cuando estás en el flow y todo parece funcionar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo uso IA en modo experimento (y por qué está bien)
&lt;/h2&gt;

&lt;p&gt;Cuando construí &lt;a href="https://dev.to/blog/como-construi-juanchi-dev"&gt;juanchi.dev&lt;/a&gt;, el proceso fue casi puro vibe-coding en las primeras semanas. Next.js 16, React 19, Tailwind v4 — todo bleeding edge, todo con poca documentación, todo con IA como primera línea de consulta.&lt;/p&gt;

&lt;p&gt;En ese contexto, el flujo era:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Prompt típico en modo experimento:&lt;/span&gt;
&lt;span class="c1"&gt;// "Necesito un componente que anime la entrada de cards&lt;/span&gt;
&lt;span class="c1"&gt;// usando Framer Motion con el nuevo hook useAnimate"&lt;/span&gt;

&lt;span class="c1"&gt;// La IA genera esto, yo lo acepto y pruebo:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAnimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stagger&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;framer-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AnimatedGrid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PostCard&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAnimate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// IA sugirió este approach — lo probé, funcionó, seguí&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&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="nf"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.card&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;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&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;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;stagger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid gap-6&lt;/span&gt;&lt;span class="dl"&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;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;card&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostCard&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;))}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;En modo experimento, si esto falla en un edge case, el costo es: yo me doy cuenta, lo corrijo, sigo. No hay usuario esperando. No hay SLA. El vibe-coding acá &lt;strong&gt;multiplica mi velocidad de exploración&lt;/strong&gt; de manera real.&lt;/p&gt;

&lt;p&gt;El problema empieza cuando ese modo mental no cambia al pasar a producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo uso IA cuando hay producción de por medio (stress-coding, pero el bueno)
&lt;/h2&gt;

&lt;p&gt;Tengo un proyecto cliente — e-commerce, tráfico real, órdenes reales. Cuando trabajé la &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;optimización de performance&lt;/a&gt; de esa app, el flujo con IA fue completamente diferente.&lt;/p&gt;

&lt;p&gt;Lo que cambió:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. El prompt incluye el contexto de negocio, siempre&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Prompt en modo producción:&lt;/span&gt;
&lt;span class="c1"&gt;// "Tengo una query que trae órdenes de los últimos 30 días&lt;/span&gt;
&lt;span class="c1"&gt;// con JOIN a usuarios y productos. Se ejecuta cada vez que&lt;/span&gt;
&lt;span class="c1"&gt;// alguien entra al dashboard admin. Promedio 2.3 segundos.&lt;/span&gt;
&lt;span class="c1"&gt;// La tabla órdenes tiene 180k filas. ¿Cómo la optimizo?&lt;/span&gt;
&lt;span class="c1"&gt;// NO quiero soluciones que rompan la paginación existente."&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que la IA genera, yo NO acepto sin revisar:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getRecentOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="c1"&gt;// IA sugirió este índice compuesto — lo evalué en staging primero&lt;/span&gt;
  &lt;span class="c1"&gt;// CREATE INDEX idx_orders_created_user &lt;/span&gt;
  &lt;span class="c1"&gt;// ON orders(created_at DESC, user_id) &lt;/span&gt;
  &lt;span class="c1"&gt;// WHERE created_at &amp;gt; NOW() - INTERVAL '30 days';&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Solo los campos que realmente necesito — la IA quería traer todo&lt;/span&gt;
      &lt;span class="na"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Removí el JOIN a products porque no se mostraba en este view&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;innerJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`NOW() - INTERVAL '30 days'`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// IA no sabía que solo quería completed&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;limit&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;La IA no sabía que solo quería órdenes &lt;code&gt;completed&lt;/code&gt;. Ese filtro cambió la query de 2.3 segundos a 400ms sin índice nuevo. El contexto de negocio que yo aporté valió más que el código que ella generó.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Nada va a producción sin que yo entienda cada línea&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Esto suena básico y lo es. Pero en modo vibe-coding es fácil hacer &lt;code&gt;Accept All&lt;/code&gt; y seguir. En producción, si no podés explicar qué hace una función en 30 segundos, no la deployás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Los edge cases los pienso yo, no los delego&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Volviendo al 87% del principio: los edge cases son exactamente el lugar donde el contexto de negocio importa. ¿Qué pasa si el usuario cancela la orden justo cuando se está procesando el pago? ¿Qué pasa si el stock llega a cero entre el &lt;code&gt;addToCart&lt;/code&gt; y el &lt;code&gt;checkout&lt;/code&gt;? Eso no está en el prompt. Nunca va a estar en el prompt a menos que vos lo pongas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores que cometí mezclando los dos modos
&lt;/h2&gt;

&lt;p&gt;El problema real no es el vibe-coding ni el stress-coding. El problema es &lt;strong&gt;no saber en cuál modo estás&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En mi &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;viaje de 30 años con tecnología&lt;/a&gt;, tiré un servidor de producción con &lt;code&gt;rm -rf&lt;/code&gt; en mi primera semana de sysadmin. Eso fue stress-coding sin criterio: urgencia, presión, ejecutar sin pensar. El equivalente moderno es vibe-coding en producción: velocidad, flow, &lt;code&gt;Accept All&lt;/code&gt; sin auditar.&lt;/p&gt;

&lt;p&gt;Dos errores concretos que cometí:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 1: Confiar en los tipos de TypeScript generados por IA sin validar en runtime&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// IA generó esto, yo lo acepté:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OrderResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Problema: la API real a veces devuelve total como string&lt;/span&gt;
&lt;span class="c1"&gt;// TypeScript no lo detecta en runtime, Zod sí:&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que debería haber hecho desde el principio:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OrderResponseSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// coerce maneja string -&amp;gt; number&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OrderItemSchema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Ahora si la API rompe el contrato, yo me entero en runtime&lt;/span&gt;
&lt;span class="c1"&gt;// y no cuando el usuario ve "NaN" en su total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este error me costó 2 horas de debugging en producción. El &lt;a href="https://dev.to/blog/stack-tecnologico-perfecto-2025"&gt;stack tecnológico que elijo hoy&lt;/a&gt; incluye Zod obligatorio por esta razón.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: Dejar que la IA decida la arquitectura de features nuevas&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;La IA es excelente implementando. Es mediocre diseñando. Cuando le preguntás "¿cómo estructuro el módulo de notificaciones?", te va a dar una respuesta técnicamente correcta y genéricamente inútil para tu contexto.&lt;/p&gt;

&lt;p&gt;Los &lt;a href="https://dev.to/blog/typescript-patrones-avanzados-que-uso"&gt;patrones de TypeScript que realmente uso&lt;/a&gt; surgieron de decisiones de diseño que tomé yo, no la IA. Ella implementa los patrones. Yo decido cuándo y por qué aplicarlos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que cambió en mi flujo de trabajo concreto
&lt;/h2&gt;

&lt;p&gt;Hoy tengo una regla interna simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Si el bug en producción me va a despertar a las 2am, no vibe-codeo esa parte.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Más específico:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Autenticación y autorización: cada línea auditada&lt;/li&gt;
&lt;li&gt;Manejo de pagos: zero vibe-coding&lt;/li&gt;
&lt;li&gt;Queries a base de datos en producción: genero con IA, reviso el plan de ejecución&lt;/li&gt;
&lt;li&gt;Manejo de errores y edge cases: los pienso yo, la IA los implementa&lt;/li&gt;
&lt;li&gt;UI components sin estado crítico: vibe-coding libre&lt;/li&gt;
&lt;li&gt;Animaciones, estilos, layout: vibe-coding con los ojos cerrados&lt;/li&gt;
&lt;li&gt;Scripts de migración de datos: auditados línea por línea, siempre&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El resultado es que uso IA el 80% de mi tiempo de coding, pero de manera diferenciada. No es menos IA — es IA con criterio.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Vibe-coding y productividad real con IA
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿El vibe-coding es solo para proyectos personales o sirve en trabajo cliente?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sirve en trabajo cliente, pero en capas específicas. Para exploración inicial, prototipos, componentes de UI sin lógica crítica — perfecto. Para la lógica de negocio core, las integraciones de pago, el manejo de datos sensibles — necesitás un modo más riguroso. La clave es saber cuál es cuál antes de empezar a tipear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo evitás aceptar código de IA que parece funcionar pero tiene bugs escondidos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Dos prácticas concretas: primero, siempre correr los tests antes del commit (tenés tests, ¿no?). Segundo, si es código que interactúa con external APIs o base de datos, lo probás con datos edge case reales: strings vacíos, IDs que no existen, respuestas con campos faltantes. La IA genera el camino feliz de maravilla. Los bordes los tenés que probar vos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué herramientas de IA usás actualmente?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cursor como editor principal con Claude Sonnet para el día a día. Claude Opus cuando necesito pensar arquitectura o debuggear algo complejo que no entiendo. ChatGPT casi no lo uso para código. GitHub Copilot lo dejé — Cursor lo supera en contexto de proyecto completo. El contexto es todo en esta ecuación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El vibe-coding no te hace perder profundidad técnica con el tiempo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Es la pregunta que más me hago. Mi respuesta honesta: sí, si no tenés cuidado. La manera de contrarrestarlo es elegir deliberadamente entender el código difícil, no solo aceptarlo. Cuando la IA genera algo que no entiendo completamente, le pido que me explique. No por paranoia — para mantener el músculo activo. Los 30 años de background técnico que describí no los quiero atrofiar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Hay un tipo de proyecto donde NO usarías IA en el loop?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestamente, no. Pero hay partes de proyectos donde la IA está en el loop de manera diferente. En código crítico de seguridad, la uso para revisar lo que yo escribo, no para generar. "Acá está mi implementación de rate limiting, ¿qué ataques no estoy cubriendo?" Eso es IA como auditor, no como generador. Es un modo más, no ausencia de IA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo sabés cuándo el código generado por IA está bien y cuándo hay que reescribirlo?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Señales de reescritura: no podés explicar qué hace en 30 segundos, tiene más de 3 niveles de anidamiento sin razón obvia, los nombres de variables son genéricos (&lt;code&gt;data&lt;/code&gt;, &lt;code&gt;result&lt;/code&gt;, &lt;code&gt;temp&lt;/code&gt;), o no tiene manejo de errores. Señales de que está bien: lo leerías orgulloso en un code review, los edge cases están contemplados, y si algo falla, el error va a ser claro sobre qué falló y por qué.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que haría diferente si empezara hoy
&lt;/h2&gt;

&lt;p&gt;El vibe-coding es real, es productivo, y vino para quedarse. Pero la narrativa de "describís y la IA hace" tiene un problema: omite que el valor del desarrollador senior no está en escribir código. Está en saber qué código escribir, cuándo, y con qué trade-offs.&lt;/p&gt;

&lt;p&gt;Empecé a usar IA en serio durante la pandemia, cuando hice el pivot a desarrollo de software. Los primeros meses fueron devastadores — todo lo que sabía de infra no valía en React. Tuve que aprender a pensar diferente. Y hoy, con IA, pasa algo parecido: el que no aprende cuándo confiar y cuándo auditar va a tener el mismo problema que el dev que copiaba Stack Overflow sin entender. Los bugs van a aparecer en el peor momento, en los bordes, exactamente donde el negocio duele más.&lt;/p&gt;

&lt;p&gt;La IA multiplicó mi productividad real. Pero la productividad real incluye no despertar a las 2am por algo que "parecía funcionar".&lt;/p&gt;

&lt;p&gt;¿Vos cómo dividís el uso de IA entre proyectos que importan y experimentación? Me interesa saber si tu criterio es diferente al mío.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/vibe-coding-vs-stress-coding-ia-proyectos-reales" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>nextjs</category>
      <category>productividad</category>
      <category>ia</category>
    </item>
    <item>
      <title>Cómo Linux ejecuta un binario: lo entendí a los 33 años de programar y me da vergüenza</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 23:31:49 +0000</pubDate>
      <link>https://forem.com/jtorchia/como-linux-ejecuta-un-binario-lo-entendi-a-los-33-anos-de-programar-y-me-da-verguenza-2g2o</link>
      <guid>https://forem.com/jtorchia/como-linux-ejecuta-un-binario-lo-entendi-a-los-33-anos-de-programar-y-me-da-verguenza-2g2o</guid>
      <description>&lt;p&gt;Hay exactamente &lt;strong&gt;127 syscalls&lt;/strong&gt; que hace un proceso Node.js vacío antes de ejecutar una sola línea de tu código. Ciento veintisiete. Cuando lo medí con &lt;code&gt;strace&lt;/code&gt; la semana pasada, tuve que releer el output dos veces y después cerrar la terminal y salir a caminar.&lt;/p&gt;

&lt;p&gt;Tengo 33 años de historia con computadoras. &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;Arranqué con una Amiga a los 3 años, pasé por DOS, monté servidores Linux a los 18, y hoy deployeo en Railway con Next.js&lt;/a&gt;. Y en todo ese tiempo nunca entendí — de verdad, en detalle — qué pasa entre que escribís &lt;code&gt;./mi-programa&lt;/code&gt; y el programa corre. Lo esquivé. Siempre había algo más urgente. Un deploy. Un bug en producción. Un cliente.&lt;/p&gt;

&lt;p&gt;Esta semana me obligué a bajar al metal. Y esto es lo que encontré.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux ELF dynamic linking: cómo funciona realmente
&lt;/h2&gt;

&lt;p&gt;Empecemos por el principio. Cuando ejecutás un binario en Linux, el kernel no simplemente "arranca" tu programa. Hay una cadena de eventos que la mayoría de los devs de producto nunca vemos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Miremos qué tipo de archivo es un binario cualquiera&lt;/span&gt;
file /usr/bin/node
&lt;span class="c"&gt;# ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),&lt;/span&gt;
&lt;span class="c"&gt;# dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahí está. &lt;code&gt;dynamically linked&lt;/code&gt;. &lt;code&gt;interpreter /lib64/ld-linux-x86-64.so.2&lt;/code&gt;. Eso es el dynamic linker, y es el protagonista de esta historia.&lt;/p&gt;

&lt;h3&gt;
  
  
  El formato ELF: el sobre que envuelve todo
&lt;/h3&gt;

&lt;p&gt;ELF significa &lt;strong&gt;Executable and Linkable Format&lt;/strong&gt;. Es básicamente un formato de archivo — como un ZIP pero para código ejecutable. Todo binario de Linux es un archivo ELF, y tiene una estructura muy específica:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# readelf te muestra las entrañas de un ELF&lt;/span&gt;
readelf &lt;span class="nt"&gt;-h&lt;/span&gt; /usr/bin/ls

&lt;span class="c"&gt;# ELF Header:&lt;/span&gt;
&lt;span class="c"&gt;#   Magic:   7f 45 4c 46 02 01 01 00 ...  &amp;lt;- "\x7fELF" — la firma del formato&lt;/span&gt;
&lt;span class="c"&gt;#   Class:                             ELF64&lt;/span&gt;
&lt;span class="c"&gt;#   Entry point address:               0x67d0  &amp;lt;- acá empieza TU código&lt;/span&gt;
&lt;span class="c"&gt;#   Start of program headers:          64 (bytes into file)&lt;/span&gt;
&lt;span class="c"&gt;#   Number of program headers:         13&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;Entry point&lt;/code&gt; es la dirección de memoria donde va a arrancar la ejecución. Pero — y acá está lo que me voló la cabeza — &lt;strong&gt;ese código no es el primero que corre&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  El dynamic linker: el intermediario que nunca viste
&lt;/h3&gt;

&lt;p&gt;Cuando el kernel ve que un ELF es "dynamically linked", no ejecuta el entry point directamente. Ejecuta primero el &lt;strong&gt;interpreter&lt;/strong&gt; — que en la práctica es &lt;code&gt;/lib64/ld-linux-x86-64.so.2&lt;/code&gt;, el dynamic linker.&lt;/p&gt;

&lt;p&gt;Este proceso hace, en orden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Veamos qué bibliotecas necesita un binario&lt;/span&gt;
ldd /usr/bin/node

&lt;span class="c"&gt;# linux-vdso.so.1 (0x00007ffd8c9f3000)      &amp;lt;- virtual, vive en el kernel&lt;/span&gt;
&lt;span class="c"&gt;# libdl.so.2 =&amp;gt; /lib/x86_64-linux-gnu/libdl.so.2&lt;/span&gt;
&lt;span class="c"&gt;# libstdc++.so.6 =&amp;gt; /lib/x86_64-linux-gnu/libstdc++.so.6&lt;/span&gt;
&lt;span class="c"&gt;# libm.so.6 =&amp;gt; /lib/x86_64-linux-gnu/libm.so.6&lt;/span&gt;
&lt;span class="c"&gt;# libc.so.6 =&amp;gt; /lib/x86_64-linux-gnu/libc.so.6&lt;/span&gt;
&lt;span class="c"&gt;# /lib64/ld-linux-x86-64.so.2 (0x00007f3a...)  &amp;lt;- el dynamic linker mismo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Carga el ELF en memoria&lt;/strong&gt; — mapea los segmentos del archivo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resuelve las dependencias&lt;/strong&gt; — busca cada &lt;code&gt;.so&lt;/code&gt; que necesita el binario&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hace la relocación&lt;/strong&gt; — parchea las direcciones de memoria para que todo encaje&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ejecuta los constructores&lt;/strong&gt; — código de inicialización antes del &lt;code&gt;main()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entrega el control&lt;/strong&gt; al entry point real&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Todo eso antes de que tu &lt;code&gt;main()&lt;/code&gt; corra una sola línea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bajando más: qué pasa con strace
&lt;/h2&gt;

&lt;p&gt;La herramienta que me abrió los ojos fue &lt;code&gt;strace&lt;/code&gt;. Intercepta todas las syscalls de un proceso:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Contemos las syscalls de un programa C mínimo&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hola.c &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#include &amp;lt;stdio.h&amp;gt;
int main() {
    printf("hola&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;");
    return 0;
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;gcc &lt;span class="nt"&gt;-o&lt;/span&gt; hola hola.c
strace &lt;span class="nt"&gt;-c&lt;/span&gt; ./hola

&lt;span class="c"&gt;# % time     seconds  usecs/call     calls    syscall&lt;/span&gt;
&lt;span class="c"&gt;# 27.45    0.000156          31         5    mmap       &amp;lt;- mapear memoria&lt;/span&gt;
&lt;span class="c"&gt;# 18.23    0.000104          20         5    mprotect   &amp;lt;- proteger regiones&lt;/span&gt;
&lt;span class="c"&gt;# 14.67    0.000083          83         1    munmap&lt;/span&gt;
&lt;span class="c"&gt;#  9.44    0.000054          27         2    openat     &amp;lt;- abrir archivos .so&lt;/span&gt;
&lt;span class="c"&gt;#  8.92    0.000051          25         2    read&lt;/span&gt;
&lt;span class="c"&gt;# ...&lt;/span&gt;
&lt;span class="c"&gt;# Total calls antes de main(): ~25&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Veinticinco syscalls para "hola mundo". Para Node.js son 127. Esto tiene sentido cuando entendés que Node linkea contra un montón de bibliotecas compartidas — V8, libuv, OpenSSL.&lt;/p&gt;

&lt;h3&gt;
  
  
  El section header: el índice del binario
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Miremos las secciones de un ELF&lt;/span&gt;
readelf &lt;span class="nt"&gt;-S&lt;/span&gt; /usr/bin/ls | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;

&lt;span class="c"&gt;# [Nr] Name              Type             Address&lt;/span&gt;
&lt;span class="c"&gt;# [ 0]                   NULL&lt;/span&gt;
&lt;span class="c"&gt;# [ 1] .interp           PROGBITS         &amp;lt;- path al dynamic linker&lt;/span&gt;
&lt;span class="c"&gt;# [ 2] .note.gnu.build-i NOTE&lt;/span&gt;
&lt;span class="c"&gt;# [ 3] .gnu.hash         GNU_HASH         &amp;lt;- tabla hash para búsqueda de símbolos&lt;/span&gt;
&lt;span class="c"&gt;# [ 4] .dynsym           DYNSYM           &amp;lt;- tabla de símbolos dinámicos&lt;/span&gt;
&lt;span class="c"&gt;# [ 5] .dynstr           STRSYM           &amp;lt;- strings de los nombres de funciones&lt;/span&gt;
&lt;span class="c"&gt;# [12] .plt              PROGBITS         &amp;lt;- Procedure Linkage Table&lt;/span&gt;
&lt;span class="c"&gt;# [13] .text             PROGBITS         &amp;lt;- TU CÓDIGO acá&lt;/span&gt;
&lt;span class="c"&gt;# [24] .got              PROGBITS         &amp;lt;- Global Offset Table&lt;/span&gt;
&lt;span class="c"&gt;# [25] .got.plt          PROGBITS         &amp;lt;- GOT para PLT&lt;/span&gt;
&lt;span class="c"&gt;# [26] .data             PROGBITS         &amp;lt;- variables globales inicializadas&lt;/span&gt;
&lt;span class="c"&gt;# [27] .bss              NOBITS           &amp;lt;- variables globales sin inicializar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PLT y GOT: el truco de magia del lazy binding
&lt;/h3&gt;

&lt;p&gt;Acá está la parte más elegante del sistema. Cuando tu programa llama a &lt;code&gt;printf()&lt;/code&gt;, no sabe en tiempo de compilación en qué dirección de memoria va a estar esa función. La biblioteca puede estar en cualquier lugar.&lt;/p&gt;

&lt;p&gt;La solución son dos estructuras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PLT (Procedure Linkage Table)&lt;/strong&gt;: código intermedio que salta a través del GOT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GOT (Global Offset Table)&lt;/strong&gt;: tabla de punteros a las direcciones reales
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Primera llamada a printf — lazy binding en acción&lt;/span&gt;
&lt;span class="c"&gt;# 1. Salta a printf@PLT&lt;/span&gt;
&lt;span class="c"&gt;# 2. PLT lee el GOT — todavía apunta al dynamic linker&lt;/span&gt;
&lt;span class="c"&gt;# 3. Dynamic linker resuelve la dirección real de printf&lt;/span&gt;
&lt;span class="c"&gt;# 4. Actualiza el GOT con la dirección real&lt;/span&gt;
&lt;span class="c"&gt;# 5. Ejecuta printf&lt;/span&gt;

&lt;span class="c"&gt;# Segunda llamada a printf — ya resuelto&lt;/span&gt;
&lt;span class="c"&gt;# 1. Salta a printf@PLT  &lt;/span&gt;
&lt;span class="c"&gt;# 2. PLT lee el GOT — ahora apunta directo a printf&lt;/span&gt;
&lt;span class="c"&gt;# 3. Ejecuta printf (sin pasar por el dynamic linker)&lt;/span&gt;

&lt;span class="c"&gt;# Podés ver esto con:&lt;/span&gt;
&lt;span class="nv"&gt;LD_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bindings ./hola 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="c"&gt;# binding file ./hola [0] to /lib/x86_64-linux-gnu/libc.so.6 [0]: &lt;/span&gt;
&lt;span class="c"&gt;# normal symbol `printf' [GLIBC_2.2.5]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso es &lt;strong&gt;lazy binding&lt;/strong&gt; — el dynamic linker solo resuelve una función la primera vez que la llamás. Elegante y eficiente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores que me hicieron entender esto a las piñas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Error 1: "No such file or directory" en un binario que existe
&lt;/h3&gt;

&lt;p&gt;Este me pasó hace años y lo "arreglé" sin entenderlo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./mi-binario
&lt;span class="c"&gt;# bash: ./mi-binario: No such file or directory&lt;/span&gt;

&lt;span class="c"&gt;# Pero el archivo existe:&lt;/span&gt;
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; mi-binario
&lt;span class="c"&gt;# -rwxr-xr-x 1 juan juan 45231 Feb 20 14:32 mi-binario&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El error no es que el binario no existe. Es que &lt;strong&gt;el interpreter no existe&lt;/strong&gt;. El dynamic linker especificado en el ELF no está en el sistema. Pasaba cuando copiaba binarios entre distros con diferentes layouts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Diagnóstico:&lt;/span&gt;
readelf &lt;span class="nt"&gt;-l&lt;/span&gt; mi-binario | &lt;span class="nb"&gt;grep &lt;/span&gt;interpreter
&lt;span class="c"&gt;# [Requesting program interpreter: /lib/ld-musl-x86_64.so.1]&lt;/span&gt;
&lt;span class="c"&gt;# ^ Fue compilado contra musl libc, no glibc. Diferente distro.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error 2: library version mismatch en producción
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./mi-app
&lt;span class="c"&gt;# ./mi-app: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33' not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compilé en Ubuntu 22.04, deployé en Debian 10. La versión de glibc era diferente. La solución real es buildear en el mismo entorno que producción — que es básicamente por qué Docker existe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile que evita este problema&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="c"&gt;# Alpine usa musl, no glibc — cuidado con binarios nativos&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;runner  &lt;/span&gt;
&lt;span class="c"&gt;# Debian slim, misma glibc que la mayoría de producción&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto conecta directo con lo que aprendí cuando estuve &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;optimizando performance en producción&lt;/a&gt; — el ambiente de build importa tanto como el código.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 3: LD_PRELOAD para bien y para mal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# LD_PRELOAD te permite inyectar una biblioteca ANTES que cualquier otra&lt;/span&gt;
&lt;span class="c"&gt;# Úsalo con cuidado — es poderoso y peligroso&lt;/span&gt;

&lt;span class="c"&gt;# Ejemplo legítimo: usar tcmalloc en vez del allocator default&lt;/span&gt;
&lt;span class="nv"&gt;LD_PRELOAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/x86_64-linux-gnu/libtcmalloc.so.4 ./mi-app

&lt;span class="c"&gt;# Ejemplo de debugging: interceptar llamadas a funciones&lt;/span&gt;
&lt;span class="c"&gt;# (básicamente cómo funcionan algunos sandboxes de agentes)&lt;/span&gt;
&lt;span class="c"&gt;# Relacionado con lo que exploré en /blog/sandboxes-coding-agents-freestyle&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La sandbox de Freestyle que analicé hace unos días usa mecanismos similares — interceptar syscalls a nivel de proceso para aislar lo que puede hacer el agente.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Linux ELF y dynamic linking
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es un archivo ELF en Linux?&lt;/strong&gt;&lt;br&gt;
ELF (Executable and Linkable Format) es el formato estándar para binarios ejecutables, bibliotecas compartidas y archivos objeto en Linux. Es básicamente un contenedor estructurado que le dice al kernel cómo cargar y ejecutar el código. Todo binario de Linux moderno es un ELF — podés verificarlo con &lt;code&gt;file /ruta/al/binario&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué diferencia hay entre static linking y dynamic linking?&lt;/strong&gt;&lt;br&gt;
Con &lt;strong&gt;static linking&lt;/strong&gt;, todas las bibliotecas que necesita tu programa se copian adentro del binario en tiempo de compilación. El resultado es un binario más grande pero completamente autónomo. Con &lt;strong&gt;dynamic linking&lt;/strong&gt;, el binario solo guarda referencias a las bibliotecas, y el dynamic linker las carga en tiempo de ejecución. Dynamic linking es el default porque ahorra memoria (varias apps comparten el mismo código de libc en RAM) y facilita las actualizaciones de seguridad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué a veces un binario dice "No such file or directory" aunque existe?&lt;/strong&gt;&lt;br&gt;
Generalmente significa que el &lt;strong&gt;interpreter&lt;/strong&gt; (dynamic linker) especificado en el ELF no existe en ese sistema. Pasás un binario de Alpine (que usa musl libc) a Ubuntu (que usa glibc) y el path al dynamic linker no existe. Podés diagnosticarlo con &lt;code&gt;readelf -l tu-binario | grep interpreter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es LD_PRELOAD y por qué es peligroso?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;LD_PRELOAD&lt;/code&gt; es una variable de entorno que le dice al dynamic linker que cargue una biblioteca específica ANTES que cualquier otra, incluyendo libc. Esto permite interceptar y reemplazar funciones del sistema. Es útil para profiling y debugging, pero peligroso porque puede usarse para inyectar código malicioso. Por eso los binarios con setuid lo ignoran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es la vDSO (linux-vdso.so.1)?&lt;/strong&gt;&lt;br&gt;
Es una biblioteca virtual que el kernel mapea automáticamente en el espacio de memoria de cada proceso. Contiene implementaciones de syscalls muy frecuentes (como &lt;code&gt;gettimeofday&lt;/code&gt;) que se ejecutan en espacio de usuario sin hacer un context switch real al kernel. Es por eso que &lt;code&gt;ldd&lt;/code&gt; la muestra sin path — no es un archivo en disco, vive en el kernel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cómo afecta esto a Docker y los contenedores?&lt;/strong&gt;&lt;br&gt;
Mucho. Los contenedores comparten el kernel del host, pero tienen su propio filesystem. Si buildeas un binario en una imagen con glibc 2.35 y lo corrés en un contenedor con glibc 2.17, va a fallar. Es por eso que las imágenes de Docker deben ser consistentes entre build y runtime. También es por qué las imágenes basadas en Alpine (musl libc) a veces tienen comportamientos inesperados con binarios compilados para glibc.&lt;/p&gt;
&lt;h2&gt;
  
  
  Lo que me llevé: el dev de producto que finalmente bajó al metal
&lt;/h2&gt;

&lt;p&gt;Honestamente, me da un poco de vergüenza haber esquivado esto por tanto tiempo. Trabajé con Linux desde los 18 años, administré servidores, diagnostiqué cortes de red a las 11pm con el cyber lleno, y nunca me pregunté en serio qué pasa en esos microsegundos entre &lt;code&gt;./programa&lt;/code&gt; y la primera línea de código.&lt;/p&gt;

&lt;p&gt;El pivot que hice en 2020 hacia desarrollo de software me hizo subir capas de abstracción — React, TypeScript, Next.js. &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;Aprender a pensar en componentes fue difícil cuando venías de pensar en paquetes de red&lt;/a&gt;. Pero subir no significa que las capas de abajo desaparezcan. Siguen ahí.&lt;/p&gt;

&lt;p&gt;Cuando trabajo en &lt;a href="https://dev.to/blog/llm-pequeno-browser-edge-inferencia-nextjs"&gt;inferencia de LLMs en el edge&lt;/a&gt; o pienso en &lt;a href="https://dev.to/blog/sandboxes-coding-agents-freestyle"&gt;cómo aislar agentes de código&lt;/a&gt;, entender qué pasa a nivel de proceso importa. Las abstracciones son útiles hasta que se rompen — y cuando se rompen, bajás al metal o pagás a alguien que entienda el metal.&lt;/p&gt;

&lt;p&gt;Mi recomendación concreta: pasá una tarde con &lt;code&gt;strace&lt;/code&gt;, &lt;code&gt;ldd&lt;/code&gt; y &lt;code&gt;readelf&lt;/code&gt;. No para convertirte en systems programmer — para entender la máquina que ejecuta tu código todos los días.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Empezá por acá. Cinco minutos, en cualquier Linux:&lt;/span&gt;
strace &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;ls&lt;/span&gt; /tmp 2&amp;gt;&amp;amp;1  &lt;span class="c"&gt;# ¿Cuántas syscalls hace ls?&lt;/span&gt;
ldd &lt;span class="si"&gt;$(&lt;/span&gt;which node&lt;span class="si"&gt;)&lt;/span&gt;        &lt;span class="c"&gt;# ¿De qué depende Node?&lt;/span&gt;
readelf &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;which &lt;span class="nb"&gt;ls&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# ¿Qué tiene adentro un binario?&lt;/span&gt;
file /bin/&lt;span class="k"&gt;*&lt;/span&gt;              &lt;span class="c"&gt;# ¿Qué tipos de ELF hay en tu sistema?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La Amiga de 1994 no tenía dynamic linking — todo era estático, todo estaba en ROM o en el disco, y el sistema era lo que era. En cierto punto esa simplicidad era más honesta. Hoy corremos sobre capas de capas de capas, y cada tanto vale la pena bajar a ver en qué está parado todo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Cuántas syscalls hace tu app antes de ejecutar una línea de código? Medilo con &lt;code&gt;strace -c ./tu-binario&lt;/code&gt; y mandame el número. Apuesto a que te sorprende.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/linux-elf-dynamic-linking-como-funciona" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>sistemas</category>
      <category>elf</category>
    </item>
    <item>
      <title>Quantum computing para el dev web que no estudió física: ¿cuándo preocuparse en serio?</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:32:37 +0000</pubDate>
      <link>https://forem.com/jtorchia/quantum-computing-para-el-dev-web-que-no-estudio-fisica-cuando-preocuparse-en-serio-3ea4</link>
      <guid>https://forem.com/jtorchia/quantum-computing-para-el-dev-web-que-no-estudio-fisica-cuando-preocuparse-en-serio-3ea4</guid>
      <description>&lt;p&gt;Hay días que abrís Hacker News y encontrás un post que te hace sentir que sabés muy poco de algo que creías tener medianamente claro. Esta semana fue uno de esos días.&lt;/p&gt;

&lt;p&gt;Un criptógrafo cuántico posteó un análisis técnico sobre el estado actual del quantum computing aplicado a criptografía. 289 puntos. 340 comentarios. Yo entendí quizás el 30% del hilo. Y eso que llevo más de una hora intentando seguirlo.&lt;/p&gt;

&lt;p&gt;La pregunta que me quedó dando vueltas no es abstracta: &lt;strong&gt;¿cuándo debería yo — un full-stack developer que deployea en Railway, piensa en milisegundos de response time y la semana pasada optimizó &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;una app Next.js de 3 segundos a 300ms&lt;/a&gt; — empezar a preocuparme en términos prácticos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No sé la respuesta. Pero eso es exactamente por qué vale la pena escribir este post.&lt;/p&gt;




&lt;p&gt;El quantum computing es básicamente como tener un cerrajero que no intenta cada llave una por una, sino que de alguna manera prueba todas las llaves posibles al mismo tiempo. Y lo que hoy te lleva millones de años romper con fuerza bruta, con esa lógica podría llevar horas.&lt;/p&gt;

&lt;p&gt;Una vez que lo ves así, entendés por qué los criptógrafos se ponen nerviosos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El quantum computing timeline para desarrolladores web: el estado real en 2025
&lt;/h2&gt;

&lt;p&gt;Primer aclaramiento importante: &lt;strong&gt;no estoy hablando de algo que pasa mañana&lt;/strong&gt;. El quantum computing que existe hoy es ruidoso, inestable, y no escala bien. Las máquinas de IBM o Google tienen decenas o cientos de qubits "reales" pero con tasas de error que hacen que la mayoría de los algoritmos criptográficamente relevantes sean imposibles de correr.&lt;/p&gt;

&lt;p&gt;Para romper RSA-2048 — el estándar que protege buena parte de HTTPS hoy — necesitarías aproximadamente 4000 qubits lógicos estables. Los qubits lógicos son distintos de los físicos; necesitás muchos físicos para hacer uno lógico confiable por culpa del error de corrección.&lt;/p&gt;

&lt;p&gt;Hoy estamos, dependiendo de a quién le preguntés, &lt;strong&gt;entre 10 y 20 años lejos&lt;/strong&gt; de tener máquinas con esa capacidad. Algunos dicen 15 años. Otros dicen que nunca llegamos. Otros dicen que ya hay actores estado-nación haciendo cosas que no vemos.&lt;/p&gt;

&lt;p&gt;Ese rango de incertidumbre es exactamente el problema.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que el post de HN me hizo entender
&lt;/h3&gt;

&lt;p&gt;El hilo que mencioné giraba alrededor de algo que se llama &lt;strong&gt;"harvest now, decrypt later"&lt;/strong&gt; (HNDL). La idea es: aunque hoy no podés romper el cifrado, podés interceptar tráfico cifrado &lt;em&gt;ahora&lt;/em&gt; y guardarlo para cuando tengas la capacidad cuántica de descifrarlo en el futuro.&lt;/p&gt;

&lt;p&gt;Eso cambia la ecuación dramáticamente. Si alguien está capturando tu tráfico HTTPS de 2025 para descifrarlo en 2035, el timeline "10 a 20 años" se convierte en &lt;strong&gt;ahora mismo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;¿Quiénes hacen eso? Principalmente actores estado-nación. ¿Le importa eso a mi API de Next.js que muestra recetas de cocina? Probablemente no. ¿Le importa a un sistema de salud, a comunicaciones de defensa, a transacciones financieras de largo plazo? Absolutamente sí.&lt;/p&gt;

&lt;p&gt;Así que la primera respuesta práctica es: &lt;strong&gt;depende de qué estás construyendo&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que como developer full-stack debería saber sobre post-quantum cryptography
&lt;/h2&gt;

&lt;p&gt;Aquí está lo que aprendí intentando entender el hilo sin tener un PhD en física:&lt;/p&gt;

&lt;h3&gt;
  
  
  NIST ya tomó decisiones
&lt;/h3&gt;

&lt;p&gt;En 2024, el &lt;strong&gt;NIST (National Institute of Standards and Technology)&lt;/strong&gt; finalizó los primeros estándares de criptografía post-cuántica. Los algoritmos que pasaron son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ML-KEM&lt;/strong&gt; (antes CRYSTALS-Kyber): para intercambio de claves&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ML-DSA&lt;/strong&gt; (antes CRYSTALS-Dilithium): para firmas digitales
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SLH-DSA&lt;/strong&gt; (antes SPHINCS+): para firmas digitales&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto es importante porque significa que el trabajo de estandarización ya pasó. No estamos esperando que los matemáticos se pongan de acuerdo — ya lo hicieron.&lt;/p&gt;

&lt;h3&gt;
  
  
  TLS 1.3 ya está preparándose
&lt;/h3&gt;

&lt;p&gt;Chrome, Firefox y algunos servidores ya están experimentando con &lt;strong&gt;hybrid key exchange&lt;/strong&gt; — combinan el algoritmo clásico (X25519) con uno post-cuántico (ML-KEM) en el mismo handshake. Si uno falla, el otro sigue funcionando. Si el cuántico resulta tener vulnerabilidades no descubiertas todavía, el clásico te cubre.&lt;/p&gt;

&lt;p&gt;Como dev web, probablemente esto te llegue transparentemente vía updates de OpenSSL, nginx, o Node.js. No tenés que hacer nada... todavía.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lo que SÍ tenés que pensar activamente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Esto es lo que MUCHOS proyectos hacen hoy&lt;/span&gt;
&lt;span class="c1"&gt;// y que puede ser problemático en un horizonte post-cuántico&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Algoritmos que eventualmente van a ser vulnerables&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// RSA&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publicEncrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rsaPublicKey&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="c1"&gt;// RSA&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Algoritmos simétricos — estos están relativamente bien&lt;/span&gt;
&lt;span class="c1"&gt;// AES-256 sigue siendo seguro en un mundo cuántico&lt;/span&gt;
&lt;span class="c1"&gt;// (Grover's algorithm lo debilita pero no lo rompe — 256 bits -&amp;gt; 128 bits efectivos)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// OK por ahora&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Más seguro&lt;/span&gt;

&lt;span class="c1"&gt;// 🤔 La pregunta real: ¿tus secrets/tokens tienen que durar décadas?&lt;/span&gt;
&lt;span class="c1"&gt;// Si un JWT expira en 1 hora, el riesgo post-cuántico es casi nulo&lt;/span&gt;
&lt;span class="c1"&gt;// Si estás firmando contratos que tienen que ser válidos en 2040...&lt;/span&gt;
&lt;span class="c1"&gt;// ahí sí hay que pensar distinto&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La clave práctica que saqué: &lt;strong&gt;el riesgo escala con el tiempo de vida de lo que firmás o cifrás&lt;/strong&gt;. Un access token de 15 minutos y un certificado de firma de documentos legales tienen riesgos completamente distintos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los errores de framing más comunes cuando leés sobre quantum computing
&lt;/h2&gt;

&lt;p&gt;Acá está lo que me parece que la mayoría de los posts de divulgación hacen mal — incluyendo probablemente este:&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 1: Confundir "quantum advantage" con "quantum supremacy" con "cryptographically relevant"
&lt;/h3&gt;

&lt;p&gt;Cuando Google o IBM anuncian un hito cuántico, los medios lo presentan como "ya pueden romper el cifrado". Casi nunca es eso. Quantum advantage significa que resolvieron &lt;em&gt;algún problema específico&lt;/em&gt; más rápido que una computadora clásica. Ese problema específico suele ser artificioso y diseñado para que la computadora cuántica brille.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cryptographically relevant&lt;/strong&gt; quantum computing — el que realmente importa — es una barra mucho más alta.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 2: Pensar que bcrypt o Argon2 están muertos
&lt;/h3&gt;

&lt;p&gt;El hashing de passwords como bcrypt, scrypt o Argon2 usa funciones hash simétricas. El algoritmo de Grover (el que aplica computación cuántica a búsqueda) efectivamente reduce su seguridad a la mitad — pero Argon2 con parámetros modernos tiene margen de sobra. &lt;strong&gt;No necesitás cambiar tu sistema de autenticación ahora mismo.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 3: Ignorarlo completamente porque "falta mucho"
&lt;/h3&gt;

&lt;p&gt;Este es el error que me parece más peligroso para devs que construyen infraestructura de largo plazo. Si estás construyendo algo que va a manejar datos sensibles por décadas, &lt;strong&gt;el timeline importa&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pensá en todo lo que construí durante &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;el pivote a software development en 2020&lt;/a&gt; — la infraestructura que elegís hoy puede seguir corriendo en producción en 2035. Cuando elegís un stack de autenticación o cifrado hoy, estás eligiendo para ese horizonte de tiempo también.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error 4: Pensar que esto es solo un problema del devops o del sysadmin
&lt;/h3&gt;

&lt;p&gt;En el ecosistema de 2025 que describí cuando &lt;a href="https://dev.to/blog/como-construi-juanchi-dev"&gt;armé mi stack para juanchi.dev&lt;/a&gt;, los developers full-stack tomamos decisiones de arquitectura que antes eran de ops. Eso incluye qué librería de crypto usás, cómo firmás tokens, qué tipo de certificados pedís.&lt;/p&gt;

&lt;p&gt;No podés delegarlo completamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Código: qué revisar en tu proyecto hoy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Auditoría rápida de superficie de ataque post-cuántica&lt;/span&gt;
&lt;span class="c1"&gt;// Revisá estos patrones en tu codebase&lt;/span&gt;

&lt;span class="c1"&gt;// 1. ALGORITMOS ASIMÉTRICOS — los más vulnerables&lt;/span&gt;
&lt;span class="c1"&gt;// Buscá: RSA, ECDH, ECDSA, DH&lt;/span&gt;
&lt;span class="c1"&gt;// ¿Dónde aparecen?&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createSign&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ❓ RSA — vulnerable a Shor's algorithm&lt;/span&gt;
&lt;span class="c1"&gt;// Si estos datos tienen que ser válidos en 2035+, pensalo&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rsa&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;modulusLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Esto eventualmente no va a ser suficiente&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// ❓ ECDSA — también vulnerable, aunque más eficiente hoy&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ecKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPairSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ec&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;namedCurve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prime256v1&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="c1"&gt;// 2. ALGORITMOS SIMÉTRICOS — relativamente OK&lt;/span&gt;
&lt;span class="c1"&gt;// AES-256, ChaCha20-Poly1305, SHA-256/384/512&lt;/span&gt;
&lt;span class="c1"&gt;// Estos necesitan el doble de bits para ser vulnerables (Grover)&lt;/span&gt;
&lt;span class="c1"&gt;// pero con 256 bits tenés colchón de sobra&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createDecipheriv&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cifrarDatosLocales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// AES-256-GCM — esto sigue siendo seguro post-quantum&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCipheriv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aes-256-gcm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cifrado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;datos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;final&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;cipher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthTag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// El tag de autenticación es importante&lt;/span&gt;
  &lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cifrado&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 3. JWT — el caso práctico más común&lt;/span&gt;
&lt;span class="c1"&gt;// La respuesta corta: si expira en horas, no te preocupés&lt;/span&gt;
&lt;span class="c1"&gt;// Si firmás algo permanente con JWT... cuestioná el diseño&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluarRiesgoJWT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;años&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;expiresInSeconds&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;años&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Riesgo post-cuántico prácticamente nulo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;años&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Riesgo bajo, monitoreá el timeline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;años&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Riesgo moderado, considerá migración&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Riesgo alto — rediseñá este componente&lt;/span&gt;&lt;span class="dl"&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="nf"&gt;evaluarRiesgoJWT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// "Riesgo post-cuántico prácticamente nulo"&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="nf"&gt;evaluarRiesgoJWT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// "Riesgo moderado..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La función &lt;code&gt;evaluarRiesgoJWT&lt;/code&gt; es una simplificación obvia, pero captura el punto central: &lt;strong&gt;el tiempo de vida de lo que firmás es la variable más importante&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Cuando definís los &lt;a href="https://dev.to/blog/typescript-patrones-avanzados-que-uso"&gt;patrones de TypeScript que usás en producción&lt;/a&gt;, incluir tipos explícitos para el contexto de seguridad — cuánto tiempo vive un token, qué nivel de sensibilidad tienen los datos — es el tipo de diseño que ayuda cuando tenés que hacer una auditoría de esto en el futuro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hacer concretamente hoy (sin entrar en pánico)
&lt;/h2&gt;

&lt;p&gt;Esta es mi lista personal, honesta, sin exagerar el riesgo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ahora mismo (sin importar el tipo de proyecto):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usá TLS 1.3 — ya implementa algunas mejoras de seguridad y va a recibir actualizaciones post-cuánticas&lt;/li&gt;
&lt;li&gt;Preferí AES-256 sobre AES-128 para cifrado simétrico&lt;/li&gt;
&lt;li&gt;Mantenés las dependencias actualizadas — la migración post-cuántica va a llegar vía updates de librerías&lt;/li&gt;
&lt;li&gt;No implementés crypto propio — en serio, nunca, quantum o no quantum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;En los próximos 1-2 años (si manejás datos sensibles de largo plazo):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inventariá qué en tu sistema usa criptografía asimétrica y cuánto tiempo tienen que vivir esos datos&lt;/li&gt;
&lt;li&gt;Empezá a leer sobre las librerías que van a adoptar los algoritmos NIST — Open Quantum Safe ya tiene implementaciones&lt;/li&gt;
&lt;li&gt;Considerá diseñar para "cripto-agilidad": que tu sistema pueda cambiar de algoritmo sin reescribir todo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Para el &lt;a href="https://dev.to/blog/stack-tecnologico-perfecto-2025"&gt;stack que elegiría en 2025&lt;/a&gt;:&lt;/strong&gt;&lt;br&gt;
Hoy elegiría librerías activamente mantenidas por organizaciones que ya tienen planes de migración post-cuántica documentados. Node.js y OpenSSL están en ese camino. Es un criterio más para sumar a la evaluación.&lt;/p&gt;




&lt;h2&gt;
  
  
  FAQ: quantum computing y desarrollo web
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿HTTPS va a dejar de ser seguro por el quantum computing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No en el corto plazo, y probablemente no de golpe. TLS ya está siendo actualizado con algoritmos post-cuánticos (hybrid key exchange en TLS 1.3). El browser que usás hoy ya está recibiendo estas actualizaciones gradualmente. Lo que sí es una amenaza real es el ataque "harvest now, decrypt later" para datos muy sensibles — pero eso aplica a actores estado-nación, no al tráfico web promedio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Tengo que cambiar el sistema de login/passwords de mi app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No urgentemente. bcrypt, Argon2, y scrypt usan funciones hash simétricas que son mucho más resistentes a quantum computing que los algoritmos asimétricos. Argon2id con parámetros modernos tiene margen de seguridad suficiente. La recomendación es seguir con las mejores prácticas actuales y mantenerte actualizado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Los JWTs quedan obsoletos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depende de cómo los usés. Si firmás access tokens que expiran en 15 minutos o 1 hora, el riesgo es prácticamente nulo — para cuando exista quantum computing relevante, esos tokens llevan años vencidos. Si usás JWTs para firmar algo que tiene que ser válido por años (documentos, contratos), ahí sí hay que empezar a pensar en alternativas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué es "post-quantum cryptography" y en qué se diferencia de "quantum cryptography"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Post-quantum cryptography (PQC) son algoritmos clásicos — corren en computadoras normales — diseñados para ser resistentes a ataques de computadoras cuánticas. Quantum cryptography (QKD, quantum key distribution) usa principios cuánticos para la comunicación en sí. Como dev web, lo que te importa es PQC — es lo que vas a implementar en tu stack. QKD requiere hardware especializado y es un campo completamente distinto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Cuándo debería empezar a usar las librerías post-cuánticas en producción?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Para la mayoría de las aplicaciones web: cuando lleguen via updates de tus dependencias actuales, que es probablemente lo que va a pasar. Node.js, OpenSSL y los providers de TLS van a implementar los estándares NIST gradualmente. Si manejás datos críticos de largo plazo, vale la pena explorar Open Quantum Safe hoy y empezar a hacer pruebas. Para el resto, mantenete actualizado y no entres en pánico.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿El quantum computing afecta a blockchain y crypto también?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sí, y bastante. Las criptomonedas usan ECDSA para firmar transacciones — vulnerable a Shor's algorithm. Bitcoin y Ethereum tendrían que migrar sus sistemas de firma antes de que quantum computing sea relevante. Es uno de los debates más activos en esas comunidades. Como dato de color: las wallets activas están más expuestas que las inactivas, porque las activas exponen la clave pública en cada transacción.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión: la ignorancia honesta como posición válida
&lt;/h2&gt;

&lt;p&gt;Cuando empecé a escribir este post no sabía bien qué iba a concluir. Sigo sin saber si en 10 años estamos re-cifrando todo el internet o si el quantum computing sigue siendo una promesa que no termina de llegar.&lt;/p&gt;

&lt;p&gt;Lo que sí saqué en limpio:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;El timeline importa más que el tema&lt;/strong&gt;. No es un problema binario de "hay que preocuparse" o "no hay que preocuparse". Es una función del tiempo de vida de tus datos y la sensibilidad de los mismos.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La industria ya se está moviendo&lt;/strong&gt;. NIST finalizó estándares. TLS ya está experimentando con algoritmos híbridos. No vas a tener que hacer todo a mano — va a llegar via ecosistema.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La cripto-agilidad es la mejor inversión&lt;/strong&gt;. Diseñar tus sistemas para que puedan cambiar de algoritmo sin reescritura total es buena práctica con o sin quantum computing. La historia de la criptografía es la historia de algoritmos que se rompen y se reemplazan.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Para la mayoría de los proyectos: mantenete actualizado y no entres en pánico&lt;/strong&gt;. Si tu app maneja credenciales de usuarios que expiran, tokens de sesión normales, y datos que no tienen que ser válidos por décadas — el riesgo hoy es bajo. Aplicá las mejores prácticas actuales.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lo que me queda pendiente es seguir leyendo. El hilo de HN que disparó esto tenía respuestas de gente con décadas de experiencia en criptografía que no se ponía de acuerdo. Eso me dice que la humildad epistémica es la postura correcta.&lt;/p&gt;

&lt;p&gt;Si sos el tipo de developer que, como yo, viene de diagnosticar redes a las 11pm en un cyber o de tirar un servidor de producción con &lt;code&gt;rm -rf&lt;/code&gt; en la primera semana de trabajo — sabés que la mejor preparación para los problemas grandes no es entrar en pánico cuando aparecen, sino construir sistemas que puedan adaptarse.&lt;/p&gt;

&lt;p&gt;Eso aplica acá también.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/quantum-computing-timeline-desarrolladores-web" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>fullstack</category>
      <category>quantumcomputing</category>
      <category>criptografa</category>
    </item>
    <item>
      <title>Google Maps para codebases: pegué la URL de mi propio repo y me asusté un poco</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:32:36 +0000</pubDate>
      <link>https://forem.com/jtorchia/google-maps-para-codebases-pegue-la-url-de-mi-propio-repo-y-me-asuste-un-poco-1ca4</link>
      <guid>https://forem.com/jtorchia/google-maps-para-codebases-pegue-la-url-de-mi-propio-repo-y-me-asuste-un-poco-1ca4</guid>
      <description>&lt;p&gt;Gitingest, Repomix, CodeViz — la semana pasada cayeron en mi radar varias herramientas que prometen lo mismo: pegás una URL de GitHub y podés conversar con el código, mapearlo, entender su arquitectura en segundos. La comunidad las está descubriendo con el entusiasmo habitual. Yo también las probé. Y el experimento más interesante no fue analizar el repo de algún framework famoso.&lt;/p&gt;

&lt;p&gt;Fue analizar el mío.&lt;/p&gt;

&lt;p&gt;Hay algo levemente narcisista en pegar la URL de tu propio proyecto en una herramienta de análisis. Y algo levemente aterrador en lo que te devuelve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Codebase visualization con GitHub AI análisis: de qué hablamos exactamente
&lt;/h2&gt;

&lt;p&gt;Antes de entrar en lo que encontré, vale la pena aclarar de qué hablamos cuando hablamos de "visualización de codebases con AI".&lt;/p&gt;

&lt;p&gt;No es simplemente un grafo de dependencias. Eso existía hace años y nadie lo usaba porque los grafos de dependencias de Node.js parecen el mapa del subte de Tokio después de un terremoto.&lt;/p&gt;

&lt;p&gt;Lo que cambió es la capa de lenguaje natural encima. Herramientas como &lt;strong&gt;Gitingest&lt;/strong&gt; convierten el repo entero en un formato que puede ingerir un LLM. Después podés hacer preguntas en lenguaje natural: "¿dónde están los cuellos de botella de performance?", "¿qué componentes tienen más acoplamiento?", "¿hay patrones inconsistentes en el manejo de errores?"&lt;/p&gt;

&lt;p&gt;Repomix hace algo similar pero más enfocado en generar un archivo de contexto comprimido. La idea es que ese archivo se lo pasás a Claude o GPT-4 como contexto y preguntás lo que quieras.&lt;/p&gt;

&lt;p&gt;Lo que obtienen estas herramientas no es magia — es contexto masivo entregado de forma eficiente. El análisis lo hace el LLM. La herramienta es el preprocesador.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalación básica de Repomix&lt;/span&gt;
npx repomix

&lt;span class="c"&gt;# O apuntando a un repo remoto directamente&lt;/span&gt;
npx repomix &lt;span class="nt"&gt;--remote&lt;/span&gt; juanchi-dev/juanchi.dev

&lt;span class="c"&gt;# Genera un archivo repomix-output.xml con todo el código&lt;/span&gt;
&lt;span class="c"&gt;# comprimido y listo para pasarle a un LLM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hasta acá, nada nuevo. La novedad es qué pasa cuando el código analizado es tuyo y lo conocés de memoria — o eso creías.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que un LLM encontró en juanchi.dev que yo no veía
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://juanchi.dev" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt; es mi proyecto público. Lo construí con Next.js 15, React 19, Tailwind v4, desplegado en Railway. &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;Ya escribí sobre el stack en detalle&lt;/a&gt;. Creía que lo conocía bien.&lt;/p&gt;

&lt;p&gt;Pasé el repo por Repomix, generé el archivo de contexto, y lo cargué en Claude con un prompt simple: &lt;em&gt;"Analizá este codebase como si fueras un senior developer haciendo code review. Sé honesto. No me des palmaditas en la espalda."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lo que salió me hizo abrir tres archivos que no tocaba hace semanas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallazgo 1: Inconsistencia en el manejo de errores&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mis Server Components y mis Route Handlers manejaban los errores de forma distinta. En algunos lugares usaba try/catch con logging explícito. En otros, dejaba que Next.js manejara el error silenciosamente. No había una estrategia unificada.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cómo manejaba errores en algunos Server Components&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBlogPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Logging explícito, re-throw controlado&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Error fetching post &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Post no encontrado&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="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Cómo manejaba errores en OTROS lugares (el problema)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProjects&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Sin try/catch. Si falla, falla en silencio o explota arriba&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El LLM lo identificó como "falta de estrategia de error handling consistente". Tenía razón. No era un bug — era deuda técnica acumulada de haber construido el proyecto en múltiples sesiones sin un estándar definido.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hallazgo 2: Componentes con demasiadas responsabilidades&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Había un componente que el análisis identificó como un "God Component" — hacía fetching, formateo de datos Y renderizado, todo junto. Yo lo había construido así porque en el momento era más rápido. Funcionaba. Pero no era correcto.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// El componente problemático (simplificado)&lt;/span&gt;
&lt;span class="c1"&gt;// Hace demasiado: fetch + transform + render&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;BlogPostCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Lógica de fetching que debería estar separada&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/posts/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="c1"&gt;// Transformación que debería estar en una utility&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;es-AR&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;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;long&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="c1"&gt;// Solo esto debería estar acá&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="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;formattedDate&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&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;&lt;strong&gt;Hallazgo 3: El que más me dolió&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tenía lógica de negocio duplicada en dos lugares distintos. La misma transformación de datos escrita dos veces, ligeramente diferente cada vez. El tipo de cosa que en una code review rechazarías en el primer comentario.&lt;/p&gt;

&lt;p&gt;No lo había visto porque cuando estás dentro del código, navegás por él de forma funcional — abrís el archivo que necesitás, hacés el cambio, cerrás. No tenés la vista panorámica.&lt;/p&gt;

&lt;p&gt;Eso es exactamente lo que &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;hice cuando optimicé el performance del sitio a 300ms&lt;/a&gt;: fui archivo por archivo, función por función. Eficiente para ese objetivo. Ciego para el panorama general.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los antipatrones que no ves porque sos vos quien los escribió
&lt;/h2&gt;

&lt;p&gt;Hay un problema epistemológico en revisar tu propio código: sabés demasiado.&lt;/p&gt;

&lt;p&gt;Sabés por qué tomaste cada decisión. Sabés el contexto. Sabés qué quisiste hacer. Y ese conocimiento actúa como una capa de racionalización que filtra los problemas antes de que los veas.&lt;/p&gt;

&lt;p&gt;Cuando un externo (humano o LLM) mira el código, no tiene ese contexto. Ve solo lo que está escrito. Y a veces lo que está escrito no refleja lo que tenías en mente.&lt;/p&gt;

&lt;p&gt;Esto me recuerda algo que aprendí mucho antes de escribir TypeScript. Cuando estudiaba para el &lt;a href="https://dev.to/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619"&gt;CCNA en 2009&lt;/a&gt;, practicaba configuraciones de red en Packet Tracer hasta memorizarlas. Pero cuando me sentaba a hacer los simulacros de examen, me equivocaba en exactamente las cosas que "sabía". El conocimiento implícito no siempre sobrevive el cambio de contexto.&lt;/p&gt;

&lt;p&gt;El LLM operando sobre mi código es ese cambio de contexto forzado.&lt;/p&gt;

&lt;p&gt;Dicho esto — no es magia y tiene límites claros.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo que el análisis NO vio:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Por qué algunas decisiones de arquitectura son intencionales (tradeoffs que yo conozco)&lt;/li&gt;
&lt;li&gt;El contexto de evolución del proyecto (code que parece legacy pero tiene razón de ser)&lt;/li&gt;
&lt;li&gt;Problemas de performance que solo se ven en runtime — para eso tuve que &lt;a href="https://dev.to/blog/optimizacion-performance-nextjs-3s-a-300ms"&gt;instrumentar manualmente&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Integraciones específicas con servicios externos donde la "inconsistencia" es necesaria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combinado con lo que sí encontró, el mapa es útil. Solo. No suficiente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errores comunes cuando usás estas herramientas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Error 1: Tomar todo como verdad absoluta&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El LLM no sabe si esa "inconsistencia" en el manejo de errores es deuda técnica o una decisión deliberada. Vos sí. Filtrá.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 2: Usarlo sobre repos gigantes sin acotar el contexto&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repomix sobre un monorepo de 500k líneas va a generar un archivo de contexto que ningún LLM puede procesar bien. El análisis se degrada. Mejor acotar: pasá solo los directorios relevantes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mejor acotar el análisis a lo que importa&lt;/span&gt;
npx repomix &lt;span class="nt"&gt;--include&lt;/span&gt; &lt;span class="s2"&gt;"src/components/**,src/lib/**"&lt;/span&gt;

&lt;span class="c"&gt;# En vez de pasar todo el repo con node_modules y ruido&lt;/span&gt;
npx repomix  &lt;span class="c"&gt;# sin filtros = mucho ruido&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error 3: Esperar que reemplace el code review humano&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El análisis AI encontró tres problemas reales en mi código. Un senior developer con contexto de negocio hubiera encontrado esos tres más cinco que el LLM racionalizó o no pudo evaluar. Esto es &lt;a href="https://dev.to/blog/sandboxes-coding-agents-freestyle"&gt;lo mismo que aprendí con los coding agents&lt;/a&gt;: el AI acelera, no reemplaza el criterio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 4: No iterar el prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El primer análisis que pedí fue genérico. El útil vino cuando refiné: &lt;em&gt;"Enfocate específicamente en patrones de fetching de datos y cómo se propagan los errores hasta el cliente. Ignorá styling y configuración."&lt;/em&gt; La especificidad importa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error 5: Usarlo solo una vez&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El valor real es usarlo como checkpoint periódico. Un snapshot del estado del código cada mes, con las mismas preguntas, te muestra si tu deuda técnica está creciendo o achicándose.&lt;/p&gt;

&lt;p&gt;Esto conecta con algo que vi cuando &lt;a href="https://dev.to/blog/llm-pequeno-browser-edge-inferencia-nextjs"&gt;metí un LLM chico en una app Next.js&lt;/a&gt;: la calidad del output depende brutalmente de la calidad del contexto que le das. Garbage in, garbage out — pero también ruido in, análisis diluido out.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ: Preguntas frecuentes sobre codebase visualization con AI
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Cuál es la mejor herramienta para analizar un repo de GitHub con AI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depende del objetivo. Para conversación libre sobre el código, &lt;strong&gt;Gitingest&lt;/strong&gt; (gitingest.com) es el más directo — pegás la URL y podés preguntar en lenguaje natural. Para generar contexto que pasarle a tu LLM preferido, &lt;strong&gt;Repomix&lt;/strong&gt; es más flexible y configurable. Para visualización gráfica de dependencias, &lt;strong&gt;CodeViz&lt;/strong&gt; o &lt;strong&gt;Mermaid&lt;/strong&gt; generado por el LLM funcionan bien. No hay una bala de plata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Es seguro pegar la URL de un repo privado en estas herramientas?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Depende de la herramienta y tu modelo de amenaza. Repomix instalado localmente nunca sale de tu máquina — el código no va a ningún servidor externo. Herramientas web como Gitingest procesan el código en sus servidores. Para repos privados con código sensible, la opción local siempre es más segura. Para repos públicos, no hay diferencia.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Qué tan precisos son los análisis que devuelve el LLM?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En mi experiencia: precisos en detectar inconsistencias de patrones, imprecisos en evaluar decisiones de arquitectura sin contexto. El LLM ve el código tal como está escrito. No ve por qué está así. Los falsos positivos existen — te va a señalar como problema algo que es una decisión intencional. El filtro humano es obligatorio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Funciona bien con repos grandes (más de 100k líneas)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mal, en general. Los LLMs tienen ventanas de contexto finitas. Repomix comprime el código para maximizar lo que entra, pero con repos muy grandes hay que acotar el análisis a subdirectorios o módulos específicos. Mejor análisis focalizado que análisis superficial de todo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Puede reemplazar a un code review humano?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Complementarlo, sí. El análisis AI es rápido, sin ego, y no tiene contexto — esas tres cosas son simultáneamente su fortaleza y su límite. Encuentra lo que un humano podría pasar por alto por cansancio o familiaridad. No puede evaluar si una decisión es correcta para el negocio, el equipo, o la historia del proyecto. Son herramientas distintas para capas distintas del problema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Vale la pena usarlo sobre código propio si ya lo conocés bien?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Especialmente sobre código propio. Esa es la paradoja. Cuanto más conocés el código, más necesitás la perspectiva externa. El conocimiento implícito que tenés actúa como filtro — te impide ver los problemas porque ya los racionalizaste. Un LLM sin contexto ve solo lo que está escrito. A veces eso es exactamente lo que necesitás.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Mapa útil o ansiolítico digital?
&lt;/h2&gt;

&lt;p&gt;La pregunta que me hice antes de escribir esto: ¿cambié algo después del análisis?&lt;/p&gt;

&lt;p&gt;Sí. Unifiqué el manejo de errores. Rompí el God Component en tres piezas. Eliminé la duplicación de lógica. Tres cambios concretos en código que funciona en producción hoy.&lt;/p&gt;

&lt;p&gt;Pero también me pregunté si el ejercicio no era en parte ansiolítico — la sensación de que "auditaste" tu código sin haber realmente enfrentado el problema más profundo, que es tener más disciplina desde el principio.&lt;/p&gt;

&lt;p&gt;La respuesta honesta es: probablemente ambas cosas. Y está bien.&lt;/p&gt;

&lt;p&gt;Lo que aprendí en 30 años con tecnología — desde diagnosticar cortes de red a las 11pm en un cyber hasta deployar en Railway — es que las herramientas que te dan perspectiva son valiosas aunque sean imperfectas. El CCNA no me enseñó a administrar redes reales. Me dio el vocabulario para entender qué estaba mirando. &lt;a href="https://dev.to/blog/claude-code-updates-febrero-2025"&gt;Claude Code&lt;/a&gt; no escribe el código por mí. Acelera el tiempo que paso en lo que ya sé hacer.&lt;/p&gt;

&lt;p&gt;Estas herramientas de visualización hacen algo similar: te devuelven tu propio código con ojos nuevos. Lo que hacés con eso depende de vos.&lt;/p&gt;

&lt;p&gt;Pegá la URL de tu repo. Date un susto productivo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/codebase-visualization-github-ai-analisis" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>ai</category>
      <category>github</category>
      <category>anlisisdecdigo</category>
    </item>
    <item>
      <title>De DOS a Cloud: mi viaje de 33 años con la tecnología — desde una Amiga en 1994 hasta deployar en Railway con Next.js</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:29:58 +0000</pubDate>
      <link>https://forem.com/jtorchia/de-dos-a-cloud-mi-viaje-de-33-anos-con-la-tecnologia-desde-una-amiga-en-1994-hasta-deployar-en-5172</link>
      <guid>https://forem.com/jtorchia/de-dos-a-cloud-mi-viaje-de-33-anos-con-la-tecnologia-desde-una-amiga-en-1994-hasta-deployar-en-5172</guid>
      <description>&lt;p&gt;Hay una foto que no tengo pero que existe perfectamente nítida en mi memoria: yo, 1994, tres años recién cumplidos, parado frente a una Commodore Amiga 500 con un joystick que me quedaba enorme en las manos. Mi viejo había traído esa máquina de quién sabe dónde y yo no entendía absolutamente nada de lo que pasaba en la pantalla. Pero algo en ese monitor — los colores, el sonido, la idea de que &lt;em&gt;yo&lt;/em&gt; podía hacer que algo pasara — me enganchó de una manera que nunca más me soltó.&lt;/p&gt;

&lt;p&gt;Eso fue hace treinta y un años. Y acá estoy.&lt;/p&gt;

&lt;h2&gt;
  
  
  La Amiga como primer maestro
&lt;/h2&gt;

&lt;p&gt;La Amiga 500 no era una computadora de Windows ni de DOS. Era un bicho aparte, con su propio sistema operativo (AmigaOS), con una interfaz gráfica en una época donde la mayoría del mundo todavía tipeaba comandos en pantallas verdes. Obviamente yo no sabía nada de eso. Lo que sabía era que si agarraba el disquete correcto y lo metía en el drive, aparecía un juego. Y si hacía algo mal, aparecía el Guru Meditation — esa pantalla de error roja que para mí era aterradora, como si la máquina se estuviera muriendo.&lt;/p&gt;

&lt;p&gt;Pero el Guru Meditation fue mi primer contacto con la idea de que las computadoras &lt;em&gt;fallan&lt;/em&gt;. Que no son magia. Que hay algo adentro que puede romperse. Esa intuición — que después se convirtió en conocimiento real — es probablemente lo más valioso que me dejó esa Amiga oxidada.&lt;/p&gt;

&lt;p&gt;A los 5 años ya tenía mi primer dominio. No, no es un chiste. Mi viejo era de esa generación que entendió internet antes que nadie en Argentina, y de alguna manera yo estaba metido en ese mundo también. No entendía qué era un DNS ni por qué funcionaba, pero sabía que ese nombre era &lt;em&gt;mío&lt;/em&gt; y que apuntaba a algo en internet. La semilla del orgullo nerd estaba plantada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los cyber cafés como universidad
&lt;/h2&gt;

&lt;p&gt;Salteemos algunos años de caos típico de crecer en Argentina en los '90 y lleguemos a los 14. Estaba trabajando en cyber cafés. Y cuando digo trabajando, no digo atendiendo la caja — digo &lt;em&gt;metido abajo de los escritorios&lt;/em&gt;, pasando cables, configurando Windows 98 que se rompía solo mirándolo, instalando drivers de red que no existían para hardware que tampoco debería haber existido.&lt;/p&gt;

&lt;p&gt;Los cyber cafés de esa época eran una jungla. Diez máquinas conectadas con cable UTP pelado, hubs baratos que se calentaban como hornos, Windows pirata que cada tanto decidía que el mejor momento para reiniciar era en medio de un Counter-Strike. Mi trabajo no oficial era que todo siguiera andando. Y aprendí más redes en seis meses de cyber café que en cualquier curso formal.&lt;/p&gt;

&lt;p&gt;Aprendí qué es una subred porque tuve que configurarlas. Aprendí qué es DHCP porque cuando no funcionaba, los chicos no podían jugar y me gritaban. Aprendí qué es una dirección MAC porque era la única manera de identificar cuál de esas diez máquinas era la que se caía siempre. El conocimiento forzado por el caos es el que más dura.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linux a las 3am y el primer servidor real
&lt;/h2&gt;

&lt;p&gt;A los 18 el salto fue a web hosting con Linux. Y acá es donde la cosa se pone seria.&lt;/p&gt;

&lt;p&gt;Instalar Linux en 2009 no era como ahora. No había un instalador bonito que te guiaba de la mano. Había particiones que tenías que calcular a mano, había GRUB que si lo instalabas mal te quedabas sin sistema operativo en absolutamente todas las particiones que tenías, había controladores de red que a veces no existían para tu hardware y tenías que compilarlos desde código fuente descargado con la única máquina que sí tenía internet.&lt;/p&gt;

&lt;p&gt;Pero una vez que andaba... era mío. Completamente mío. Un servidor corriendo Apache, MySQL, PHP — el stack LAMP que movía la mitad de internet en esa época. Configurando virtual hosts a las 3am porque ese era el único momento en que podía trabajar sin interrupciones. Mirando logs en tiempo real con &lt;code&gt;tail -f&lt;/code&gt; como si fueran telemetría de una nave espacial.&lt;/p&gt;

&lt;p&gt;Esa sensación — la de tener una máquina real en internet, respondiendo requests de gente real — es algo que nunca se va. Es adictiva. Hoy tengo esa misma sensación cuando hago deploy y veo los logs de Railway actualizarse en tiempo real, pero la primera vez que la sentí tenía 18 años y era con un servidor físico en algún datacenter que nunca llegué a ver en persona.&lt;/p&gt;

&lt;h2&gt;
  
  
  El desvío: Cisco CCNA y la UBA
&lt;/h2&gt;

&lt;p&gt;Hubo un período donde me fui más hacia las redes que hacia el software. Hice la certificación Cisco CCNA — una de esas credenciales que te hacen estudiar routing protocols, spanning tree, VLANs, subnetting hasta que lo soñás — y entré a Ciencias de la Computación en la UBA.&lt;/p&gt;

&lt;p&gt;La UBA me enseñó a pensar. Eso suena a cliché pero es literal. Álgebra, lógica, algoritmos — la parte de la computación que no se aprende tocheando servidores. Aprendí por qué ciertos algoritmos son más eficientes que otros. Aprendí a demostrar cosas. Aprendí que hay una diferencia enorme entre código que funciona y código que es correcto.&lt;/p&gt;

&lt;p&gt;También aprendí que la academia argentina tiene una relación complicada con la tecnología del mundo real. Estábamos estudiando teoría de compiladores mientras afuera el mundo estaba explotando con Node.js y el primer boom de las startups. No me arrepiento — la base teórica vale oro — pero había una desconexión que a veces desesperaba.&lt;/p&gt;

&lt;p&gt;El CCNA, por otro lado, fue brutal en el buen sentido. Estudiar para esa certificación es como meterse en la cabeza de los ingenieros de Cisco y entender por qué internet funciona como funciona. Por qué los paquetes toman ciertas rutas. Por qué falla cuando falla. Ese conocimiento de red de bajo nivel es algo que hoy, cuando debuggeo problemas de conectividad en Docker o configuro reglas de firewall en un VPS, sigo usando constantemente.&lt;/p&gt;

&lt;h2&gt;
  
  
  2020: el pivot que cambió todo
&lt;/h2&gt;

&lt;p&gt;Y llegamos al momento que cambió todo. 2020. Sí, ese año.&lt;/p&gt;

&lt;p&gt;Con el mundo en pausa forzada, yo tomé la decisión de meterme de lleno en desarrollo de software moderno. No como hobby — como carrera principal. Y el mundo que encontré era irreconocible comparado con el PHP que había tocado diez años antes.&lt;/p&gt;

&lt;p&gt;React. TypeScript. Next.js. Docker. PostgreSQL. Un ecosistema completamente diferente, con sus propias convenciones, sus propias peleas internas (¿Redux o Context? ¿REST o GraphQL? ¿tabs o espacios — bueno, esa está resuelta), su propia cultura.&lt;/p&gt;

&lt;p&gt;El primer mes fue humillante. Yo que había configurado servidores Linux, que entendía cómo funciona TCP/IP, que había estudiado algoritmos en la UBA — no podía hacer andar un componente de React sin romper todo. El modelo mental es completamente diferente. El estado reactivo, el ciclo de vida de los componentes, el sistema de tipos de TypeScript que al principio parece un obstáculo y después te das cuenta de que es lo que te salva la vida — todo nuevo.&lt;/p&gt;

&lt;p&gt;Pero acá es donde los treinta años anteriores pagaron dividendos. Cuando algo fallaba, yo sabía leer el error. Cuando había un problema de red en Docker, yo sabía qué estaba pasando. Cuando la base de datos se portaba raro, tenía intuición de dónde buscar. La experiencia acumulada no era transferible directamente, pero creaba un contexto que aceleraba el aprendizaje de manera brutal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Railway, Next.js y el deploy de 2024
&lt;/h2&gt;

&lt;p&gt;Hoy el stack en el que trabajo es Next.js con TypeScript, PostgreSQL para los datos persistentes, Docker para que el ambiente local sea igual al de producción (la promesa eterna que Docker por fin cumplió), y Railway para el deploy.&lt;/p&gt;

&lt;p&gt;Railway merece un párrafo aparte porque representa exactamente el contraste con mis inicios. En 2009, para poner algo en producción, necesitaba: contratar un servidor, configurarlo desde cero con SSH, instalar todo el stack, configurar el dominio, configurar SSL manualmente con Let's Encrypt o pagar un certificado, configurar backups, monitoreo... Días de trabajo para la infraestructura antes de poder desplegar una línea de código de producto.&lt;/p&gt;

&lt;p&gt;Con Railway hoy: &lt;code&gt;railway up&lt;/code&gt;. Listo. En serio. La infraestructura está toda abstraída. PostgreSQL con un click. Variables de entorno en una interfaz. Deploys automáticos desde GitHub. SSL automático. Monitoreo incluido.&lt;/p&gt;

&lt;p&gt;La primera vez que hice un deploy así me quedé mirando la terminal en silencio unos segundos. Pensé en las noches configurando Apache, en los GRUB rotos, en los cyber cafés con calor de diciembre y cables por todos lados. Y pensé: &lt;em&gt;esto es demasiado fácil&lt;/em&gt;. Y después me corregí: no es fácil, es que alguien hizo el trabajo duro por vos.&lt;/p&gt;

&lt;p&gt;Esa es la paradoja de la abstracción tecnológica. Todo se vuelve más accesible, y eso es bueno — significa más gente puede construir cosas. Pero también significa que hay capas de complejidad que se vuelven invisibles, y cuando algo sale mal en esas capas, el desarrollador que no pasó por los servidores físicos no tiene las herramientas mentales para entender qué está pasando.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué importa de dónde venís
&lt;/h2&gt;

&lt;p&gt;Soy un producto raro: demasiado joven para haber vivido los mainframes, demasiado viejo para haber empezado con smartphones y tutoriales de YouTube. Caí justo en el medio de la transición más grande de la historia de la computación personal.&lt;/p&gt;

&lt;p&gt;Y eso me dio algo que valoro cada vez más: contexto histórico. Sé por qué las cosas son como son. Sé por qué Docker existe — porque el "funciona en mi máquina" es un problema real que yo viví. Sé por qué TypeScript existe — porque JavaScript a escala es una pesadilla de mantenimiento que yo también viví. Sé por qué los servicios cloud existen — porque la alternativa era lo que yo hacía a los 18.&lt;/p&gt;

&lt;p&gt;Esta historia — la historia programador argentino que creció con la tecnología en tiempo real — no es nostalgia. Es contexto. Y el contexto es lo que separa a alguien que usa herramientas de alguien que las entiende.&lt;/p&gt;

&lt;p&gt;La Amiga 500 de 1994 y el Railway de 2024 son el mismo continuo. Cambió todo y no cambió nada: sigue siendo sobre hacer que las máquinas hagan lo que vos querés que hagan. Sigue siendo sobre entender qué pasa cuando algo falla. Sigue siendo sobre esa sensación — adictiva, visceral, única — de ver algo que construiste funcionando en el mundo real.&lt;/p&gt;

&lt;p&gt;Tres años. Una Amiga. Treinta y uno después, acá sigo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/de-dos-a-cloud-mi-viaje-33-anos-1775496952619" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>desarrolloweb</category>
      <category>nextjs</category>
      <category>fullstack</category>
      <category>linux</category>
    </item>
    <item>
      <title>De 3 segundos a 300ms: cómo optimicé el performance de una app Next.js en producción</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:29:49 +0000</pubDate>
      <link>https://forem.com/jtorchia/de-3-segundos-a-300ms-como-optimice-el-performance-de-una-app-nextjs-en-produccion-3ceh</link>
      <guid>https://forem.com/jtorchia/de-3-segundos-a-300ms-como-optimice-el-performance-de-una-app-nextjs-en-produccion-3ceh</guid>
      <description>&lt;p&gt;Hay un momento específico en la vida de un desarrollador donde te das cuenta que rompiste algo. No con un error. Con silencio. Con lentitud. Con ese spinner que gira y gira mientras el usuario se pregunta si tu app está viva o ya murió.&lt;/p&gt;

&lt;p&gt;Me pasó en producción. Una app Next.js que habíamos lanzado con orgullo estaba tardando &lt;strong&gt;entre 2.8 y 3.4 segundos&lt;/strong&gt; en el First Contentful Paint. En mobile, peor. El LCP rondaba los 4 segundos. Google Lighthouse me miraba con cara de asco y yo no tenía excusas — era mi código, mis decisiones, mi problema.&lt;/p&gt;

&lt;p&gt;Este es el relato de cómo diagnostiqué el desastre, qué cambié, y cómo llegué a &lt;strong&gt;300ms de FCP en producción&lt;/strong&gt;. Sin bullshit, sin "simplemente usá un CDN", con el trabajo sucio que nadie muestra en los tutoriales.&lt;/p&gt;

&lt;h2&gt;
  
  
  El diagnóstico: primero entendé qué está ardiendo
&lt;/h2&gt;

&lt;p&gt;Antes de tocar una sola línea de código, necesitás saber qué está lento. Yo cometí el error clásico: asumir. "Seguro es el bundle", pensé. Spoiler: no era solo el bundle.&lt;/p&gt;

&lt;p&gt;Las herramientas que usé:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lighthouse&lt;/strong&gt; en modo incógnito (sin extensiones que contaminen los resultados)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome DevTools → Network tab&lt;/strong&gt; con throttling a "Fast 3G"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Analytics&lt;/strong&gt; para datos reales de usuarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;next build&lt;/code&gt; con &lt;code&gt;ANALYZE=true&lt;/code&gt;&lt;/strong&gt; para ver el bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para el bundle analyzer, instalé esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @next/bundle-analyzer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y en &lt;code&gt;next.config.js&lt;/code&gt;:&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;withBundleAnalyzer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@next/bundle-analyzer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)({&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANALYZE&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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="cm"&gt;/** @type {import('next').NextConfig} */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// tu config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withBundleAnalyzer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después corrés:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ANALYZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y ahí fue cuando vi el horror. Tenía &lt;strong&gt;moment.js&lt;/strong&gt; importado completo — 67kb gzipped — para formatear dos fechas en toda la app. Tenía una librería de gráficos cargando en el bundle principal cuando solo aparecía en una página de dashboard. Tenía componentes que fetcheaban datos en el cliente cuando perfectamente podían ser Server Components.&lt;/p&gt;

&lt;p&gt;El diagnóstico real mostró tres problemas grandes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bundle de cliente inflado con dependencias innecesarias&lt;/li&gt;
&lt;li&gt;Waterfall de requests en el cliente (fetch tras fetch, en cadena)&lt;/li&gt;
&lt;li&gt;Imágenes sin optimizar y sin tamaño declarado (layout shift asesino)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problema 1: el bundle era un desastre
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bye bye moment.js
&lt;/h3&gt;

&lt;p&gt;Reemplacé moment.js con &lt;code&gt;date-fns&lt;/code&gt; usando imports específicos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Antes — importaba todo moment&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;moment&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;moment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fecha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DD/MM/YYYY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Después — solo lo que necesito&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;format&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;date-fns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;es&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;date-fns/locale&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fecha&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dd/MM/yyyy&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;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resultado: -67kb gzipped del bundle principal. Sí, así de ridículo era.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dynamic imports para lo que no se ve al inicio
&lt;/h3&gt;

&lt;p&gt;El gráfico de dashboard no debería estar en el bundle de la página de inicio. Dynamic import con &lt;code&gt;next/dynamic&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dynamic&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;next/dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ❌ Antes&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RevenueChart&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;@/components/RevenueChart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Después&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RevenueChart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/RevenueChart&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;loading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ChartSkeleton&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;// este componente usa window, no puede hacer SSR&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;Esto sacó ~45kb del bundle inicial y el usuario ve el skeleton mientras carga — mucho mejor UX que ver nada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problema 2: el waterfall de fetches en el cliente
&lt;/h2&gt;

&lt;p&gt;Acá estaba el problema más gordo. Tenía una página de perfil de usuario que hacía esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ El horror — cada fetch espera al anterior&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProfilePage&lt;/span&gt; &lt;span class="o"&gt;=&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPosts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/user&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Espera al user para fetchear posts&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/posts?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Espera a posts para fetchear stats&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/stats?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&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;Tres requests en cadena. Esperaba request 1 para lanzar request 2. Esperaba request 2 para lanzar request 3. En una conexión normal eso son 800ms de overhead puro.&lt;/p&gt;

&lt;p&gt;La solución en dos pasos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Paso 1: Paralizar lo que se puede paralelizar&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si tenés el userId desde el principio (por ejemplo, de la sesión), no necesitás esperar a que llegue el user para pedir sus posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Paralelo cuando es posible&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProfilePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="nf"&gt;useEffect&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/user/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/posts?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/stats?userId=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&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="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;setPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nf"&gt;setStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&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;userId&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;&lt;strong&gt;Paso 2: Moverlo al servidor con Server Components (la solución real)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pero la solución de verdad era dejar de fetchear en el cliente. Con el App Router de Next.js 13+, esto se convierte en:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/perfil/[userId]/page.tsx&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Server Component — todo en el servidor, en paralelo&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getUserData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getUserPosts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getUserStats&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;@/lib/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProfilePage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; 
  &lt;span class="nx"&gt;params&lt;/span&gt; 
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Paralelo en el servidor — no hay waterfall, no hay round trip al cliente&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;]&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;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;getUserPosts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;getUserStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserHeader&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StatsBar&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PostsList&lt;/span&gt; &lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;Esto eliminó completamente el round trip cliente → servidor para el data fetching inicial. El HTML llega al navegador ya con los datos adentro. El tiempo de esos tres fetches dejó de contar para el usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problema 3: las imágenes me estaban matando
&lt;/h2&gt;

&lt;p&gt;Tenía imágenes con &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; nativo en vez de &lt;code&gt;next/image&lt;/code&gt;. Sin width/height declarados. Sin lazy loading inteligente. El Cumulative Layout Shift era de 0.34 — Google te odia si superás 0.1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Layout shift garantizado&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;span class="c1"&gt;// ✅ Next.js Image con todo configurado&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Image&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;next/image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Image&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rounded-full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// true solo para imágenes above the fold&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para las imágenes hero (above the fold), usé &lt;code&gt;priority={true}&lt;/code&gt; para que Next.js las precargue. Para todo lo demás, lazy loading automático.&lt;/p&gt;

&lt;p&gt;También configuré los dominios permitidos en &lt;code&gt;next.config.js&lt;/code&gt;:&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&lt;/span&gt;&lt;span class="p"&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;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;storage.googleapis.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mi-bucket/**&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="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/avif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/webp&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next.js convierte automáticamente a WebP/AVIF según lo que soporte el browser. Mis imágenes de 800kb bajaron a 120kb en WebP.&lt;/p&gt;

&lt;h2&gt;
  
  
  El toque final: caching agresivo
&lt;/h2&gt;

&lt;p&gt;Venía cacheando prácticamente nada. Las rutas del App Router tienen cache por defecto, pero yo lo estaba rompiendo sin querer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Esto desactiva el cache estático&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dynamic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-dynamic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Revalidación cada 60 segundos — fresco pero cacheado&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;revalidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para el fetch dentro de Server Components, usé las opciones de cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cache con revalidación por tiempo&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ejemplo.com/data&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;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;revalidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// 1 hora&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Cache estático (no cambia nunca hasta el próximo deploy)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ejemplo.com/config&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;force-cache&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Sin cache (datos en tiempo real)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;liveData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ejemplo.com/live&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;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-store&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;h2&gt;
  
  
  Los resultados reales
&lt;/h2&gt;

&lt;p&gt;Una semana después del deploy con todos los cambios, los números de Vercel Analytics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Antes&lt;/th&gt;
&lt;th&gt;Después&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FCP (p75)&lt;/td&gt;
&lt;td&gt;3.1s&lt;/td&gt;
&lt;td&gt;310ms&lt;/td&gt;
&lt;td&gt;-90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LCP (p75)&lt;/td&gt;
&lt;td&gt;4.2s&lt;/td&gt;
&lt;td&gt;820ms&lt;/td&gt;
&lt;td&gt;-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLS&lt;/td&gt;
&lt;td&gt;0.34&lt;/td&gt;
&lt;td&gt;0.02&lt;/td&gt;
&lt;td&gt;-94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;487kb&lt;/td&gt;
&lt;td&gt;198kb&lt;/td&gt;
&lt;td&gt;-59%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTFB&lt;/td&gt;
&lt;td&gt;890ms&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;td&gt;-80%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El score de Lighthouse pasó de 42 a 91. En mobile, de 31 a 84.&lt;/p&gt;

&lt;p&gt;Lo que más impactó, en orden:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Server Components eliminando el client waterfall (40% de la mejora)&lt;/li&gt;
&lt;li&gt;Bundle splitting y eliminación de dependencias pesadas (30%)&lt;/li&gt;
&lt;li&gt;Optimización de imágenes (20%)&lt;/li&gt;
&lt;li&gt;Caching (10%)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lo que aprendí — y lo que hubiera hecho diferente
&lt;/h2&gt;

&lt;p&gt;El error fundamental fue no medir desde el principio. Desarrollé meses asumiendo que "estaba bien" y recién en producción con usuarios reales vi el desastre. Ahora tengo Lighthouse en el CI/CD que falla el build si el score baja de 80.&lt;/p&gt;

&lt;p&gt;También aprendí que &lt;strong&gt;optimizar performance no es un sprint, es una mentalidad&lt;/strong&gt;. Cada dependencia que agregás tiene un costo. Cada fetch en el cliente tiene un costo. Cada imagen sin dimensiones tiene un costo. El costo se paga después, con usuarios frustrados y SEO en el piso.&lt;/p&gt;

&lt;p&gt;La optimización de performance en Next.js no es magia — es diagnóstico honesto, decisiones conservadoras con las dependencias, y aprovechar las herramientas que ya tenés. Los Server Components existen para esto. El Image component existe para esto. El bundle analyzer existe para esto.&lt;/p&gt;

&lt;p&gt;Usalos antes de que el Lighthouse te grite.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/optimizacion-performance-nextjs-3s-a-300ms" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>performance</category>
      <category>nextjs</category>
      <category>servercomponents</category>
    </item>
    <item>
      <title>pnpm vs npm vs yarn vs bun: la comparativa definitiva que nadie te va a dar en 2025</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:29:00 +0000</pubDate>
      <link>https://forem.com/jtorchia/pnpm-vs-npm-vs-yarn-vs-bun-la-comparativa-definitiva-que-nadie-te-va-a-dar-en-2025-4nlf</link>
      <guid>https://forem.com/jtorchia/pnpm-vs-npm-vs-yarn-vs-bun-la-comparativa-definitiva-que-nadie-te-va-a-dar-en-2025-4nlf</guid>
      <description>&lt;p&gt;Hay decisiones en el desarrollo de software que parecen triviales hasta que te explotan en la cara. Qué package manager usar es una de esas. Yo las pagué todas: proyectos con node_modules de 4GB, deploys que fallaban por conflictos de versiones que "no deberían existir", monorepos que tardaban 8 minutos en instalarse en CI. Todo eso me hizo obsesionarme con este tema.&lt;/p&gt;

&lt;p&gt;Así que vamos directo al hueso: &lt;strong&gt;pnpm vs npm vs yarn vs bun&lt;/strong&gt; — qué son, qué hacen diferente, cuándo usar cada uno, y cuál ganó mi corazón (y mi &lt;code&gt;.zshrc&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Un poco de historia para que entiendas por qué existe este caos
&lt;/h2&gt;

&lt;p&gt;npm llegó en 2010 pegado a Node.js. Era la única opción y, francamente, era un desastre. El &lt;code&gt;node_modules&lt;/code&gt; flat que conocemos hoy ni existía — en las versiones viejas tenías árbol anidado infinito, carpetas dentro de carpetas que llegaban a rutas tan largas que Windows directamente se rendía. En serio.&lt;/p&gt;

&lt;p&gt;Yarn apareció en 2016, creado por Facebook (ahora Meta) en colaboración con Google y otros. Fue una bocanada de aire fresco: instalaciones paralelas, lockfile determinístico, cache local. npm tardó años en ponerse al día.&lt;/p&gt;

&lt;p&gt;pnpm llegó también por esa época pero tardó más en tomar tracción. Y Bun... Bun es el newcomer que llegó en 2023 a decir que todos los demás son lentos y tiene cierta razón.&lt;/p&gt;




&lt;h2&gt;
  
  
  npm: el que ya tenés instalado
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lo bueno:&lt;/strong&gt; Viene con Node. Sin instalación extra, sin explicarle nada a nadie. Para un proyecto pequeño o para onboardear a alguien nuevo es imbatible en simplicidad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo malo:&lt;/strong&gt; Sigue siendo el más lento de los cuatro en instalaciones en frío. El &lt;code&gt;node_modules&lt;/code&gt; es un monstruo flat que duplica paquetes alegremente. En un monorepo mediano vi node_modules llegar a &lt;strong&gt;3.8GB&lt;/strong&gt;. Eso no es normal. Eso es un problema.&lt;/p&gt;

&lt;p&gt;La versión 7 trajo workspaces, la 8 mejoró bastante el performance, y npm hoy en día es decente. Pero "decente" no es suficiente cuando existían mejores alternativas hace años.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi experiencia real:&lt;/strong&gt; En 2021 arranqué un proyecto con npm por defecto. A los tres meses el CI tardaba 6 minutos solo en el &lt;code&gt;npm install&lt;/code&gt;. Migré a pnpm en un viernes a la tarde (error mío, nunca migrés nada importante un viernes) y el lunes el CI tardaba 90 segundos. Eso me marcó.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt; Cuando el proyecto es chico, cuando el equipo no quiere fricción de setup, o cuando trabajás con herramientas que tienen bugs conocidos con pnpm (sí, existen, aunque cada vez menos).&lt;/p&gt;




&lt;h2&gt;
  
  
  Yarn: el que prometía mucho y se complicó
&lt;/h2&gt;

&lt;p&gt;Yarn clásico (v1) fue revolucionario en su momento. Lockfile determinístico, caché que realmente funcionaba, parallelismo. npm tardó literalmente años en igualar esas features.&lt;/p&gt;

&lt;p&gt;Pero después llegó &lt;strong&gt;Yarn Berry (v2 en adelante)&lt;/strong&gt; y todo se complicó. Introdujeron &lt;strong&gt;Plug'n'Play (PnP)&lt;/strong&gt;: en lugar de node_modules, un sistema de resolución propio donde los paquetes están en archivos &lt;code&gt;.zip&lt;/code&gt; y un loader los resuelve en runtime. La teoría es preciosa. La práctica es otro tema.&lt;/p&gt;

&lt;p&gt;Probé Yarn Berry en un proyecto Next.js y fue una tarde entera de debuggear por qué ciertos paquetes no levantaban. Algunos tools del ecosistema simplemente no entienden el modelo PnP. Terminé en &lt;code&gt;nodeLinker: node-modules&lt;/code&gt; en el &lt;code&gt;.yarnrc.yml&lt;/code&gt;, que básicamente es usar Yarn Berry actuando como Yarn clásico. ¿Para qué, entonces?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo bueno de Yarn:&lt;/strong&gt; La DX cuando funciona es muy linda. Los workspaces de Yarn son muy maduros. El equipo de Yarn lleva años puliendo esto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo malo:&lt;/strong&gt; La fragmentación entre v1 y v2/v3/v4 es un quilombo. Si buscás un error en Stack Overflow, el 60% de las respuestas son para la versión equivocada. Y PnP, aunque brillante conceptualmente, genera fricción real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt; Yarn clásico (v1) en proyectos legados que ya lo usan y no vale la pena migrar. Yarn Berry si estás dispuesto a invertir tiempo en entender PnP y tu equipo está alineado.&lt;/p&gt;




&lt;h2&gt;
  
  
  pnpm: mi favorito sin discusión
&lt;/h2&gt;

&lt;p&gt;Acá es donde me pongo intenso, así que aguantame.&lt;/p&gt;

&lt;p&gt;pnpm resuelve el problema fundamental del package management de Node de una manera elegante: &lt;strong&gt;el content-addressable store&lt;/strong&gt;. En lugar de copiar paquetes en cada &lt;code&gt;node_modules&lt;/code&gt;, usa &lt;strong&gt;hard links&lt;/strong&gt; a un store global en tu máquina. &lt;code&gt;lodash&lt;/code&gt; instalado en 47 proyectos distintos ocupa espacio de disco una sola vez. El &lt;code&gt;node_modules&lt;/code&gt; de cada proyecto son mayormente symlinks y hard links.&lt;/p&gt;

&lt;p&gt;Esto tiene consecuencias reales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Velocidad:&lt;/strong&gt; Primera instalación comparable a npm/yarn. Segunda instalación y en adelante: ridículamente rápida.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Espacio en disco:&lt;/strong&gt; Tengo quizás 200 proyectos en esta máquina. Si usara npm serían cientos de GB. Con pnpm el store global ocupa ~15GB para todo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correctitud:&lt;/strong&gt; pnpm es &lt;strong&gt;estricto con las dependencias&lt;/strong&gt;. No podés acceder a un paquete que no declaraste en tu &lt;code&gt;package.json&lt;/code&gt;. Esto parece una molestia hasta que te das cuenta de que npm/yarn te dejan acceder a dependencias transitivas silenciosamente — una bomba de tiempo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Los workspaces de pnpm&lt;/strong&gt; son los mejores del ecosistema, punto. El archivo &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; es simple, el hoisting configurable, y el comando &lt;code&gt;pnpm -r&lt;/code&gt; para correr scripts en todos los packages es una joya.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mi experiencia real:&lt;/strong&gt; Hoy uso pnpm en absolutamente todos mis proyectos personales y profesionales. El comando que más escribo después de &lt;code&gt;git&lt;/code&gt; es probablemente &lt;code&gt;pnpm install&lt;/code&gt;. Tuve exactamente un problema de compatibilidad en dos años: una librería vieja que asumía hoisting de npm. Lo resolví en 10 minutos con &lt;code&gt;.npmrc&lt;/code&gt; ajustado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo malo de pnpm:&lt;/strong&gt; La instalación inicial es un paso extra. Algunos proyectos open source con configs de npm legacy pueden dar dolores de cabeza. Y el store global puede crecer mucho si no hacés &lt;code&gt;pnpm store prune&lt;/code&gt; de vez en cuando (yo lo tengo en un cron).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt; Casi siempre. Especialmente en monorepos. Especialmente si te importa el espacio en disco. Especialmente si querés que tus dependencias estén correctamente declaradas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bun: el que llegó a romper todo
&lt;/h2&gt;

&lt;p&gt;Bun no es solo un package manager — es un runtime completo (reemplazo de Node), un bundler, un test runner, y un transpiler. Pero acá hablamos de su faceta como package manager.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Los números:&lt;/strong&gt; Bun install es absurdamente rápido. Hablo de instalaciones en frío de proyectos medianos en &lt;strong&gt;2-3 segundos&lt;/strong&gt;. Lo que npm hace en 45 segundos, Bun lo hace en 3. Está escrito en Zig, usa su propio runtime, y tiene un caché binario que hace que las instalaciones repetidas sean casi instantáneas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lo probé en un proyecto Next.js&lt;/strong&gt; y funcionó perfecto. El lockfile (&lt;code&gt;bun.lockb&lt;/code&gt;) es binario, lo cual es raro conceptualmente pero funciona. Los workspaces están soportados. La compatibilidad con el ecosistema npm es muy buena.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pero acá viene mi hesitación:&lt;/strong&gt; Bun como runtime todavía tiene edge cases donde el comportamiento difiere de Node. Si usás Bun solo como package manager (con Node como runtime), eso desaparece — pero entonces estás instalando un tool enorme para usar solo una fracción.&lt;/p&gt;

&lt;p&gt;El ecosistema de Bun maduró muchísimo en 2024. Para 2025 es una opción real y seria. Pero yo todavía no migré mis proyectos productivos porque el riesgo/beneficio no cierra para mí. La velocidad de pnpm con caché es más que suficiente, y el modelo mental de Bun como "todo en uno" todavía me genera incertidumbre en deployments complejos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usarlo:&lt;/strong&gt; Si arrancás un proyecto nuevo y querés experimentar. Si el performance de instalación es crítico para vos (¿monorepo enorme en CI sin caché?). Si sos el tipo de persona que le gusta estar en la vanguardia y bancarse algún rough edge.&lt;/p&gt;




&lt;h2&gt;
  
  
  La tabla que todos quieren
&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;npm&lt;/th&gt;
&lt;th&gt;Yarn&lt;/th&gt;
&lt;th&gt;pnpm&lt;/th&gt;
&lt;th&gt;Bun&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Velocidad (frío)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lento&lt;/td&gt;
&lt;td&gt;Medio&lt;/td&gt;
&lt;td&gt;Rápido&lt;/td&gt;
&lt;td&gt;Muy rápido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Velocidad (caché)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medio&lt;/td&gt;
&lt;td&gt;Rápido&lt;/td&gt;
&lt;td&gt;Muy rápido&lt;/td&gt;
&lt;td&gt;Muy rápido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Espacio en disco&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Mucho&lt;/td&gt;
&lt;td&gt;Mucho&lt;/td&gt;
&lt;td&gt;Poco&lt;/td&gt;
&lt;td&gt;Poco&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monorepos&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Básico&lt;/td&gt;
&lt;td&gt;Bueno&lt;/td&gt;
&lt;td&gt;Excelente&lt;/td&gt;
&lt;td&gt;Bueno&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Madurez&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;Media&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compatibilidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Total&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;td&gt;Alta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Strictness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Sí&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Mi recomendación final, sin rodeos
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;¿Proyecto nuevo, 2025?&lt;/strong&gt; → pnpm. Sin dudarlo. La curva de aprendizaje es mínima (es casi idéntico a npm en la interfaz), los beneficios son inmediatos y reales, y el soporte del ecosistema es excelente.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Monorepo?&lt;/strong&gt; → pnpm con workspaces. Es la mejor opción disponible hoy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Querés vivir en el futuro?&lt;/strong&gt; → Bun. Pero sabé que vas a ser el beta tester de algunas cosas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Proyecto legado que ya tiene yarn.lock o package-lock.json?&lt;/strong&gt; → No migrés por migrar. El lockfile es contrato. Si no tenés un motivo real de performance o correctitud, dejalo como está.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm puro en 2025 sin una razón específica:&lt;/strong&gt; No. Ya no. Es como usar jQuery en un proyecto nuevo — funciona, pero hay mejores opciones y vos lo sabés.&lt;/p&gt;

&lt;p&gt;El ecosistema JavaScript tiene sus mil problemas, pero en package management llegamos a un punto donde tenemos opciones genuinamente buenas. Aprovechalo. Yo tardé demasiado en salir de npm por inercia, y esos 6 minutos de CI en 2021 me los voy a cobrar de alguna forma.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/pnpm-vs-npm-vs-yarn-vs-bun-comparativa-2025" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pnpm</category>
      <category>npm</category>
      <category>yarn</category>
      <category>bunjs</category>
    </item>
    <item>
      <title>Cómo construí juanchi.dev con el stack más bleeding edge de 2025: Next.js 16, React 19, Tailwind v4 y Railway</title>
      <dc:creator>Juan Torchia</dc:creator>
      <pubDate>Tue, 07 Apr 2026 00:28:36 +0000</pubDate>
      <link>https://forem.com/jtorchia/como-construi-juanchidev-con-el-stack-mas-bleeding-edge-de-2025-nextjs-16-react-19-tailwind-v4-3eef</link>
      <guid>https://forem.com/jtorchia/como-construi-juanchidev-con-el-stack-mas-bleeding-edge-de-2025-nextjs-16-react-19-tailwind-v4-3eef</guid>
      <description>&lt;p&gt;Hay una pregunta que me hice durante meses antes de arrancar con juanchi.dev: ¿uso el stack probado o me tiro de cabeza con lo más nuevo y aguanto los golpes?&lt;/p&gt;

&lt;p&gt;Elegí los golpes. Siempre elijo los golpes.&lt;/p&gt;

&lt;p&gt;Esto es lo que pasó cuando intenté montar un &lt;strong&gt;portfolio de desarrollador con Next.js 16, React 19, Tailwind v4 y Railway&lt;/strong&gt; en producción — con todo lo que salió mal documentado en tiempo real, porque alguien tiene que hacerlo.&lt;/p&gt;




&lt;h2&gt;
  
  
  El setup inicial: la arrogancia de los primeros 20 minutos
&lt;/h2&gt;

&lt;p&gt;Empecé con confianza de CEO de startup imaginaria. Tres comandos y ya:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app@latest juanchi-dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--typescript&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tailwind&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--eslint&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--src-dir&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--import-alias&lt;/span&gt; &lt;span class="s2"&gt;"@/*"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bien. Proyecto andando. Tailwind v4 instalado automáticamente porque usé el flag correspondiente. Acá fue cuando me di cuenta que v4 no tiene &lt;code&gt;tailwind.config.js&lt;/code&gt; por defecto — toda la configuración vive en el CSS directamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--font-family-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter Variable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;62%&lt;/span&gt; &lt;span class="m"&gt;0.25&lt;/span&gt; &lt;span class="m"&gt;240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--color-brand-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;45%&lt;/span&gt; &lt;span class="m"&gt;0.25&lt;/span&gt; &lt;span class="m"&gt;240&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;--breakpoint-xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20rem&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;Esto es raro al principio. Muy raro. Durante dos horas busqué dónde meter mi &lt;code&gt;extend&lt;/code&gt; de colores personalizados hasta que leí la documentación de verdad. Con v4, el archivo CSS &lt;em&gt;es&lt;/em&gt; la configuración. Una vez que lo internalizás, es hermoso. Hasta entonces, duele.&lt;/p&gt;




&lt;h2&gt;
  
  
  React 19 y los Server Components: amigos con beneficios que te complican la vida
&lt;/h2&gt;

&lt;p&gt;La idea era simple: portfolio estático en su mayoría, con algunas partes dinámicas. Uso de Server Components para todo lo que pueda, Client Components solo donde necesito interactividad.&lt;/p&gt;

&lt;p&gt;Acá está la estructura que terminé usando:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  app/
    page.tsx          → Server Component (hero + about)
    projects/
      page.tsx        → Server Component (fetch de proyectos)
      [slug]/
        page.tsx      → Server Component (detalle del proyecto)
    blog/
      page.tsx        → Server Component
    contact/
      page.tsx        → mezcla de los dos mundos
  components/
    ui/               → Client Components (animaciones, forms)
    server/           → Server Components (cards, layouts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El problema vino con las animaciones. Quería ese efecto de entrada donde cada sección aparece al hacer scroll. Usé &lt;code&gt;framer-motion&lt;/code&gt; y el compilador me mandó directo al carajo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: useState can only be used in a Client Component.
Add the "use client" directive at the top of the file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claro. &lt;code&gt;framer-motion&lt;/code&gt; necesita el DOM. Solución: wrapper client-side que envuelve los Server Components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// components/ui/animated-section.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;motion&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;framer-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AnimatedSectionProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ReactNode&lt;/span&gt;
  &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AnimatedSection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;AnimatedSectionProps&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="nt"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
      &lt;span class="na"&gt;initial&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="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;whileInView&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="na"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;viewport&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="na"&gt;once&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;transition&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="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;easeOut&lt;/span&gt;&lt;span class="dl"&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="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;motion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nt"&gt;div&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;Y en el Server Component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/page.tsx (Server Component)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AnimatedSection&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;@/components/ui/animated-section&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HeroContent&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;@/components/server/hero-content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;HomePage&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="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AnimatedSection&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;HeroContent&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AnimatedSection&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&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 patrón de "wrapper client, contenido server" es la clave. Lo entendí tarde, pero lo entendí.&lt;/p&gt;




&lt;h2&gt;
  
  
  El sistema de proyectos: MDX + generación estática
&lt;/h2&gt;

&lt;p&gt;Para los proyectos decidí usar archivos MDX locales. Sin CMS, sin base de datos, sin dependencias externas para el contenido. Los archivos viven en el repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/projects.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&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;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;matter&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;gray-matter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectsDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content/projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Project&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;stack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;liveUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;repoUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;featured&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAllProjects&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Project&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.mdx&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.mdx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&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;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;matter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&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="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&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;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&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;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;stack&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;stack&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="na"&gt;year&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;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;liveUrl&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;liveUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;repoUrl&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;repoUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;featured&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;featured&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;content&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProjectBySlug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getAllProjects&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto funciona perfecto en local. En Railway empezó el drama.&lt;/p&gt;




&lt;h2&gt;
  
  
  Railway: el deployment que casi me quiebra
&lt;/h2&gt;

&lt;p&gt;Railway es mi plataforma de hosting favorita para proyectos propios. Precio razonable, DX excelente, deploys desde GitHub automáticos. Pero con Next.js 16 hay que tener cuidado con algo: el &lt;strong&gt;output mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Por defecto, Next.js genera un bundle que asume que tenés Node.js disponible en runtime. Railway lo maneja bien, pero el &lt;code&gt;fs.readdirSync&lt;/code&gt; que uso para leer los archivos MDX &lt;strong&gt;no funciona si configurás &lt;code&gt;output: 'export'&lt;/code&gt;&lt;/strong&gt; (modo totalmente estático).&lt;/p&gt;

&lt;p&gt;Yo, genio que soy, lo había puesto en &lt;code&gt;output: 'export'&lt;/code&gt; porque quería el deploy más rápido posible. El resultado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: ENOENT: no such file or directory, scandir '/app/content/projects'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El directorio &lt;code&gt;content/&lt;/code&gt; no estaba en el build de producción. El problema era que Railway copiaba el output exportado pero no los archivos fuente. Dos opciones:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cambiar a modo Node.js (server-side rendering real)&lt;/li&gt;
&lt;li&gt;Mantener export pero pre-generar todo en build time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Elegí el modo Node.js porque igual necesitaba el endpoint de contacto con lógica server-side:&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="c1"&gt;// next.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&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;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Sin output: 'export' — modo Node.js&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&lt;/span&gt;&lt;span class="p"&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;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github.com&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;experimental&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;optimizePackageImports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;framer-motion&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lucide-react&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y el &lt;code&gt;railway.toml&lt;/code&gt; que me salvó la vida:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[build]&lt;/span&gt;
&lt;span class="py"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"nixpacks"&lt;/span&gt;
&lt;span class="py"&gt;buildCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"npm run build"&lt;/span&gt;

&lt;span class="nn"&gt;[deploy]&lt;/span&gt;
&lt;span class="py"&gt;startCommand&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"npm run start"&lt;/span&gt;
&lt;span class="py"&gt;healthcheckPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;
&lt;span class="py"&gt;healthcheckTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="py"&gt;restartPolicyType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"on_failure"&lt;/span&gt;
&lt;span class="py"&gt;restartPolicyMaxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="nn"&gt;[[services]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"juanchi-dev"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  El formulario de contacto: Server Actions al rescate
&lt;/h2&gt;

&lt;p&gt;Con Next.js 15+ y React 19, los Server Actions son ciudadanos de primera clase. El formulario de contacto que manda un mail fue el lugar perfecto para usarlos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/contact/actions.ts&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Resend&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;resend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resend&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;Resend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESEND_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ContactSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendContactEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;prevState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormData&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ContactSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Datos inválidos. Revisá los campos.&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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacto@juanchi.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yo@juanchi.dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Nuevo contacto: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`De: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;parsed&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;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error enviando mail:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error al enviar. Probá de nuevo.&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="p"&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 tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/contact/contact-form.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useActionState&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sendContactEmail&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;./actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ContactForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendContactEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col gap-4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Tu nombre"&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border border-neutral-700 bg-neutral-900 px-4 py-3 rounded-lg"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"tu@mail.com"&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border border-neutral-700 bg-neutral-900 px-4 py-3 rounded-lg"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;
        &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"En qué puedo ayudarte..."&lt;/span&gt;
        &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"border border-neutral-700 bg-neutral-900 px-4 py-3 rounded-lg resize-none"&lt;/span&gt;
        &lt;span class="na"&gt;required&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-brand text-white py-3 rounded-lg disabled:opacity-50"&lt;/span&gt;
      &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enviando...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mandar mensaje&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-green-400"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;¡Mensaje enviado!&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-red-400"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&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;&lt;code&gt;useActionState&lt;/code&gt; es el hook nuevo de React 19 que reemplaza al viejo patrón de &lt;code&gt;useFormState&lt;/code&gt; de react-dom. Más limpio, mejor tipado, manejo de pending nativo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que salió mal: el resumen ejecutivo
&lt;/h2&gt;

&lt;p&gt;Para los que llegaron acá directamente desde el título buscando el drama:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tailwind v4 rompió todos mis snippets guardados.&lt;/strong&gt; Las utilities cambiaron sutilmente. &lt;code&gt;text-sm&lt;/code&gt; sigue existiendo pero los valores por defecto son diferentes. Pasé 40 minutos debuggeando un font-size que "se veía raro" hasta que lo medí con DevTools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;framer-motion&lt;/code&gt; con React 19 tuvo un bug de hidratación&lt;/strong&gt; la primera semana. Se resolvió actualizando a &lt;code&gt;framer-motion@12.x&lt;/code&gt;. Lección: cuando usás bleeding edge, los paquetes de terceros se quedan atrás.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Railway construía bien pero el healthcheck fallaba&lt;/strong&gt; porque el servidor tardaba más de 10 segundos en responder al primer request (cold start). Solución: aumentar &lt;code&gt;healthcheckTimeout&lt;/code&gt; a 30 segundos en el &lt;code&gt;railway.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Los tipos de TypeScript de Next.js 16&lt;/strong&gt; para algunos parámetros de layouts y pages cambiaron. &lt;code&gt;params&lt;/code&gt; ahora es una Promise en algunos contextos. Esto me rompió tres archivos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Antes (Next.js 14):&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProjectPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="c1"&gt;// Ahora (Next.js 15/16):&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProjectPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slug&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ¿Lo volvería a hacer?
&lt;/h2&gt;

&lt;p&gt;Sí. Sin dudas.&lt;/p&gt;

&lt;p&gt;Hay algo en trabajar con stack bleeding edge que te fuerza a leer documentación de verdad, a entender por qué las cosas funcionan y no solo cómo. Cuando algo rompe en territorio desconocido no podés copypastear Stack Overflow — tenés que pensar.&lt;/p&gt;

&lt;p&gt;Y el resultado final es un &lt;strong&gt;portfolio de desarrollador con Next.js y Railway&lt;/strong&gt; que carga en menos de 1.2 segundos, tiene Lighthouse a 98/100, corre en producción por menos de 5 dólares al mes, y lo más importante: lo entiendo de punta a punta.&lt;/p&gt;

&lt;p&gt;La tech nueva duele al principio. Después de eso, es una ventaja competitiva.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;El código fuente de juanchi.dev va a estar público en GitHub cuando termine de limpiar los comentarios avergonzantes del proceso. Pronto.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este artículo fue publicado originalmente en &lt;a href="https://juanchi.dev/blog/como-construi-juanchi-dev" rel="noopener noreferrer"&gt;juanchi.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>portfolio</category>
      <category>typescript</category>
      <category>nextjs</category>
    </item>
  </channel>
</rss>
