BLOG · DARUMA schedule 8 min event 1 de julio de 2026

De Astro 4 a Astro 7: migración real y la caza de un fantasma de rendimiento

Documentando la migración de laparadadelbus.com, un portal editorial de música indie con 240 páginas estáticas, WordPress headless como CMS y Tailwind como sistema de diseño. De Astro v4.0.0 a v7.0.3, sin downtime, en una copia local separada del proyecto en producción — y lo que vino después, cuando los números de rendimiento empezaron a no cuadrar entre sí.

Por qué migrar

No fue por features nuevas. Fue por seguridad. Un npm audit rutinario en el proyecto reveló 13 vulnerabilidades de severidad alta en Astro v4: XSS reflejado, bypass de autenticación por doble URL-encoding, SSRF vía header Host, lectura arbitraria de archivos en el servidor de desarrollo. Todas resueltas en versiones posteriores. La línea de comandos de npm lo dejaba claro:

Plaintext

fix available via `npm audit fix --force`
Will install astro@7.0.3, which is a breaking change

Astro v4 a v7 implica tres saltos mayores (v4→v5→v6→v7). La recomendación general —y la que yo misma había dado en sesiones anteriores antes de decidirme— es no saltar de golpe en producción. La solución: clonar el proyecto a una carpeta de pruebas completamente aislada y validar cada paso antes de tocar el sitio real.

Paso 1: aislar el entorno

Bash

git clone <repo> laparadadelbus-astro7-test
cd laparadadelbus-astro7-test
node --version  # mínimo 22.12.0 requerido por Astro v6+

Astro v6 dejó de soportar Node 18 y 20. Si tu entorno de build (local o CI) no cumple el mínimo, esto se resuelve antes de tocar una sola línea de código.

Paso 2: limpiar vulnerabilidades no relacionadas con Astro

Bash

npm audit

El audit mezclaba problemas de Astro con una vulnerabilidad moderada en js-yaml (dependencia transitiva de gray-matter). Separar ambos frentes evita confundir causas:

Bash

npm audit fix          # resuelve js-yaml sin breaking changes
npx @astrojs/upgrade    # gestiona el salto de Astro y sus integraciones oficiales

@astrojs/upgrade detecta automáticamente las versiones compatibles del ecosistema —en este caso, también marcó @astrojs/tailwind para actualizar de v5 a v6.

Paso 3: el obstáculo real — @astrojs/tailwind está deprecado

El intento de instalar @astrojs/tailwind@6.0.2 junto a astro@7.0.3 falló:

Plaintext

npm error peer astro@"^3.0.0 || ^4.0.0 || ^5.0.0" from @astrojs/tailwind@6.0.2

La integración oficial de Tailwind para Astro fue deprecada a partir de Tailwind v4. El camino recomendado ahora es el plugin nativo de Vite:

Bash

npm uninstall @astrojs/tailwind tailwindcss
npm install tailwindcss@latest @tailwindcss/vite@latest
npm install astro@7.0.3

Esto no es solo un cambio de paquete. Tailwind v4 mueve la configuración del tema (colors, fontFamily, etc.) desde tailwind.config.mjs a un bloque @theme directamente en CSS:

JavaScript

/* astro.config.mjs */
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  output: 'static',
  vite: {
    plugins: [tailwindcss()],
  },
});
CSS

/* global.css */
@import "tailwindcss";

@theme {
  --color-lpdb-accent: #B8440E;
  --font-headline: 'Syne', sans-serif;
  /* ... */
}

Lo importante: esto genera las mismas clases de utilidad que ya existían (bg-lpdb-accent, font-headline) sin tocar ni una línea de las plantillas .astro ya escritas. tailwind.config.mjs se elimina del proyecto.

Paso 4: el primer error de compilación

Plaintext

Cannot apply unknown utility class `tag-pill`

Tailwind v4 no resuelve cadenas de @apply sobre clases propias definidas en el mismo archivo. Esto funcionaba en v3:

CSS

.tag-pill { @apply inline-block px-2 py-0.5 text-xs uppercase; }
.tag-accent { @apply tag-pill bg-lpdb-accent text-white; } /* ❌ ya no funciona */

La solución es desenrollar las utilidades explícitamente en cada clase derivada, en lugar de encadenar @apply sobre clases custom:

CSS

.tag-accent {
  @apply inline-block px-2 py-0.5 text-xs uppercase bg-lpdb-accent text-white;
}

No es elegante, pero es la realidad del compilador actual. Si tu sistema de diseño tiene clases compuestas con esta cadena, revísalas todas antes de dar la migración por terminada.

Paso 4.5: el compilador Rust y los template literals anuidos

Un segundo error apareció más adelante, al tocar otras partes del sitio:

Plaintext

[CompilerError] Unexpected token

Sin línea exacta útil en el stack trace. La causa: expresiones JS con ternarios anidados dentro de un class={\…`}` con interpolación:

HTML

<a class={`px-3 py-1 ${i === 0 ? 'bg-black text-white' : 'border-2 border-black'}`}>

El compilador reescrito en Rust es más estricto analizando expresiones complejas dentro de template literals JSX. La solución, calcular la clase en una variable antes del JSX en lugar de anidarla:

JavaScript

{items.map((item, i) => {
  const pillClass = i === 0 ? 'bg-black text-white' : 'border-2 border-black';
  return <a class={`px-3 py-1 ${pillClass}`}>...</a>;
})}

Mismo resultado visual, sintaxis más simple para el parser. Si tu proyecto tiene muchas clases condicionales construidas inline con ternarios anidados, es otro punto a revisar antes de dar la migración por cerrada.

Paso 5: build y validación

Bash

npm run build

240 páginas, 0 errores de compilación. El compilador de Astro v7 está reescrito en Rust y es notablemente más estricto con HTML inválido —tags sin cerrar que antes se autocorregían en silencio ahora producen error en build time. En este proyecto no se encontró ningún caso, pero es el primer sitio a revisar si el build falla en un proyecto con contenido más heterogéneo (HTML embebido desde un CMS, por ejemplo).

Verificación antes de tocar producción:

  • Preview local: npm run preview junto a una revisión visual de la home, ficha de concierto y ficha de noticia.

  • Control de estilos: Comparación de clases reescritas (tag-pill, tag-accent, etc.) contra el sitio en vivo.

  • Consola limpia: Inspección de la consola del navegador en busca de nuevos warnings.

  • Lighthouse precavido: Uso de Lighthouse solo como referencia en preview, ya que las métricas de caché y compresión solo son fiables en producción.

Lo que NO cambió

El .htaccess, los redirects 301 heredados, la lógica de WordPress headless, el sitemap generado vía PowerShell. La migración de Astro es ortogonal a la infraestructura de hosting y SEO ya existente — y así debe ser: cuantas menos variables se muevan a la vez, más fácil es diagnosticar si algo rompe.

Después del deploy: por qué el Lighthouse local mentía

Con la migración ya en producción, el siguiente paso natural fue medir rendimiento. Y aquí apareció la parte más instructiva de todo el proceso: el mismo sitio, sin tocar una línea de código entre mediciones, daba puntuaciones de Lighthouse que oscilaban entre 61 y 97 en ejecuciones consecutivas desde Chrome DevTools local.

La tentación inicial fue perseguir ese número. El árbol de dependencias de red mostraba, en las ejecuciones malas, un TTFB del documento HTML de hasta 2,6 segundos para una página 100% estática pre-generada, y fuentes .woff2 de 14-21 KiB tardando 6-8 segundos en servirse. En las ejecuciones buenas, esos mismos recursos cargaban en 150-300 milisegundos.

La pista que rompió el misterio: tres herramientas independientes —CWV Insights de Search Console (datos de campo real, CrUX), PageSpeed Insights (laboratorio desde infraestructura de Google) y GTmetrix (laboratorio desde su propia red)— daban resultados altos y consistentes entre sí: 100/100/100/100, 97 y grado A respectivamente. Solo Lighthouse ejecutado desde la máquina local, a través de la conexión doméstica hacia el hosting compartido de IONOS, mostraba esa varianza salvaje.

Conclusión: La varianza no estaba en el código ni, aparentemente, en el servidor de forma generalizada —si lo estuviera, también la sufrirían las otras tres herramientas, que hacen peticiones reales al mismo servidor—. Estaba específicamente en la ruta de red entre el punto de medición local y el datacenter en cada momento concreto. Una lección incómoda pero útil: el Lighthouse que cualquiera puede correr gratis desde su navegador es la herramienta menos fiable de las cuatro para sacar una conclusión definitiva sobre el rendimiento real de un sitio, precisamente porque mete la conexión de quien mide como variable no controlada.

Las dos mejoras que sí importaron

Antes de llegar a esa conclusión, hubo dos cambios de código con impacto real y medible en el desglose de LCP, independientemente de la varianza de red:

  1. defer en scripts inline dentro del hero. Un script de búsqueda de ciudades, sin defer, competía por el hilo principal justo en la zona del documento donde vive la imagen de mayor contenido (LCP). Añadir defer bajó el «Retraso de renderizado de elementos» de 1980ms a 1070ms sin tocar nada más.

  2. Quemar el filtro CSS en la imagen en vez de aplicarlo en tiempo de carga. La imagen del hero llevaba grayscale contrast-125 como clases de Tailwind, es decir, filter: grayscale() contrast() aplicado por CSS. El navegador tiene que decodificar la imagen y luego componer ese filtro antes de poder pintarla — coste que se paga en cada carga, para cada visitante. Procesando el efecto directamente en el archivo .webp con Photoshop y sirviendo la imagen ya con el aspecto final, ese coste de composición desapareció. El «Retraso de renderizado de elementos» bajó de 1070ms a 50ms.

Es una lección generalizable: cualquier filter CSS sobre el elemento candidato a LCP (grayscale, blur, contrast, drop-shadow) tiene un coste de composición que se paga en cada visita. Si el efecto es estático —no depende de interacción ni de estado—, casi siempre es más barato quemarlo en el archivo de imagen que aplicarlo en CSS.

Resultado final

npm audit pasó de 13 vulnerabilidades altas a 0. El sitio compila, renderiza y se ve idéntico al anterior, con ajustes tipográficos menores resueltos sobre la marcha. En cifras de rendimiento: CWV Insights con datos de campo real en 100/100/100/100, PageSpeed Insights en 97 con LCP de 2,2 segundos, GTmetrix grado A con LCP de 1,1 segundos.

Si vas a hacer este mismo salto de versiones: no lo hagas directo en producción, no asumas que tus integraciones de terceros van a seguir el ritmo de las versiones mayores de Astro, revisa cualquier @apply encadenado o ternario anidado en clases dinámicas antes de fiarte del primer build verde, y cuando llegue la hora de medir rendimiento, no persigas el número de un Lighthouse local sin antes confirmarlo contra datos de campo real o al menos dos herramientas de laboratorio más estables.

Sigue leyendo

¿Le echamos un ojo a tu web?

Cuéntame tu caso y te digo, sin compromiso, en qué grupo estás.

ESCRÍBEME