【Guía práctica completa】Optimización del rendimiento y observabilidad en Laravel — Caching, diseño de BD, división de jobs, Octane, métricas y estados de carga accesibles
Qué aprenderás (puntos clave)
- Diseño de caches (Config/Route/View/Query) y caché HTTP
- Eliminación de N+1 de Eloquent, diseño de índices, optimización de agregación y paginación
- División de jobs, limitación de velocidad (throttling), desduplicación y manejo idempotente
- Octane (Swoole/RoadRunner), OPcache y entrega óptima de recursos estáticos
- Monitorización con Horizon, Telescope, logs estructurados y métricas (latencia/tasa de error)
- UI de carga accesible, mejora progresiva e indicaciones de progreso no dependientes del color
Lectores previstos (¿quién se beneficia?)
- Ingenieros Laravel principiantes–intermedios: aumentar velocidad y estabilidad paso a paso
- Tech leads/arquitectos: estandarizar un diseño listo para operaciones con observabilidad
- SRE/QA: ejecutar pruebas de rendimiento y detección de regresiones basadas en métricas
- Diseñadores/redactores: crear pantallas de carga y UIs esqueleto accesibles
1. Principios rectores: “Reduce el desperdicio primero”, luego “hazlo más rápido por diseño”
Si optimizas en el orden equivocado, solo crece la complejidad.
- Medir: identifica solicitudes/consultas/jobs lentos.
- Reducir: N+1, consultas innecesarias, renderizado desperdiciado.
- Cachear: datos, plantillas, HTTP.
- Asincronizar: empuja trabajo pesado a jobs.
- Endurecer la plataforma: OPcache, Octane, CDN.
- Observar: detecta regresiones y habilita rollbacks.
A continuación pasos concretos para apilar mejoras con seguridad.
2. Estrategia de caché: maximiza la superficie de aciertos, controla la invalidación
2.1 Empieza con las caches incorporadas
php artisan config:cache
php artisan route:cache
php artisan view:cache
- Reduce sobrecarga de arranque mediante caches compilados de config/rutas/Blade.
- En producción, OPcache es un hecho. Haz que el deploy/restart refresque caches automáticamente.
2.2 Caché de datos (ejemplo en la capa Repository)
class CategoryRepo {
public function all(): Collection {
return Cache::remember('categories:all', now()->addHours(6), function () {
return Category::query()->orderBy('rank')->get(['id','name','slug']);
});
}
}
- Nomenclatura de claves: piensa en
recurso:condiciones:version
. - Localidad: apunta a listas/diccionarios. Para datos específicos de usuario, usa TTL corto u otra capa.
2.3 Caché de consultas de corta vida
$top = Cache::remember("posts:top:{$page}", 60, fn() =>
Post::with('author')
->published()
->orderByDesc('score')
->paginate(20)
);
- Caché de corta vida para listas pesadas con paginación +
with()
. - Invalida vía eventos (en create/update/delete llama
Cache::forget
).
2.4 Caché HTTP (ETag/Last-Modified)
$etag = sha1($post->updated_at.$post->id);
if (request()->header('If-None-Match') === $etag) {
return response()->noContent(304);
}
return response()->view('post.show', compact('post'))
->header('ETag', $etag)
->header('Cache-Control', 'public, max-age=120');
- Combina con CDN para reducir ancho de banda.
- Para páginas autenticadas, prefiere
private
y vidas cortas.
3. Optimización de Eloquent: N+1, índices, agregación
3.1 Detecta y elimina N+1
// Ejemplo: traer autor y tags juntos
$posts = Post::with(['author:id,name','tags:id,name'])
->latest()->paginate(20);
- Usa
with()
para carga ansiosa de relaciones; selecciona solo columnas necesarias. - Para pantallas con agregados, considera counter caches o tablas preagregadas.
3.2 Diseño de índices
- Añade índices compuestos a columnas en WHERE/ORDER BY/JOIN.
- Para orden
created_at DESC
, estabiliza con(created_at, id)
, etc. - Para búsquedas LIKE, prefiere coincidencias de prefijo (
column LIKE 'abc%'
); considera un almacén aparte para full-text.
3.3 Lista blanca de opciones de búsqueda/orden
$sort = $req->enum('sort', ['-created_at','created_at','-score','score']) ?? '-created_at';
$dir = str_starts_with($sort,'-') ? 'desc' : 'asc';
$col = ltrim($sort,'-');
$query->orderBy($col, $dir);
- Prohíbe
orderBy
arbitrario; lista blanca para ceñirte a rutas optimizadas.
3.4 Agregación separada
- Para agregados pesados de dashboards, materializa periódicamente y deja la vista solo lectura/ligera.
- Si no es necesario en tiempo real, actualiza de forma asíncrona.
4. Optimización de vistas: adelgazar Blade y front-end
4.1 Condicionales y componentes
- Reduce
@include
dentro de bucles grandes; renderiza en lotes de colecciones. - Preformatea helpers pesados mediante ViewModels o Accessors.
4.2 Imágenes y estáticos
- Define width/height en
<img>
para suprimir CLS. - Usa
<picture>
,srcset
yloading="lazy"
para recortar bytes. - Con HTTP/2, empaqueta con criterio; tree-shake CSS/JS no usado en build.
4.3 Estados de carga accesibles
<div role="status" aria-live="polite" class="mb-2">
Loading data…
</div>
<ul aria-busy="true" aria-describedby="loading-desc">
<li class="skeleton h-6 w-full"></li>
<li class="skeleton h-6 w-5/6 mt-2"></li>
</ul>
<p id="loading-desc" class="sr-only">The list will appear once loading completes.</p>
- Las UIs esqueleto deben incluir descripciones de texto.
- Tras completar, pon
aria-busy="false"
y mueve el foco al inicio de la lista para restaurar contexto.
5. Asincronía: división de jobs, desduplicación, throttling
5.1 Descarga trabajo pesado a jobs
class ExportOrders implements ShouldQueue {
public $tries = 3;
public $timeout = 1800; // 30 min
public function handle(ExportService $svc) { $svc->run(); }
}
- Garantiza idempotencia (mismo input, mismo output) para que los reintentos no rompan el estado.
5.2 Desduplicación (jobs únicos)
$lock = Cache::lock("export:{$userId}", 600);
if ($lock->get()) {
ExportOrders::dispatch($userId)->onQueue('exports');
// Release $lock->release() cuando el job termine
}
- Serializa trabajo del mismo tipo por usuario para proteger recursos.
5.3 Middleware de rate-limit
public function middleware(): array {
return [ new \Illuminate\Queue\Middleware\RateLimited('exports') ];
}
- Aplica throttling para APIs externas y envíos de email.
5.4 Fallback en front-end
- Si no hay push en tiempo real, usa polling.
- Mientras carga usa
role="status"
, y en fallo proporciona causa breve + próximos pasos.
6. Octane/OPcache/ajuste de procesos
6.1 Fundamentos de OPcache
- En producción:
opcache.enable=1
,opcache.validate_timestamps=0
(para builds inmutables). - Reinicia procesos en el deploy para refrescar caches.
6.2 Dónde ayuda Laravel Octane
- Reduce sobrecarga de E/S síncrona.
- Comparte sesión/caché/colas vía almacenes externos.
- Evita singletons con estado y contaminación entre requests; reinicializa periódicamente (p. ej.,
Octane::tick()
).
6.3 Servir imágenes/estáticos vía CDN
- Si la app no tiene que servirlos, sirve vía CDN.
- Usa URLs firmadas para acceso con tiempo limitado.
7. Observabilidad: logs, trazas, métricas
7.1 Logs estructurados
Log::info('order.created', [
'order_id' => $order->id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'request_id' => request()->header('X-Request-Id')
]);
- Registra siempre claves buscables (user ID, order ID, request ID).
- Enmascara PII, nunca registres tokens/secretos.
7.2 Latencia, tasa de error, throughput
- Rastrea percentiles (p50/p95/p99) para reflejar experiencia de usuario.
- “Lento ≠ malo” — define SLOs y detecta desviaciones.
- Compara métricas por deploy; haz rollback ante regresiones.
7.3 Telescope/Horizon
- Telescope: visibilidad de requests/queries/exceptions/jobs.
- Horizon: backlogs de colas, jobs fallidos, paneles de tiempos de proceso.
- Restringe en producción; habilítalo brevemente con fines forenses.
8. Paginación y carga incremental
8.1 simplePaginate
e infinite scroll
$items = Item::orderByDesc('id')->simplePaginate(50);
- Evita el coste de consultas de total-count.
- Para scroll infinito, provee también un control manual (botón “Cargar más”).
8.2 Carga accesible
<button id="more" class="btn" aria-controls="list" aria-describedby="load-hint">
Load more
</button>
<p id="load-hint" class="sr-only">Additional results will be appended at the bottom.</p>
<div id="alist" role="feed" aria-busy="false"></div>
- Asegura que sea operable incluso sin auto-carga.
9. Haz que los fallos sean inocuos: timeouts, reintentos, circuit breakers
- Para clientes HTTP, define timeouts explícitos de conexión/respuesta.
- Reintenta errores transitorios con backoff exponencial.
- Ante fallos persistentes, abre un circuito temporal y muestra copia de fallback en la UI.
- Limita el radio de impacto con scopes (transacciones/actualizaciones parciales).
10. Rendimiento de i18n y zonas horarias
- Configura la localización de Carbon una vez en el boot.
- Proporciona traducciones vía JSON o claves nombradas para mejorar ratios de acierto de caché.
- Consolida formato de moneda/fecha en el servidor; minimiza cómputo en cliente.
11. Equilibrio seguridad vs. velocidad
- Aplica límites de tasa más estrictos a operaciones sensibles.
- Aplica CSP/HSTS/Referrer-Policy vía headers, y si usas nonces dinámicos, minimiza la sobrecarga en plantillas.
- Para URLs firmadas y checks de auth, considera rutas que deben evitar caches.
12. UX: guía para estados lentos, fallidos y offline
12.1 Guía ante respuesta lenta
<div id="status" role="status" aria-live="polite">
No response for over 3 seconds. Please check your connection.
</div>
- A medida que pasa el tiempo, muestra guía corta y próximos pasos (recargar, modo lite, etc.).
12.2 Guía ante fallos
- Pon un botón “Intentar de nuevo” en primera línea.
- Muestra un ID de solicitud utilizable por soporte.
- No dependas solo del color—usa icono + texto para estados.
13. Muestras de implementación: de medir cuellos de botella a arreglos
13.1 Mide tiempo de respuesta vía middleware
class RequestTimer {
public function handle($req, Closure $next) {
$start = microtime(true);
$res = $next($req);
$ms = (int)((microtime(true) - $start) * 1000);
Log::info('http.timing', [
'path' => $req->path(),
'status' => $res->getStatusCode(),
'ms' => $ms,
'request_id' => $req->headers->get('X-Request-Id'),
]);
return $res->headers->set('Server-Timing', "app;dur={$ms}");
}
}
- En el panel Network del navegador, inspecciona
Server-Timing
para detectar vistas lentas.
13.2 Visualiza N+1 (solo dev)
DB::listen(function ($query) {
if (str_contains($query->sql, 'select') && $query->time > 30) {
logger()->debug('slow.query', ['sql' => $query->sql, 'ms' => $query->time]);
}
});
- Registra consultas por encima de un umbral.
13.3 Mejoras fundamentales de consulta
- Usa
withCount
yselectRaw
para empujar agregación al DB. - Acumula histórico/estadísticas en tablas separadas para evitar hot paths intensivos en escritura.
14. Checklist (para distribución)
Medición
- [ ] p50/p95/p99, tasa de errores, throughput
- [ ] Logs de consultas/solicitudes lentas
- [ ] Dashboard comparando con el último deploy
Reducción
- [ ] N+1 eliminado (
with()
/ minimización de columnas) - [ ] Índices adecuados
- [ ] Poda de recálculos / ramificación en plantillas
Caché
- [ ] Config/Route/View/OPcache
- [ ] Caché de datos de corta vida + estrategia de invalidación
- [ ] HTTP ETag/Last-Modified/CDN
Asincronía
- [ ] Jobify + reintentos + timeouts
- [ ] Jobs únicos / rate limiting
- [ ] Fallbacks (polling / modo lite)
Plataforma
- [ ] Considerar cobertura de Octane
- [ ] CDN para estáticos
- [ ]
srcset
de imágenes / lazy-load / width & height
Observabilidad
- [ ] Logs estructurados e IDs de solicitud
- [ ] Operación segura de Horizon/Telescope
- [ ] Umbrales de alerta y procedimientos de rollback
Accesibilidad
- [ ] Texto de carga +
role="status"
- [ ] Indicaciones de estado más allá del color; restauración de foco
- [ ] Próximas acciones ante fallo
15. Errores comunes y cómo evitarlos
- Cachear mientras siguen N+1 → Elimina primero, cachea después.
- Caches de larga vida sin invalidación → invalidación dirigida por eventos.
- Pasar orden/búsqueda arbitrarios tal cual → lista blanca para rutas optimizadas.
- Sentirse seguro tras “solo async” → Sin reintentos/idempotencia, los fallos se acumulan.
- Estados de carga silenciosos → Proporciona guía breve y formas de actuar.
- Optimizar sin medir → No sabrás el impacto; ejecuta medir → mejorar → volver a medir.
16. Resumen
- Avanza en orden: medir → reducir → cachear → async → endurecer plataforma → observar.
- Para Eloquent, usa
with()
+ índices y externaliza agregados para aligerar lo fundamental. - Haz los jobs resilientes con desduplicación, límites de tasa, idempotencia.
- Impulsa la velocidad base con OPcache/Octane/CDN.
- Mantén accesibles los flujos de carga/fallo/reintento.
- Pon métricas en dashboards para detectar y revertir regresiones pronto.
El rendimiento no es un arreglo único: es un hábito de medir y mejorar. Usa esta plantilla para hacer crecer un Laravel de equipo rápido y comprensible.
Referencias
- Laravel (oficial)
- Rendimiento/HTTP
- Bases de datos
- Accesibilidad/UX