Cambiar tema

Gestión de estados de carga en Next.js: guía práctica de loading.tsx y Suspense

Easton editorial illustration: hydration gauge console

Seguro que te ha pasado: el usuario hace clic en un enlace y la página se queda en blanco durante 3 segundos enteros, sin ningún feedback. Empieza a pensar: «¿Se ha colgado?» y pulsa F5 como loco; justo cuando la página termina de cargar, la recarga y vuelve a empezar…

Antes yo también gestionaba el loading así. En cada página nueva tenía que añadir en el componente:

const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);

useEffect(() => {
  setLoading(true);
  fetchData()
    .then(setData)
    .finally(() => setLoading(false));
}, []);

if (loading) return <Spinner />;

Código largo y feo, y repetido en cada página. Peor aún: en el equipo cada uno implementaba el loading a su manera — unos con estado global, otros con Context — y mantenerlo era una pesadilla.

Hasta que, leyendo la documentación oficial de Next.js, descubrí que Next.js ya trae un sistema más elegante para gestionar la carga: loading.tsx y Suspense.

Al usarlo me di cuenta de que gestionar el loading puede ser así de simple. La mitad de código y una experiencia de usuario mucho mejor. En este artículo comparto la experiencia práctica con este enfoque.

Por qué usar loading.tsx y Suspense

Los problemas del enfoque tradicional

Te muestro un ejemplo real. Supongamos una página de listado de blog; el enfoque clásico sería algo así:

// app/blog/page.tsx
'use client';
import { useState, useEffect } from 'react';

export default function BlogPage() {
  const [loading, setLoading] = useState(true);
  const [posts, setPosts] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div className="spinner">Loading...</div>;
  }

  if (error) {
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

Parece razonable, ¿no? Pero los problemas son:

  1. Código redundante: en cada página repites este bloque de gestión de estado
  2. Estado fragmentado: loading, data y error en tres states distintos; fácil desincronizarlos
  3. Tiene que ser Client Component: con useState y useEffect todo corre en el cliente y pierdes ventajas del SSR
  4. Mala UX: entre el clic y ver el loading hay una pantalla en blanco notable

Si has hecho code review, sabes que cada desarrollador lo hace distinto: Context, Zustand global o lógica local en cada componente. En proyectos grandes es imposible de mantener.

La solución de Next.js

El App Router de Next.js ofrece tres piezas clave:

1. loading.tsx — convención sobre configuración

Creas un archivo loading.tsx en la carpeta de la ruta y Next.js lo usa como UI de carga de esa ruta. Sin useState manual, sin gestionar estado, sin envolver Suspense tú mismo.

2. Suspense — soporte nativo de React 18

Suspense te permite controlar el loading a nivel de componente. Lo que tarde en cargar lo envuelves en un límite Suspense; el resto se muestra sin esperar a toda la página.

3. Streaming — mostrar mientras carga

Con el renderizado en streaming de Next.js, la página aparece por partes: primero la cabecera, luego la barra lateral y al final los datos lentos. Menos pantalla en blanco y mejor experiencia.

Con skeleton screens y streaming suele bajar el FCP (First Contentful Paint) y el LCP (Largest Contentful Paint); en PageSpeed Insights puedes ganar varios puntos.

Uso básico de loading.tsx

Primeros pasos: tu primer loading.tsx

Vamos directo al grano con el loading.tsx más simple.

Supón esta estructura:

app/
  blog/
    page.tsx

Solo añade loading.tsx en blog:

app/
  blog/
    loading.tsx  ← nuevo
    page.tsx

Contenido mínimo:

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center min-h-screen">
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
      <p className="ml-4">Cargando...</p>
    </div>
  );
}

Así de simple, unas 10 líneas. Al visitar /blog, antes de que termine page.tsx, Next.js muestra este componente.

Importante: no hace falta envolver Suspense a mano; Next.js envuelve tu page.tsx en <Suspense fallback={<Loading />}>.

La primera vez pensé: «¿Tan fácil? ¿De verdad funciona?» Lo probé y sí. El código queda mucho más limpio, sin useState en cada página.

Alcance de loading.tsx

Concepto clave: el segmento de ruta (Route Segment). loading.tsx afecta al page.tsx de la misma carpeta y a todas las subrutas.

Ejemplo:

app/
  blog/
    loading.tsx     ← afecta a /blog y /blog/[id]
    page.tsx        ← listado /blog
    [id]/
      page.tsx      ← detalle /blog/123

Se muestra cuando:

  • el usuario entra en /blog (carga del listado)
  • pasa del listado a /blog/123 (carga del detalle)

No afecta al layout. Si blog/layout.tsx tiene una barra de navegación, sigue visible; solo se sustituye la parte de page.tsx por el loading.

Eso es lo que la documentación llama «el layout compartido sigue siendo interactivo»: mientras carga la nueva página, el usuario puede usar la navegación sin bloquear toda la interfaz.

Esquema:

Layout (siempre visible)
  ├─ Barra de navegación
  └─ Límite Suspense
       ├─ Loading UI (mientras cargan datos)
       └─ Page (cuando terminan)

Server Component vs Client Component

loading.tsx por defecto es un Server Component. En la mayoría de casos basta con devolver JSX.

Si quieres animaciones (Framer Motion, librerías que necesitan JS en cliente), añade 'use client':

// app/blog/loading.tsx
'use client';
import { motion } from 'framer-motion';

export default function Loading() {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      className="flex items-center justify-center min-h-screen"
    >
      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900"></div>
    </motion.div>
  );
}

Mi regla: Server Component siempre que puedas; 'use client' solo si hace falta interacción en cliente. Menos bundle en el cliente y carga más rápida.

Skeleton screens en la práctica

Por qué un skeleton supera al spinner

Todos hemos visto spinners girando. En UX, un skeleton screen suele funcionar mejor que un spinner.

La psicología: con skeleton el cerebro anticipa «el contenido llega ya» y la espera se percibe más corta. Con spinner solo sabes «está cargando», sin saber qué ni cuánto — más ansiedad.

Además el skeleton anticipa el layout: tres barras sugieren tres artículos; hay expectativa y menos nervios.

Tres formas de implementarlo

Opción 1: solo CSS (más ligera)

Sin dependencias extra:

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8 animate-pulse">
          {/* Skeleton del título */}
          <div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
          {/* Skeleton del resumen */}
          <div className="space-y-2">
            <div className="h-4 bg-gray-200 rounded"></div>
            <div className="h-4 bg-gray-200 rounded w-5/6"></div>
          </div>
          {/* Skeleton de metadatos */}
          <div className="flex gap-4 mt-4">
            <div className="h-3 bg-gray-200 rounded w-20"></div>
            <div className="h-3 bg-gray-200 rounded w-24"></div>
          </div>
        </div>
      ))}
    </div>
  );
}

Cero dependencias y buen rendimiento; tú escribes los estilos.

Opción 2: react-loading-skeleton (rápida)

npm install react-loading-skeleton
// app/blog/loading.tsx
'use client';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';

export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8">
          <Skeleton height={32} width="75%" className="mb-4" />
          <Skeleton count={2} />
          <div className="flex gap-4 mt-4">
            <Skeleton width={80} />
            <Skeleton width={100} />
          </div>
        </div>
      ))}
    </div>
  );
}

Cómodo y con buenas animaciones; lo uso en proyectos pequeños.

Opción 3: shadcn/ui (más profesional)

Si ya usas shadcn/ui:

npx shadcn-ui@latest add skeleton
// app/blog/loading.tsx
import { Skeleton } from '@/components/ui/skeleton';

export default function Loading() {
  return (
    <div className="max-w-4xl mx-auto p-6">
      {[1, 2, 3].map((i) => (
        <div key={i} className="mb-8">
          <Skeleton className="h-8 w-3/4 mb-4" />
          <Skeleton className="h-4 w-full mb-2" />
          <Skeleton className="h-4 w-5/6 mb-4" />
          <div className="flex gap-4">
            <Skeleton className="h-3 w-20" />
            <Skeleton className="h-3 w-24" />
          </div>
        </div>
      ))}
    </div>
  );
}

Los estilos encajan con tu design system.

Principios de diseño del skeleton

  1. Coincidir con el layout real: título, resumen y etiquetas en el listado → el skeleton debe reflejarlo.
  2. Animación sutil: un pulso suave basta; animaciones llamativas distraen y alargan la sensación de espera.
  3. Cantidad razonable: 3–5 ítems suele bastar; llenar la pantalla cansa.

Caso completo: listado de blog

loading.tsx:

// app/blog/loading.tsx
export default function BlogLoading() {
  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <div className="h-12 bg-gray-200 rounded w-1/3 mb-8 animate-pulse"></div>

      <div className="space-y-8">
        {[1, 2, 3].map((i) => (
          <article key={i} className="border-b pb-8 animate-pulse">
            <div className="h-8 bg-gray-200 rounded w-3/4 mb-3"></div>
            <div className="space-y-2 mb-4">
              <div className="h-4 bg-gray-200 rounded"></div>
              <div className="h-4 bg-gray-200 rounded w-11/12"></div>
              <div className="h-4 bg-gray-200 rounded w-4/5"></div>
            </div>
            <div className="flex gap-3">
              <div className="h-6 bg-gray-200 rounded-full w-16"></div>
              <div className="h-6 bg-gray-200 rounded-full w-20"></div>
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

page.tsx como Server Component:

// app/blog/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store' // asegura fetch fresco cada vez
  });

  if (!res.ok) throw new Error('Failed to fetch posts');

  return res.json();
}

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

  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8">Artículos del blog</h1>

      <div className="space-y-8">
        {posts.map((post) => (
          <article key={post.id} className="border-b pb-8">
            <h2 className="text-2xl font-semibold mb-3">
              <a href={`/blog/${post.slug}`} className="hover:text-blue-600">
                {post.title}
              </a>
            </h2>
            <p className="text-gray-600 mb-4">{post.excerpt}</p>
            <div className="flex gap-3">
              {post.tags.map((tag) => (
                <span key={tag} className="px-3 py-1 bg-gray-100 rounded-full text-sm">
                  {tag}
                </span>
              ))}
            </div>
          </article>
        ))}
      </div>
    </div>
  );
}

page.tsx es async y hace await de los datos en el componente. Sin useState ni useEffect. Al ser Server Component, no aumenta el bundle del cliente y la primera carga es más rápida.

Depuración con React DevTools

A veces los datos cargan tan rápido que el loading apenas se ve.

Truco: React DevTools para suspender el límite Suspense manualmente.

  1. Instala la extensión React DevTools
  2. Abre las herramientas de desarrollo, pestaña Components
  3. Localiza <Suspense>
  4. Clic derecho → «Suspend this Suspense boundary»

El loading queda fijo y puedes ajustar estilos. Luego cancelas el suspend.

Me costó varios tropiezos descubrirlo; habría ahorrado mucho tiempo.

Suspense: técnicas avanzadas

Límites Suspense manuales

loading.tsx es cómodo, pero a veces quieres control fino: varias fuentes de datos con loading independiente, no esperar a que todo esté listo.

Ahí entran los límites Suspense manuales.

Error habitual (yo al principio): poner Suspense dentro del componente que obtiene datos:

// ❌ Incorrecto — Suspense demasiado abajo
async function PostList() {
  const posts = await fetchPosts();

  return (
    <Suspense fallback={<Loading />}>  {/* ¡no funciona así! */}
      <div>
        {posts.map(post => <Post key={post.id} {...post} />)}
      </div>
    </Suspense>
  );
}

No surte efecto. Suspense debe estar más arriba en el árbol para «capturar» las operaciones asíncronas de los hijos.

Forma correcta en el padre:

// ✅ Correcto — Suspense en el padre
export default function BlogPage() {
  return (
    <div>
      <h1>Artículos del blog</h1>

      <Suspense fallback={<PostListSkeleton />}>
        <PostList />
      </Suspense>
    </div>
  );
}

// El hijo obtiene datos
async function PostList() {
  const posts = await fetchPosts();

  return (
    <div>
      {posts.map(post => <Post key={post.id} {...post} />)}
    </div>
  );
}

Imagina Suspense como una compuerta: vigila lo que hay debajo; si algún hijo espera datos, se cierra y muestra el fallback; cuando todo llega, abre y muestra el contenido.

Rutas dinámicas

Un caso que me costó: detalle de producto /products/[id], de producto A (id=1) a B (id=2). ¡loading.tsx no aparece!

El contenido salta de A a B sin transición; se siente brusco.

React reutiliza la misma instancia si el tipo de componente es el mismo (ProductPage) y solo cambian props. Suspense piensa «no hay componente nuevo, no suspendo de nuevo».

Solución: key en Suspense para forzar un montaje nuevo.

// app/products/[id]/page.tsx
import { Suspense } from 'react';

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <Suspense key={params.id} fallback={<ProductSkeleton />}>
      <ProductDetail id={params.id} />
    </Suspense>
  );
}

async function ProductDetail({ id }: { id: string }) {
  const product = await fetchProduct(id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <span>${product.price}</span>
    </div>
  );
}

Fíjate en <Suspense key={params.id} ...>.

Al cambiar el id, React destruye el Suspense anterior y crea uno nuevo; vuelve a suspender y el loading se muestra bien.

Lo vi en un issue de GitHub; una línea de key y listo.

Varias cargas a la vez

Escenario: dashboard con usuario, estadísticas y actividad reciente, cada uno con su API.

Estrategia 1: mostrar todo junto (un Suspense para todo)

export default function Dashboard() {
  return (
    <Suspense fallback={<DashboardSkeleton />}>
      <UserInfo />      {/* API 1 */}
      <Statistics />    {/* API 2 */}
      <RecentActivity /> {/* API 3 */}
    </Suspense>
  );
}

Ventaja: simple, contenido completo de golpe. Inconveniente: la API más lenta frena todo; espera = tiempo de la más lenta.

Estrategia 2: mostrar por partes (varios límites Suspense)

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserInfoSkeleton />}>
        <UserInfo />
      </Suspense>

      <Suspense fallback={<StatsSkeleton />}>
        <Statistics />
      </Suspense>

      <Suspense fallback={<ActivitySkeleton />}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

Ventaja: lo rápido aparece primero; espera percibida menor. Inconveniente: la página «salta» al ir entrando bloques.

Yo elijo según importancia de los datos:

  • Datos core (usuario) en un Suspense para mostrarlos juntos
  • Secundarios (recomendaciones, anuncios) en Suspense aparte

Equilibrio entre experiencia y rendimiento.

Problemas frecuentes y soluciones

Suspense no funciona

Comprueba:

1. Forma de obtener datos

Suspense solo con métodos compatibles. En App Router:

  • ✅ await directo en Server Component (recomendado)
  • ✅ librerías con Suspense (SWR, React Query)
  • ❌ fetch en useEffect
  • ❌ Promise.then clásico

2. Posición del componente

Suspense encima del componente que obtiene datos, no en el mismo nivel ni debajo.

3. Versiones

  • React 18+
  • Next.js 13+ (App Router)

4. Depuración

Suspende manualmente con React DevTools; si no reacciona, Suspense no está activo — revisa lo anterior.

Trampa de useFormStatus

Con Server Actions para formularios, useFormStatus muestra el estado de envío.

useFormStatus solo funciona en Client Component.

El <form> debe renderizarse en Server Component para enlazar la Server Action.

Patrón: Server Component para el form, Client Component para el estado.

// app/actions.ts
'use server';
export async function submitForm(formData: FormData) {
  // procesar formulario...
  await saveToDatabase(formData);
}
// app/page.tsx (Server Component)
import { submitForm } from './actions';
import { SubmitButton } from './submit-button';

export default function Page() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" />
      <SubmitButton />
    </form>
  );
}
// app/submit-button.tsx (Client Component)
'use client';
import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Enviando...' : 'Enviar'}
    </button>
  );
}

Form en servidor, botón en cliente: Server Action y estado de carga funcionan.

Prefetch y el loading

<Link> de Next.js hace prefetch por defecto cuando el enlace entra en el viewport.

A veces al hacer clic el loading apenas se ve porque la página ya está precargada.

Para probar el loading, desactiva prefetch temporalmente:

<Link href="/blog" prefetch={false}>
  Blog
</Link>

En producción conviene dejar prefetch activo. Si el loading es demasiado breve, puedes imponer un mínimo (p. ej. 300 ms) o usar skeleton en lugar de spinner.

Resumen

Puntos clave:

  1. loading.tsx es la mejor práctica a nivel de ruta: en la carpeta de la ruta; Next.js hace el resto. Menos useState manual y la mitad de código.

  2. Skeleton mejor que spinner: anticipa el layout y reduce ansiedad. CSS puro, react-loading-skeleton o tu UI kit.

  3. Suspense arriba en el árbol: es una compuerta; mal colocado no funciona.

  4. key en rutas dinámicas: sin él, al cambiar id no hay loading. No olvides <Suspense key={params.id}>.

  5. Varias fuentes: Suspense según necesidad: core junto, secundario en paralelo.

Pasar de useState manual a loading.tsx no es más trabajo: es trabajar más inteligente. Menos código, menos bugs y mejor UX.

Próximos pasos

Acción inmediata: en un proyecto existente, elige un listado simple y migra el loading a loading.tsx. Probarlo vale más que leer diez artículos.

Siguiente nivel: cuando domines loading, mira Error Boundaries — loading y errores van de la mano. Tengo previsto un artículo práctico sobre Error Boundaries.

Comparte: ¿cómo gestionas el loading en tus proyectos? ¿Qué enfoque usas? ¿Qué trampas has encontrado? Cuéntalo en los comentarios.


Referencias:

Flujo completo de gestión de carga en Next.js

Usa loading.tsx y Suspense para una experiencia de carga profesional sin useState manual

⏱️ Estimated time: 1 hr

  1. 1

    Step 1: Crear el archivo loading.tsx

    Crea loading.tsx en el directorio de la ruta:
    • app/dashboard/loading.tsx: estado de carga de dashboard
    • app/products/[id]/loading.tsx: estado de carga en ruta dinámica

    Contenido del archivo:
    export default function Loading() {
    return <div>Cargando...</div>
    }

    Next.js mostrará este componente automáticamente al cargar la página
  2. 2

    Step 2: Implementar skeleton screen

    Crea una UI de carga más profesional:
    • Usa componentes Skeleton que imiten el layout del contenido
    • Mantén un layout similar al contenido real
    • Añade animación para mejorar la experiencia

    Ejemplo:
    export default function Loading() {
    return (
    <div className="animate-pulse">
    <div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
    <div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
    <div className="h-4 bg-gray-200 rounded w-5/6"></div>
    </div>
    )
    }
  3. 3

    Step 3: Envolver componentes asíncronos con Suspense

    Usa Suspense en los componentes:
    • Envuelve componentes que obtienen datos de forma asíncrona
    • Define fallback para el estado de carga
    • Suspense anidado para granularidad fina

    Ejemplo:
    <Suspense fallback={<Loading />}>
    <AsyncComponent />
    </Suspense>

    Varios componentes pueden tener su propio Suspense:
    • Cada uno carga de forma independiente
    • Lo rápido aparece primero
    • Mejor experiencia de usuario
  4. 4

    Step 4: Gestionar loading en rutas dinámicas

    Loading en rutas dinámicas:
    • Crea loading.tsx en el directorio de la ruta dinámica
    • Next.js gestiona la carga cuando cambian los parámetros
    • Sin gestión manual del estado de carga

    Ejemplo:
    app/products/[id]/
    ├── loading.tsx # se muestra al cambiar parámetros
    └── page.tsx

    Al navegar de /products/1 a /products/2,
    loading.tsx se muestra automáticamente
  5. 5

    Step 5: Optimizar la experiencia de carga

    Consejos de optimización:
    • Skeleton en lugar de un Spinner simple
    • Mantén la UI de carga alineada con el layout real
    • Animación (animate-pulse) para mejor sensación
    • Suspense razonable para streaming

    Evita:
    • loading.tsx en todas partes sin criterio
    • UI de carga demasiado compleja
    • Ignorar errores (combina con error.tsx)
  6. 6

    Step 6: Probar y validar

    Puntos de prueba:
    • Estado de carga al navegar entre páginas
    • Carga al cambiar parámetros en rutas dinámicas
    • Experiencia con red lenta
    • Comprobar que la UI de carga es fluida

    Checklist:
    • Cada ruta tiene un estado de carga adecuado
    • La UI de carga coincide con el layout del contenido
    • Sin parpadeos ni saltos de layout
    • Experiencia fluida para el usuario

FAQ

¿Qué diferencia hay entre loading.tsx y useState manual?
loading.tsx es una convención de Next.js que se muestra automáticamente al cargar la página, sin gestionar estado a mano. Con useState hay que repetirlo en cada página, con código duplicado y más errores. loading.tsx reduce el código un 50%, mejora la UX y admite renderizado en streaming.
¿Cuándo se muestra loading.tsx?
loading.tsx se muestra automáticamente cuando: 1) el usuario navega a esa ruta; 2) cambian los parámetros en una ruta dinámica; 3) se carga la ruta padre. Next.js gestiona el momento; al terminar de cargar los datos de la página, loading.tsx se oculta solo.
¿Qué diferencia hay entre Suspense y loading.tsx?
loading.tsx es carga a nivel de ruta, para toda la página. Suspense es a nivel de componente: envuelve componentes asíncronos concretos para un control más fino. Puedes combinarlos: loading.tsx en la ruta y Suspense dentro de componentes.
¿Cómo implementar un skeleton screen?
En loading.tsx usa componentes Skeleton que imiten el layout del contenido real. Añade animación con la clase animate-pulse de Tailwind. Mantén el skeleton alineado con el contenido final para evitar saltos de layout al cargar.
¿Cómo gestionar el loading en rutas dinámicas?
Crea loading.tsx en el directorio de la ruta dinámica, por ejemplo app/products/[id]/loading.tsx. Al ir de /products/1 a /products/2, Next.js muestra loading.tsx sin gestionar manualmente el cambio de parámetros.
¿Puedo personalizar el estilo del loading?
Sí. loading.tsx es un componente React normal; puedes usar Tailwind CSS, CSS Modules, styled-components, etc. Se recomienda skeleton en lugar de un spinner simple para mejorar la UX.
¿loading.tsx afecta al rendimiento?
No; suele mejorarlo. loading.tsx apoya el streaming: la página puede cargar por bloques sin esperar a que termine todo. Lo rápido aparece primero y la experiencia global mejora.

14 min de lectura · Publicado el: 5 ene 2026 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog