5 cosas para que tu app vaya más rápida
Escribes y la letra sale tarde, abres un menú y se queda pensando. Casi siempre es una de estas cinco cosas, y ninguna te obliga a rehacer tu app.
El prompt que revisa tu app con estas 5 cosas y arregla lo que encuentre.
Tu app va lenta. Escribes y la letra sale tarde, abres un menú y se queda pensando, o la página tarda cinco segundos en aparecer.
Casi nunca es el servidor, ni que tengas que cambiar de lenguaje. Son estas cinco cosas, SIEMPRE las mismas.
Te las explico como si no hubieras programado en tu vida. Y si no quieres tocar nada, arriba tienes el prompt: se lo pegas a tu agente y lo hace él.
Antes de nada, mide. Si no apuntas cómo estás ahora, luego no vas a saber si has mejorado.
Entra en PageSpeed Insights, pega tu URL y apunta estos tres números:
- LCP: lo que tarda en verse lo importante de la página. Por debajo de 2,5 segundos, bien.
- INP: lo que tarda en reaccionar cuando tocas un botón. Por debajo de 200 milisegundos, bien.
- CLS: cuánto salta la página mientras carga (ese botón que se mueve justo cuando vas a pulsarlo). Por debajo de 0,1, bien.
No hace falta que te los aprendas. Apúntalos, que al final los vas a comparar.
Índice
1. Detecta y elimina renders innecesarios
Tu pantalla está hecha de piezas: la cabecera, el buscador, la lista, cada tarjeta. A cada pieza se le llama componente.
Renderizar es que una pieza se vuelva a dibujar. Y aquí está la trampa: cuando una pieza se redibuja, se redibujan también todas las que lleva dentro, aunque a ellas no les haya cambiado nada.
Una vez no cuesta nada. El problema es cuando pasa en cadena.
Ejemplo: guardas lo que escribe el usuario en el buscador arriba del todo, en la página. Cada letra redibuja la página entera con sus 500 tarjetas. Es decir, 500 tarjetas por letra.
Eso es lo que notas como una app «pesada».
Cómo ves si te pasa: instala la extensión React DevTools en Chrome, abre la pestaña Profiler y activa «Highlight updates when components render». Ahora usa tu app: todo lo que se redibuja se ilumina. Si escribes una letra y se te ilumina media pantalla, ya lo tienes.
Cómo se arregla:
- Cada dato, en la pieza que lo usa. Si el texto del buscador solo lo necesita el buscador, que lo guarde él y no la página entera.
- No metas todo en la misma caja. En React hay «contextos», que son cajas de datos que comparten muchas piezas. Si en la misma caja tienes el usuario, el tema y el carrito, cada vez que cambia el carrito se redibuja todo lo que mira al usuario. Una caja por cosa.
- En las listas largas, dibuja solo lo que se ve. Si tienes 1.000 filas y en pantalla caben 15, dibuja 15. Esto se llama virtualizar y lo hacen librerías como TanStack Virtual.
- A cada elemento de una lista, su DNI. En React se llama key: ponle el id del producto, nunca su posición. Si usas la posición y borras el primero, React cree que han cambiado todos y los redibuja todos.
- memo, useMemo y useCallback le dicen a React «si esto no ha cambiado, no lo vuelvas a dibujar». Úsalos solo donde hayas visto el problema con el Profiler. Si tu proyecto usa React Compiler, esto lo hace él solo.
2. Reduce el bundle con lazy loading
Tu app son cientos de archivos: los tuyos y los de todas las librerías que has instalado.
Para que el navegador no tenga que pedirlos uno a uno, al publicar se meten todos en uno o en unos pocos archivos. A ese paquete se le llama bundle.
Piensa en una maleta. Todo lo que metes dentro, el usuario se lo descarga ANTES de poder usar tu app.
Y no solo se lo descarga. El móvil tiene que leerse todo ese código antes de que los botones respondan. Un bundle de 2 MB en un móvil normal son varios segundos con la página delante pero muerta: tocas y no pasa nada.
Lo gracioso es que tu código suele ser lo que menos pesa. Lo que pesa es la librería de gráficas que solo sale en el panel de admin, el editor de texto que solo aparece al escribir un comentario o el paquete entero de iconos para usar cuatro.
Cómo ves si te pasa: hay herramientas que te dibujan la maleta por dentro, con cada librería a su tamaño. En Next.js es @next/bundle-analyzer y en Vite, rollup-plugin-visualizer. Las tres más grandes casi siempre te van a sorprender.
Cómo se arregla: con lazy loading, que traducido es «cargar cuando haga falta». Lo que no se usa al entrar no va en la maleta. El editor se descarga cuando alguien pulsa «Comentar» y la gráfica, cuando entras en su pestaña.
En código es una línea:
// Antes: el editor va en la maleta aunque nadie lo abra
import Editor from "./Editor";
// Después: se descarga la primera vez que alguien lo abre
import dynamic from "next/dynamic";
const Editor = dynamic(() => import("./Editor"));
// Fuera de Next.js, lo mismo con React
const Editor = lazy(() => import("./Editor"));Y tres cosas más que vacían la maleta:
- Coge la pieza, no la caja entera. import debounce from "lodash/debounce" en vez de import _ from "lodash". Si importas la librería entera, te la llevas entera.
- Usa lo que ya trae el navegador. Para formatear fechas y precios tienes Intl, que viene de serie. Moment pesa casi 300 kB para hacer lo mismo.
- Tira lo que ya no usas. Esa librería que instalaste para una prueba hace tres meses sigue en la maleta.
3. Optimiza las peticiones y usa caché
Cada vez que tu app le pide algo al servidor (el usuario, sus pedidos, los productos), eso es una petición.
Cada petición es un viaje de ida y vuelta. Aunque el servidor conteste al momento, el viaje cuesta: desde un móvil, entre 100 y 300 milisegundos cada uno.
Y hay tres fallos que me encuentro en casi todas las apps:
- En fila. Pides el usuario y esperas. Luego sus pedidos y esperas. Luego los productos y esperas. Tres esperas, cuando podías pedir las tres cosas a la vez.
- Repetidas. La cabecera pide el usuario, el menú pide el usuario y el perfil pide el usuario. Tres viajes para el mismo dato.
- Gigantes. Te traes los 5.000 productos con toda su información para enseñar 20 nombres.
Cómo ves si te pasa: pulsa F12 en Chrome, pestaña Network, y recarga la página. Si ves barras que empiezan justo cuando acaba la anterior, van en fila. Si ves la misma dirección varias veces, están repetidas. Y la columna Size te dice cuánto pesa cada una.
Cómo se arregla: lo que no depende entre sí, a la vez. Lo repetido, con una librería como TanStack Query o SWR, que si tres piezas piden lo mismo hace un solo viaje y lo reparte. Y lo gigante, por páginas y pidiendo solo lo que enseñas.
// En fila: 900 ms const user = await getUser(); const orders = await getOrders(); const products = await getProducts(); // A la vez: 300 ms const [user, orders, products] = await Promise.all([ getUser(), getOrders(), getProducts(), ]);
Y luego está la caché, que es guardarte una respuesta para no tener que volver a pedirla.
Si tus categorías cambian una vez al mes, no tiene sentido ir a buscarlas cada vez que alguien entra. Te las guardas y listo.
Se pueden guardar en tres sitios: en el navegador del usuario, en un CDN (servidores repartidos por el mundo que sirven tu web desde el que el usuario tiene más cerca) o en tu propio servidor.
Solo hay una regla, y es IMPORTANTE: toda caché tiene que saber cuándo caduca. Si no lo decides tú, un día cambias un precio y medio mundo sigue viendo el viejo.
Lo conté más a fondo en mi artículo de cómo escalar tu app a un millón de usuarios.
4. Encuentra queries lentas y problemas N+1
Las peticiones van del navegador a tu servidor. Las queries van de tu servidor a la base de datos.
Una query es una pregunta a la base de datos: «dame los pedidos de este mes». Si una petición tarda, muchas veces es porque dentro lleva una query lenta.
¿Por qué es lenta? Casi siempre porque falta un índice.
Un índice es exactamente lo mismo que el índice de un libro. Sin él, para encontrar algo tienes que leerte el libro entero, página a página. Con mil filas no lo notas. Con doscientas mil, cada pregunta tarda más de un segundo.
Y luego está el N+1, que es el fallo más repetido y el más difícil de ver.
Pides los 20 últimos pedidos: 1 pregunta. Luego, para enseñar el nombre del cliente de cada pedido, haces una pregunta por cada uno: 20 más. Total, 21 preguntas para una sola pantalla. Con 100 pedidos, 101.
Es como ir al súper 21 veces a por 21 cosas, en vez de ir una vez con la lista.
Pasa muchísimo con los ORM (las librerías que te dejan hablar con la base de datos sin escribir SQL), porque escribes pedido.cliente.nombre y no parece una pregunta. Pero lo es.
// N+1: 1 pregunta para los pedidos y 1 más por cada pedido
const orders = await db.order.findMany({ take: 20 });
for (const order of orders) {
order.customer = await db.customer.findUnique({ where: { id: order.customerId } });
}
// 1 sola pregunta: los pedidos con su cliente dentro
const orders = await db.order.findMany({ take: 20, include: { customer: true } });Cómo ves si te pasa: activa el registro de queries de tu ORM y carga una pantalla. Si ves la misma pregunta muchas veces cambiando solo el id, es un N+1.
Para las lentas, si usas Supabase tienes Query Performance en el panel. En cualquier Postgres, pon EXPLAIN ANALYZE delante de la query: si te contesta «Seq Scan» en una tabla grande, se está leyendo la tabla entera. Ahí falta un índice.
Cómo se arregla: el N+1, pidiendo los pedidos y sus clientes en una sola pregunta (un JOIN en SQL, o el include de tu ORM). Las lentas, con un índice en las columnas por las que buscas u ordenas.
Eso sí, sin pasarte: cada índice hace un poco más lento guardar datos. Ponlos solo donde busques de verdad.
5. Optimiza fuentes, imágenes y scripts
Los cuatro puntos anteriores son de código. Este es de todo lo demás que se descarga tu web, y en la mayoría de webs es lo que MÁS pesa.
Imágenes. Subes la foto de portada tal cual sale del móvil: 4.000 píxeles de ancho y 4 MB. Para enseñarla a 1.200.
- Formato moderno. WebP o AVIF pesan entre la mitad y diez veces menos que un JPG o un PNG, y a la vista son iguales.
- Al tamaño al que se ve. Si se enseña a 1.200 píxeles, que pese lo de 1.200 píxeles. En Next.js, su componente Image lo hace solo.
- Con ancho y alto puestos. Así el navegador le guarda el hueco antes de que llegue y la página no salta. Ese salto es el CLS del principio.
- Las de abajo, cuando hagan falta. loading="lazy" en las imágenes que no se ven al entrar. Y la principal, al revés: con prioridad, porque es la que decide el LCP.
Fuentes. Cada grosor de una tipografía (normal, negrita, cursiva…) es un archivo aparte. Si cargas seis y usas dos, te estás descargando el triple para nada.
Carga solo los grosores que uses, en formato woff2 (el más ligero), desde tu propia web y con font-display: swap. Esto último hace que el texto salga con una letra del sistema mientras llega la tuya, en vez de dejar la página en blanco. En Next.js, next/font lo hace todo solo.
Scripts de terceros. Google Analytics, el chat de soporte, el píxel de Meta, el de TikTok, el mapa de calor que pusiste para una prueba…
Cada uno es código de otra empresa que se descarga y se ejecuta en TU web. Y entre todos suelen pesar más que tu propia app.
Quita los que nadie mira. Y los demás, que carguen cuando la página ya funciona, no antes. En Next.js, <Script strategy="lazyOnload">; fuera de Next, el atributo defer.
Cómo ves si te pasa: en la pestaña Network, ordena por tamaño. Lo de arriba es lo primero que toca. PageSpeed Insights además te lo dice con nombre y apellidos («Properly size images», «Reduce unused JavaScript») y con los kB que te ahorrarías.
Resumen
- Mide antes: LCP, INP y CLS en PageSpeed Insights.
- Renders: cada dato, en la pieza que lo usa. En las listas largas, solo lo que se ve.
- Bundle: la maleta que se descarga el usuario antes de poder usar tu app. Lo que no se ve al entrar, fuera.
- Peticiones: a la vez y no en fila, una por dato, y con caché que sepa cuándo caduca.
- Queries: un índice en lo que buscas y una sola pregunta en vez de N+1.
- Imágenes en AVIF a su tamaño, solo las fuentes que usas y los scripts de terceros al final.
Ninguna de las cinco te obliga a rehacer tu app. Son tardes sueltas, y casi siempre la primera que mires ya se nota.
Mi consejo: empieza por la que te marque PageSpeed. Arregla, vuelve a medir y pasa a la siguiente. De UNA en UNA.
Y si lo quieres hecho
Copia el prompt de arriba y pégaselo a tu agente con el proyecto abierto. Va por las cinco en este orden, mide antes de tocar nada y te devuelve una tabla con el antes y el después.
Y cuando la tengas más rápida, mándame los números por Instagram, que esas cosas me alegran el día.