Saltar al contenido principal
Intermedio AstroReactCódigo

Pasé Mi Web de React a Astro: Estos Son los Resultados de Rendimiento (Lighthouse)

Migré una web de React/Next.js a Astro y medí antes y después con Lighthouse. Los números hablan: de 62 a 98 en Performance. Cómo lo hice, qué problemas tuve y si merece la pena.

Fran Cobos 7 min de lectura 1246 palabras

Tabla de contenidos

Tenía una web personal hecha con Next.js 14 (App Router, React Server Components, el stack más moderno del momento). Funcionaba bien, se veía bien. Pero cada vez que abría Lighthouse, los números me daban vergüenza:

Performance:      62
Accessibility:    89
Best Practices:   95
SEO:              91

62 en Performance para un blog con 15 páginas. Un blog. Sin base de datos en tiempo real, sin interactividad compleja, sin nada que justifique esa puntuación.

El problema era claro: estaba enviando 187KB de JavaScript al navegador para una web que era básicamente texto e imágenes.

Los números: antes (React) vs después (Astro)

MétricaReact/Next.jsAstroMejora
Lighthouse Performance6298+58%
JavaScript enviado187 KB12 KB-93.6%
First Contentful Paint2.1s0.6s-71%
Largest Contentful Paint3.8s1.1s-71%
Time to Interactive4.2s0.8s-81%
Cumulative Layout Shift0.120.01-92%
Total Blocking Time890ms40ms-95%
Tamaño HTML (home)42 KB18 KB-57%

No es que Astro sea mágico. Es que React estaba enviando el framework completo, el runtime de React, el runtime de Next.js, los chunks de páginas pre-cargadas, y el JavaScript de hidratación… para una web que no necesitaba nada de eso.

¿Por qué mi web React era lenta?

Abrí el Network tab y analicé el JavaScript:

react-dom.production.min.js:    42 KB
next/dist/chunks/framework:     38 KB
next/dist/chunks/main:          28 KB
next/dist/chunks/pages/_app:    15 KB
next/dist/chunks/webpack:        8 KB
Página actual + componentes:    56 KB
─────────────────────────────────────
Total:                          187 KB

De esos 187 KB, 130 KB eran el framework (React + Next.js runtime). Solo 56 KB eran mi código real. Y la mayor parte de mi código eran componentes que renderizaban HTML estático — no tenían estado, no tenían efectos, no tenían interactividad.

React necesita ese runtime para la hidratación: recorre todo el DOM, adjunta los event listeners, y sincroniza el HTML del servidor con el virtual DOM del cliente. Para un blog, eso es como usar un camión de 18 ruedas para ir a comprar el pan.

Cómo hice la migración (paso a paso)

Paso 1: Crear el proyecto Astro

npm create astro@latest blog-astro
# Elegí: Empty, TypeScript Strict, Install dependencies

Paso 2: Instalar las integraciones necesarias

npx astro add tailwind
npx astro add mdx       # Si usas MDX, si es .md puro no hace falta
npx astro add sitemap

Paso 3: Convertir páginas de JSX a .astro

El cambio más grande fue la sintaxis. Astro usa .astro files que mezclan frontmatter (lógica del servidor) con HTML:

// ❌ Antes (Next.js) — page.tsx
import { getAllPosts } from '@/lib/posts';
import { PostCard } from '@/components/PostCard';

export default async function BlogPage() {
  const posts = await getAllPosts();

  return (
    <main className="max-w-3xl mx-auto px-4 py-12">
      <h1 className="text-4xl font-bold">Blog</h1>
      <div className="mt-8 space-y-6">
        {posts.map(post => (
          <PostCard key={post.slug} post={post} />
        ))}
      </div>
    </main>
  );
}
---
// ✅ Después (Astro) — index.astro
import { getCollection } from 'astro:content';
import BlogLayout from '../layouts/BlogLayout.astro';
import PostCard from '../components/PostCard.astro';

const posts = await getCollection('posts');
const sortedPosts = posts.sort((a, b) =>
  new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
---

<BlogLayout title="Blog">
  <h1 class="text-4xl font-bold">Blog</h1>
  <div class="mt-8 space-y-6">
    {sortedPosts.map(post => (
      <PostCard post={post} />
    ))}
  </div>
</BlogLayout>

Los cambios principales:

  • classNameclass
  • La lógica va en el frontmatter (---)
  • No hay export default function — el HTML va directamente
  • key no es necesario en .astro (no hay virtual DOM)
  • Content Collections reemplazan el fetch de markdown manual

Paso 4: Convertir componentes

Componentes sin interactividad → .astro:

---
// components/PostCard.astro
const { post } = Astro.props;
const url = `/blog/${post.id.replace(/\.md$/, '')}/`;
---

<article class="border-b border-gray-200 dark:border-gray-800 pb-6">
  <a href={url} class="group">
    <h2 class="text-xl font-semibold group-hover:text-blue-600 transition-colors">
      {post.data.title}
    </h2>
    <p class="mt-2 text-gray-600 dark:text-gray-400">
      {post.data.description}
    </p>
    <time class="mt-2 text-sm text-gray-500">
      {new Date(post.data.date).toLocaleDateString('es-ES')}
    </time>
  </a>
</article>

0 JavaScript enviado al navegador. Este componente se renderiza en build time como HTML puro.

Componentes CON interactividad → mantuve React con client: directive:

---
// En una página .astro
import SearchBar from '../components/SearchBar.tsx';
---

<!-- Solo este componente envía JavaScript -->
<SearchBar client:load />

Paso 5: Content Collections para los posts

// src/content.config.ts
import { defineCollection, z } from 'astro:content';

const posts = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    date: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    image: z.string().optional(),
  }),
});

export const collections = { posts };

Los markdowns van en src/content/posts/ y Astro los valida contra el schema automáticamente.

Los problemas que tuve

Problema 1: No existe useRouter() en Astro

En React hacía:

const router = useRouter();
router.push('/blog');

En Astro, los links son <a href> normales. Sin client-side routing. Cada clic es una navegación completa del navegador.

“¿Eso no es más lento?” — pensé. Pero no. Con HTML pre-renderizado, la navegación del navegador es más rápida que el client-side routing de Next.js porque no hay JavaScript que parsear y ejecutar. El HTML llega y se pinta. Punto.

Para las pocas transiciones que quería animar, usé la View Transitions API de Astro:

---
// En el layout
import { ViewTransitions } from 'astro:transitions';
---

<head>
  <ViewTransitions />
</head>

Una línea y tienes transiciones animadas entre páginas. Sin Framer Motion, sin AnimatePresence, sin 32KB de JavaScript.

Problema 2: El search necesitaba JavaScript

Mi barra de búsqueda era un componente React con estado. En Astro lo mantuve como isla de React:

<SearchBar client:idle />

client:idle carga el JavaScript cuando el navegador está idle. El usuario ve la página inmediatamente, y cuando el navegador no tiene nada que hacer, carga el search. Sin bloquear el render.

Problema 3: Dark mode con FOUC

Astro genera HTML estático, así que si pones la clase dark en <html> con JavaScript del cliente, hay un flash de tema claro antes de que el script se ejecute.

Solución: script inline en el <head> que se ejecuta antes del render:

<head>
  <script is:inline>
    if (localStorage.getItem('color-theme') === 'dark' ||
        (!localStorage.getItem('color-theme') &&
         window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  </script>
</head>

is:inline en Astro evita que el script se bundlee y lo ejecuta inmediatamente.

¿Merece la pena migrar?

Sí, si tu web es principalmente contenido (blog, docs, portfolio, landing page). Los números hablan solos:

  • 98 en Lighthouse vs 62
  • 12 KB de JS vs 187 KB
  • 0.6s FCP vs 2.1s

No, si tu web es una app interactiva (dashboard, SaaS con mucho estado, editor visual). Astro no está diseñado para apps con mucha interactividad — para eso sigue con Next.js o Remix.

La zona gris: Webs que son 80% contenido + 20% interactividad (un blog con un buscador, un portfolio con un formulario de contacto). Ahí Astro con islas de React es perfecto — HTML estático para todo, React solo para los 2-3 componentes que lo necesitan.

Mi portfolio actual usa exactamente eso: Astro para todo el contenido estático, y las pocas piezas interactivas son vanilla JS con is:inline. Zero framework runtime en el navegador.

Artículos relacionados

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.