Por qué capturioo.com es tan rápida - 05/07/2025
La velocidad de una app no es casualidad. Desgrano exactamente por qué capturioo.com se siente instantánea: desde las decisiones de stack hasta Optimistic UI, lazy loading, caché agresivo y Supabase Realtime.
App en producción: capturioo.com
La velocidad de una app no es casualidad. Es el resultado de decisiones deliberadas de arquitectura, stack y patrones de UI. En este post, desgrano exactamente por qué capturioo.com se siente instantánea, desde que el usuario abre la página hasta que interactúa con sus datos.
1. El stack es rápido por diseño
La elección del stack ya predispone la velocidad antes de escribir la primera línea de código:
- React + Vite: el bundler más rápido del ecosistema. HMR instantáneo en desarrollo, bundles optimizados con code splitting automático en producción.
- Supabase: consultas directas a PostgreSQL sin una capa de API intermedia. Menos saltos de red = menos latencia.
- Tailwind CSS: utilidades generadas en build time, CSS mínimo en producción. Sin runtime de estilos que bloquee el render.
2. Optimistic UI: la UI no espera al servidor
El patrón que más impacta la percepción de velocidad es el Optimistic UI. En vez de esperar la confirmación del servidor para actualizar la pantalla, aplico el cambio de inmediato en el estado local y persisto en segundo plano:
async function createCapture(data: NewCapture) {
// 1. Actualizo la UI al instante
const temp = { ...data, id: crypto.randomUUID(), pending: true };
setCaptures((prev) => [temp, ...prev]);
// 2. Persisto en Supabase (no bloquea la UI)
const { data: saved, error } = await supabase
.from('captures')
.insert(data)
.select()
.single();
if (error) {
// Revertir si falla
setCaptures((prev) => prev.filter((c) => c.id !== temp.id));
return;
}
// Reemplazar el item temporal con el real
setCaptures((prev) =>
prev.map((c) => (c.id === temp.id ? saved : c))
);
}
El usuario ve la acción completada en ~0ms. La latencia de red ocurre en paralelo, invisible.
3. Suspense + lazy loading: solo se carga lo que se necesita
Los módulos pesados no se incluyen en el bundle inicial. Los cargo de forma diferida cuando el usuario los necesita:
import { lazy, Suspense } from 'react';
const CaptureDetail = lazy(() => import('./CaptureDetail'));
function App() {
return (
<Suspense fallback={<SkeletonCard />}>
<CaptureDetail />
</Suspense>
);
}
El bundle inicial es mínimo. Las rutas y componentes pesados se descargan solo cuando el usuario los visita.
4. Caché agresivo con React Query
Uso TanStack Query para cachear las respuestas de Supabase. La primera vez que el usuario carga sus capturas, se hace la consulta. Las veces siguientes, la respuesta viene del caché instantáneamente mientras en segundo plano se verifica si hay datos nuevos (stale-while-revalidate):
const { data: captures } = useQuery({
queryKey: ['captures', userId],
queryFn: () => fetchCaptures(userId),
staleTime: 1000 * 60, // 1 minuto fresco
});
El usuario nunca ve una pantalla en blanco al navegar entre rutas que ya visitó.
5. Supabase Realtime en lugar de polling
El polling (consultar el servidor cada N segundos) es costoso en rendimiento y en UX. Con Supabase Realtime, el servidor empuja los cambios al cliente solo cuando ocurren. La app siempre está actualizada sin desperdiciar una sola consulta de red:
supabase
.channel('captures')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'captures' },
(payload) => {
queryClient.invalidateQueries({ queryKey: ['captures'] });
}
)
.subscribe();
6. Imágenes y assets optimizados
- Imágenes servidas en WebP para reducir peso.
- Assets estáticos con hashes en el nombre de archivo, habilitando caché inmutable en el navegador.
- Fuentes cargadas con
font-display: swappara que el texto sea visible desde el primer frame.
El resultado
| Métrica | Valor |
|---|---|
| First Contentful Paint | < 0.8s |
| Time to Interactive | < 1.2s |
| Lighthouse Performance | 95+ |
No se trata de micro-optimizaciones. Se trata de tomar las decisiones correctas desde el principio.