Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

[Guía completa y probada en campo] Optimización del rendimiento en Laravel — encontrar la causa raíz de la lentitud, corregir N+1, caché/HTTP caching, colas, índices de BD, Redis, optimización front-end y una experiencia “rápida” accesible

php elephant sticker

Photo by RealToughCandy.com on Pexels.com

[Guía completa y probada en campo] Optimización del rendimiento en Laravel — encontrar la causa raíz de la lentitud, corregir N+1, caché/HTTP caching, colas, índices de BD, Redis, optimización front-end y una experiencia “rápida” accesible

Lo que aprenderás (puntos clave)

  • Un flujo de trabajo práctico para eliminar cuellos de botella mediante medir → formular hipótesis → mejorar → volver a medir
  • Cómo identificar cuellos de botella comunes: N+1, SELECTs innecesarios, agregaciones pesadas, APIs externas lentas, E/S lenta, etc.
  • Optimización de Eloquent (with/withCount/select/chunk/cursor) y diseño de índices en la BD
  • Caché (app/query/view/config), Redis y caché HTTP (ETag/304)
  • Colas y procesamiento asíncrono (correo/imágenes/PDFs/agregaciones), monitoreo con Horizon
  • Optimización del front-end y entrega (compresión, HTTP/2, CDN, optimización de imágenes)
  • Una experiencia “rápida” accesible: estados de carga compatibles con lectores de pantalla, trampas de skeleton, prefers-reduced-motion y cómo comunicar tiempos de espera

Lectores objetivo (¿a quién beneficia?)

  • Ingenieros Laravel nivel principiante–intermedio: quieren reproducir causas de lentitud y acumular mejoras
  • Tech leads / operaciones: quieren medición y monitoreo sólidos para evitar regresiones antes del despliegue
  • Diseño / QA / accesibilidad: quieren una UI de carga que “cualquiera pueda entender”

Nivel de accesibilidad: ★★★★★

Incluye estados de carga compatibles con lectores de pantalla (aria-busy / role="status"), progreso que no depende solo del color, movimiento reducido y flujos aptos para teclado que evitan perderse durante esperas.


1. Introducción: optimizar no es “intuición”, es medición

La optimización de rendimiento no es hacer todo rápido por capricho—es un medio para reducir la espera del usuario, bajar costos de servidor y prevenir incidentes. El camino más corto a resultados reales es evitar adivinar y repetir medir → mejorar → volver a medir.

Laravel es conveniente, y eso también significa que puede volverse lento si no haces nada. Pero la ventaja es que los cuellos de botella típicos son conocidos—si los eliminas en orden, puedes hacer que la app sea confiablemente más rápida.


2. Mide primero: identifica qué es lo lento

2.1 Define tu objetivo (SLO) desde el inicio

Ejemplos:

  • p95 de páginas clave por debajo de 300ms
  • p95 de APIs por debajo de 200ms
  • tasa de 5xx por debajo de 0,1%

Si no decides qué pantallas importan, la optimización nunca termina.

2.2 Puntos centrales de medición

  • App: duración de request, número de queries SQL, tiempo de SQL, tiempo HTTP externo, retrasos de cola
  • BD: consultas lentas, bloqueos, uso de índices
  • Infra: CPU, memoria, E/S, red, ratio de acierto de caché (cache hit ratio)
  • UX: TTFB, LCP, CLS, INP (Web Vitals)

2.3 Herramientas prácticas del lado de Laravel

  • Telescope: visualizar requests/SQL/excepciones en dev y validación
  • Logs: logging estructurado (trace_id + tiempos)
  • APM: Sentry / Datadog / New Relic (muy potentes si puedes adoptar uno)

3. Mapa de cuellos de botella típicos (el orden que suele rendir primero)

En el orden que tiende a dar resultados más rápido en proyectos reales:

  1. N+1 (demasiadas consultas SQL)
  2. Traer columnas/filas innecesarias (SELECT inflado, sin paginación)
  3. Agregaciones pesadas (COUNT / GROUP BY en cada request)
  4. APIs externas lentas/inestables
  5. Trabajo pesado ejecutado de forma síncrona (generación de imágenes/PDF, etc.)
  6. Sin caché / caché ineficaz
  7. Índices de BD pobres
  8. Problemas de entrega en front-end (sin compresión, imágenes pesadas, sin CDN)

4. Correcciones N+1: lo más básico de Eloquent

4.1 Ejemplo de N+1 (malo)

$posts = Post::latest()->take(20)->get();
foreach ($posts as $post) {
  echo $post->user->name; // probablemente dispara 20 queries extra
}

4.2 Eager loading con with (bueno)

$posts = Post::with('user')->latest()->take(20)->get();

4.3 Conteos con withCount

$posts = Post::withCount('comments')->latest()->paginate(20);

4.4 Minimiza columnas con select

$posts = Post::query()
  ->select(['id','user_id','title','created_at'])
  ->with(['user:id,name'])
  ->latest()
  ->paginate(20);

Notas

  • Menos columnas = menos memoria + menos transferencia.
  • with no es magia. Usarlo en exceso puede ralentizar, así que añádelo paso a paso empezando por pantallas clave.

5. Optimización de queries: paginación, índices y consultas lentas

5.1 La paginación es obligatoria

Devolver “todas las filas” en un listado no solo es lento; también puede reventar memoria.
Usa paginate() o simplePaginate() por defecto.

5.2 Reglas prácticas para diseñar índices

  • Usa índices compuestos que coincidan con combinaciones de WHERE/ORDER BY
  • Si filtras por tenant_id, inclúyelo primero, p. ej. (tenant_id, created_at)
  • Diseña índices en el orden de “condiciones más comunes”

Ejemplo:

  • WHERE status = ? AND created_at >= ? ORDER BY created_at DESC
    → considera (status, created_at)

5.3 Leer EXPLAIN (simplificado)

  • Si aparece type=ALL (escaneo completo), trátalo como bandera roja
  • Arregla primero queries con estimaciones rows extremadamente grandes
  • Using filesort no siempre es malo, pero si es frecuente, investiga

6. Procesamiento de grandes volúmenes: chunk / cursor / lazy

6.1 chunk

User::where('active', true)->chunkById(1000, function($users){
  foreach ($users as $u) { /* trabajo */ }
});

6.2 cursor (amigable con memoria)

foreach (User::where('active', true)->cursor() as $u) {
  // procesa uno a uno, suele usar menos memoria
}

6.3 Precauciones

  • cursor() no ejecuta un SQL por fila; hace streaming con un iterador. Pero cuidado con relaciones (N+1 puede reaparecer fácilmente).
  • El procesamiento masivo normalmente combina bien con colas.

7. Caché: mayor impacto con el menor esfuerzo

7.1 Empieza con datos “alto costo, bajo cambio”

  • Rankings de la portada
  • Listas de categorías de navegación
  • Agregados de dashboard
  • Valores de configuración (feature flags, límites de plan)
$items = Cache::remember('top:popular', 300, function(){
  return Product::orderByDesc('popularity')->take(20)->get();
});

7.2 Diseño de claves de caché

  • Si los datos dependen de locale/tenant/usuario, inclúyelos en la clave
  • Ejemplo: t:{tenant_id}:home:popular:ja

7.3 Caché con etiquetas (tagged cache, según el driver)

Permite invalidar claves relacionadas de una vez, lo que facilita la operación.


8. Caché HTTP: ahorra ancho de banda con ETag/304

Para APIs y listados que cambian poco, las solicitudes condicionales son efectivas.

  • Sin cambios → 304
  • Cambió → 200 + ETag
$etag = sha1($updatedAt.$id);
if (request()->header('If-None-Match') === $etag) {
  return response()->noContent(304)->header('ETag',$etag);
}
return response()->json($data)->header('ETag',$etag);

9. Colas: no hagas que el usuario espere trabajo pesado

9.1 Candidatos típicos para async

  • Envío de correo
  • Generación de PDF/imágenes
  • Sincronización con APIs externas
  • Agregaciones grandes
  • Logs de auditoría y actualizaciones de índice de búsqueda

9.2 Presentación en UI (accesible)

En lugar de bloquear de forma síncrona, muestra al usuario:

  • “Iniciado”
  • “En progreso”
  • “Completado (descarga aquí)”
<div role="status" aria-live="polite" id="job-status">
  Exportación iniciada. Se te notificará cuando termine.
</div>

10. Redis: sesiones/caché/colas más rápidas

  • Sesiones en Redis reducen E/S y facilitan el escalado horizontal
  • Caché vía Redis es estable y rápida
  • Con Horizon, puedes visualizar la salud de las colas

Nota

  • Redis es rápido, pero un diseño descuidado de claves puede desperdiciar memoria. Define TTLs y límites.

11. Optimización front-end: si solo el servidor es rápido, igual no “se siente” rápido

11.1 Imágenes

  • Optimiza primero (WebP/AVIF, tamaños correctos, lazy loading)
  • Especifica width/height para reducir layout shifts

11.2 Compresión

  • Habilita gzip/brotli
  • Pon assets estáticos en un CDN

11.3 JS/CSS

  • Reduce JS sin usar
  • Prioriza CSS crítico
  • Respeta prefers-reduced-motion y baja la intensidad de animaciones

12. Una experiencia “rápida” accesible: comunicar el tiempo de espera

Tan importante como acelerar es evitar ansiedad mientras el usuario espera.

12.1 Estado de carga explícito

  • aria-busy="true" en la región que se actualiza
  • Anunciar progreso/finalización con role="status"
  • Usa skeletons como decoración y añade también una alternativa textual
<section id="result" aria-busy="true" aria-live="polite">
  <p class="sr-only">Cargando.</p>
  {{-- UI skeleton --}}
</section>

12.2 Rutas de reintento

Ante fallos de carga, ofrece opciones como:

  • “Reintentar”
  • “Intentar más tarde”
  • “Mostrar una versión ligera”

Esto reduce callejones sin salida.


13. Plan mínimo de mejora (adopción por fases recomendada)

  1. Añadir medición en pantallas clave (número/tiempo de SQL, tiempo de API externa)
  2. Arreglar N+1 (with / withCount)
  3. Convertir listados a select + paginación
  4. Cachear agregados de alto costo por 5 minutos
  5. Encolar trabajo pesado de imágenes/PDF/correos
  6. Añadir índices en BD
  7. Añadir caché HTTP / ETag
  8. Añadir monitoreo para detectar regresiones

14. Errores comunes y cómo evitarlos

  • Usar with() en exceso y volver todo más pesado
    • Solución: eager load solo relaciones críticas y limita columnas
  • La caché se vuelve “demasiado vieja” y perjudica
    • Solución: TTL más corto + invalidación al actualizar, o tablas de resumen “materializadas”
  • El COUNT de paginate() se vuelve caro
    • Solución: usar simplePaginate() cuando sea aceptable, o materializar agregados
  • Llamar APIs externas síncronamente para siempre
    • Solución: timeout/reintento/fallback; async si es posible
  • Solo skeleton UI deja el estado poco claro
    • Solución: acompañar con role="status" y un mensaje corto
  • Infinite scroll hace que el usuario se pierda
    • Solución: botón “Cargar más” + anuncios para lectores de pantalla

15. Checklist (para entregar)

Medición

  • [ ] Visualizar p95, conteo/tiempo de SQL, tiempo de API externa y demora de cola en pantallas clave
  • [ ] Configurar alertas por regresiones (p95 / 5xx / latencia)

BD/Eloquent

  • [ ] Eliminar N+1 con with / withCount
  • [ ] Minimizar columnas con select
  • [ ] Paginación en todos los listados
  • [ ] Índices que coinciden con patrones WHERE/ORDER

Caché/Async

  • [ ] Cachear agregados caros con Cache::remember
  • [ ] Incluir tenant/locale en claves de caché
  • [ ] Encolar trabajo pesado; monitorear con Horizon

HTTP/Front-End

  • [ ] Solicitudes condicionales con ETag/304
  • [ ] gzip/brotli, CDN
  • [ ] Optimización de imágenes (WebP, tamaños correctos, lazy)

Accesibilidad

  • [ ] Anunciar carga con aria-busy y role="status"
  • [ ] Progreso no dependiente del color
  • [ ] Rutas de reintento ante fallos
  • [ ] Respetar prefers-reduced-motion

16. Resumen

El rendimiento en Laravel mejora de forma limpia cuando mides y eliminas cuellos de botella con patrones conocidos. Empieza por N+1, paginación y minimización de columnas. Luego pasa a caché y colas para un diseño de “no hagas esperar al usuario”, y consolida la base con índices de BD y caché HTTP. Y la velocidad va de la mano con accesibilidad: no solo acortes esperas—anuncia el estado con texto breve, ofrece reintentos y alternativas, y haz que la experiencia sea calmada y usable para todos.


Enlaces de referencia

Salir de la versión móvil