Laravel es muchas cosas. Pero rápido no es una de ellas. ¡Aprendamos algunos trucos para hacerlo más rápido!
Ningún desarrollador PHP es ajeno a Laravel en estos días. O bien son desarrolladores junior o de nivel medio a los que les encanta el rápido desarrollo que ofrece Laravel, o son desarrolladores senior que se ven obligados a aprender Laravel debido a las presiones del mercado.
De cualquier manera, no se puede negar que Laravel ha revitalizado el ecosistema PHP (yo, con seguridad, habría dejado el mundo PHP hace mucho tiempo si Laravel no estuviera allí).
Sin embargo, ya que Laravel se inclina hacia atrás para hacer las cosas fáciles para usted, significa que por debajo está haciendo toneladas y toneladas de trabajo para asegurarse de que tiene una vida cómoda como desarrollador. Todas las características «mágicas» de Laravel que simplemente parecen funcionar tienen capas sobre capas de código que necesita ser azotado cada vez que una característica se ejecuta. Incluso una simple Excepción rastrea lo profunda que es la madriguera del conejo (fíjese dónde empieza el error, todo el camino hasta el núcleo principal):
Para lo que parece ser un error de compilación en una de las vistas, hay 18 llamadas a funciones que rastrear. Personalmente me he encontrado con 40, y fácilmente podría haber más si está utilizando otras bibliotecas y plugins.
El punto es que, por defecto, estas capas sobre capas de código, hacen que Laravel sea lento.
¿Qué tan lento es Laravel?
Honestamente, es totalmente imposible responder a esta pregunta por varias razones.
En primer lugar, no existe un estándar aceptado, objetivo y sensato para medir la velocidad de las aplicaciones web. ¿Más rápido o más lento comparado con qué? ¿En qué condiciones?
En segundo lugar, una aplicación web depende de tantas cosas (base de datos, sistema de archivos, red, caché, etc.) que es una tontería hablar de velocidad. Una aplicación web muy rápida con una base de datos muy lenta es una aplicación web muy lenta. 🙂
Pero esta incertidumbre es precisamente la razón por la que los puntos de referencia son populares. Aunque no signifiquen nada (véase esto), proporcionan algún marco de referencia y nos ayudan a no volvernos locos. Por lo tanto, con varias pizcas de sal preparadas, vamos a hacernos una idea equivocada y aproximada de la velocidad entre frameworks PHP.
Basándonos en esta fuente de GitHub bastante respetable, así es como se alinean los frameworks PHP cuando se comparan:
Puede que ni siquiera note a Laravel aquí (incluso si entrecierra los ojos muy fuerte) a menos que se lance hasta el final de la cola. Sí, queridos amigos, ¡Laravel ocupa el último lugar! Ahora, concedido, la mayoría de estos «frameworks» no son muy prácticos o incluso útiles, pero nos dice lo lento que es Laravel cuando se compara con otros más populares.
Normalmente, esta «lentitud» no aparece en las aplicaciones porque nuestras aplicaciones web cotidianas rara vez alcanzan cifras elevadas. Pero una vez que lo hacen (digamos, más de 200-500 concurrencias), los servidores empiezan a ahogarse y a morir. Es el momento en que ni siquiera tirar más hardware al problema sirve de nada, y las facturas de infraestructura suben tan rápido que sus altos ideales de computación en nube se vienen abajo.
Pero bueno, ¡ánimo! Este artículo no trata de lo que no se puede hacer, sino de lo que sí se puede 🙂
La buena noticia es que puede hacer mucho para que su aplicación Laravel vaya más rápido. Varias veces más rápido. Sí, no es broma. Puede hacer que el mismo código base vaya como una bala y ahorrarse varios cientos de dólares en facturas de infraestructura/hosting cada mes. ¿Cómo? Vamos a ello.
Cuatro tipos de optimizaciones
En mi opinión, la optimización puede realizarse en cuatro niveles distintos (cuando se trata de aplicaciones PHP, claro)
- A nivel de lenguaje: Esto significa que usted utiliza una versión más rápida del lenguaje y evita características/estilos específicos de codificación en el lenguaje que hacen que su código sea lento.
- Nivel de marco: Estas son las cosas que cubriremos en este artículo.
- A nivel de infraestructura: Puesta a punto de su gestor de procesos PHP, servidor web, base de datos, etc.
- A nivel de hardware: Cambiarse a un proveedor de alojamiento con un hardware mejor, más rápido y más potente.
Todos estos tipos de optimizaciones tienen su lugar (por ejemplo, la optimización PHP-fpm es bastante crítica y potente). Pero el enfoque de este artículo serán las optimizaciones puramente del tipo 2: las relacionadas con el framework.
Por cierto, no hay ningún razonamiento detrás de la numeración, y no es un estándar aceptado. Me las acabo de inventar. Por favor, no me cite nunca y diga: «Necesitamos optimización de tipo 3 en nuestro servidor», o el jefe de su equipo le matará, me encontrará y me matará a mí también. 😀
Y ahora, por fin, llegamos a la tierra prometida.
Tenga cuidado con las consultas a bases de datos n 1
El problema de las consultas n 1 es muy común cuando se utilizan ORMs. Laravel tiene su poderoso ORM llamado Eloquent, que es tan hermoso, tan conveniente, que a menudo nos olvidamos de mirar lo que está pasando.
Consideremos un escenario muy común: mostrar la lista de todos los pedidos realizados por una lista dada de clientes. Esto es bastante común en los sistemas de comercio electrónico y en cualquier interfaz de informes en general, donde necesitamos mostrar todas las entidades relacionadas con algunas entidades.
En Laravel, podríamos imaginar una función de controlador que haga el trabajo así
class PedidosController extends Controlador
{
// ...
public function getAllByCustomers(Solicitud $solicitud, array $ids) {
$clientes = Cliente::findMany($ids);
$pedidos = collect(); // nueva colección
foreach ($clientes como $cliente) {
$pedidos = $pedidos->mezclar($cliente->pedidos);
}
return view('admin.reports.orders', ['orders' => $orders]);
}
}
¡Genial! Y lo más importante, elegante, hermoso. 🤩🤩
Desafortunadamente, es una manera desastrosa de escribir código en Laravel.
He aquí por qué.
Cuando le pedimos al ORM que busque los clientes dados, se genera una consulta SQL como esta:
SELECT * FROM clientes WHERE id IN (22, 45, 34, . . .);
Que es exactamente como se esperaba. Como resultado, todas las filas devueltas se almacenan en la colección $clientes
dentro de la función del controlador.
Ahora hacemos un bucle sobre cada cliente uno por uno y obtenemos sus pedidos. Para ello se ejecuta la siguiente consulta
SELECT * FROM pedidos WHERE customer_id = 22. ;
. . tantas veces como clientes haya.
En otras palabras, si necesitamos obtener los datos de los pedidos de 1000 clientes, el número total de consultas a la base de datos ejecutadas será 1 (para obtener los datos de todos los clientes) 1000 (para obtener los datos de los pedidos de cada cliente) = 1001. De ahí viene el nombre n 1.
¿Podemos hacerlo mejor? Desde luego Utilizando lo que se conoce como eager loading, ¡podemos forzar al ORM a realizar un JOIN y devolver todos los datos necesarios en una sola consulta! Así
$pedidos = Cliente::findMany($ids)->with('pedidos')->get();
La estructura de datos resultante es anidada, seguro, pero los datos de los pedidos pueden extraerse fácilmente. La consulta única resultante, en este caso, es algo así
SELECT * FROM clientes INNER JOIN pedidos ON clientes.id = pedidos.customer_id WHERE clientes.id IN (22, 45, . . .);
Una sola consulta es, por supuesto, mejor que mil consultas adicionales. Imagine lo que ocurriría si hubiera que procesar 10.000 clientes O, Dios no lo quiera, ¡si también quisiéramos mostrar los artículos contenidos en cada pedido! Recuerde, el nombre de la técnica es eager loading, y casi siempre es una buena idea.
¡Almacene en caché la configuración!
Una de las razones de la flexibilidad de Laravel son las toneladas de archivos de configuración que forman parte del framework. ¿Quiere cambiar cómo/dónde se almacenan las imágenes?
Bueno, sólo tiene que cambiar el archivo config/filesystems.
php (al menos en el momento de escribir esto). ¿Quiere trabajar con varios controladores de cola? Siéntase libre de describirlos en config/queue
.php. Acabo de contar y he descubierto que hay 13 archivos de configuración para diferentes aspectos del framework, lo que garantiza que no se sentirá decepcionado independientemente de lo que quiera cambiar.
Dada la naturaleza de PHP, cada vez que entra una nueva petición web, Laravel se despierta, arranca todo, y analiza todos estos archivos de configuración para averiguar cómo hacer las cosas de manera diferente esta vez. ¡Excepto que es estúpido si nada ha cambiado en los últimos días! Reconstruir la configuración en cada petición es un desperdicio que puede ser (en realidad, debe ser) evitado, y la salida es un simple comando que Laravel ofrece:
php artisan config:cache
Lo que esto hace es combinar todos los archivos de configuración disponibles en uno solo y almacenarlo en caché en algún lugar para una rápida recuperación. La próxima vez que haya una petición web, Laravel simplemente leerá este único archivo y se pondrá en marcha.
Dicho esto, el almacenamiento en caché de la configuración es una operación extremadamente delicada que puede estallarle en la cara. El mayor inconveniente es que una vez que haya emitido este comando, ¡las llamadas a la función env(
) desde cualquier lugar excepto los archivos de configuración devolverán null
!
Tiene sentido si lo piensa. Si utiliza el almacenamiento en caché de la configuración, le está diciendo al framework: «Sabes qué, creo que he configurado bien las cosas y estoy 100% seguro de que no quiero que cambien» En otras palabras, está esperando que el entorno permanezca estático, que es para lo que sirven los archivos .env
.
Dicho esto, he aquí algunas reglas férreas, sagradas e inquebrantables del almacenamiento en caché de la configuración:
- Hágalo sólo en un sistema de producción.
- Hágalo sólo si está realmente, realmente seguro de que desea congelar la configuración.
- En caso de que algo salga mal, deshaga la configuración con
php artisan cache:clear
- Rece para que el daño causado no sea significativo
Reduzca los servicios autocargados
Para ser útil, Laravel carga una tonelada de servicios cuando se despierta. Estos están disponibles en el archivo config/app.
php como parte de la clave del array 'providers
‘. Echemos un vistazo a lo que tengo en mi caso:
/*
|--------------------------------------------------------------------------
| Proveedores de servicios autocargados
|--------------------------------------------------------------------------
|
| Los proveedores de servicios listados aquí se cargarán automáticamente en la
| solicitud a su aplicación. Siéntase libre de añadir sus propios servicios a
| esta matriz para otorgar funcionalidad ampliada a sus aplicaciones.
|
*/
'providers' => [
/*
* Proveedores de servicios del framework Laravel...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class,
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Paquete de proveedores de servicios...
*/
/*
* Proveedores de servicios de aplicaciones...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
AppProviders\EventServiceProvider::class,
AppProviders\RouteServiceProvider::class,
],
Una vez más, conté, ¡y hay 27 servicios en la lista! Ahora bien, puede que los necesite todos, pero es poco probable.
Por ejemplo, resulta que estoy construyendo una API REST en este momento, lo que significa que no necesito el Proveedor de Servicios de Sesión, el Proveedor de Servicios de Vista, etc. Y como estoy haciendo algunas cosas a mi manera y no siguiendo los valores predeterminados del framework, también puedo desactivar Auth Service Provider, Pagination Service Provider, Translation Service Provider, etc. En total, casi la mitad de estos son innecesarios para mi caso de uso.
Analice detenidamente su aplicación. ¿Necesita todos estos proveedores de servicios? Pero, por el amor de Dios, ¡no comente ciegamente estos servicios y los pase a producción! Ejecute todas las pruebas, compruebe las cosas manualmente en las máquinas dev y staging, y sea muy muy paranoico antes de apretar el gatillo 🙂
Sea prudente con las pilas de middleware
Cuando necesite algún tipo de procesamiento personalizado de la solicitud Web entrante, crear un nuevo middleware es la respuesta. Ahora, es tentador abrir app/Http/Kernel.php
y meter el middleware en la pila web
o api
; de esa forma, estará disponible en toda la aplicación y si no está haciendo algo intrusivo (como registrar o notificar, por ejemplo).
Sin embargo, a medida que la aplicación crece, esta colección de middleware global puede convertirse en una carga silenciosa para la aplicación si todos (o la mayoría) de ellos están presentes en cada solicitud, incluso si no hay ninguna razón de negocio para ello.
En otras palabras, tenga cuidado de dónde añade/aplica un nuevo middleware. Puede ser más cómodo añadir algo de forma global, pero la penalización de rendimiento es muy alta a largo plazo. Sé el dolor que tendría que sufrir si tuviera que aplicar middleware de forma selectiva cada vez que hay un nuevo cambio, ¡pero es un dolor que aceptaría de buen grado y que recomiendo!
Evite el ORM (a veces)
Aunque Eloquent hace que muchos aspectos de la interacción con la BD sean placenteros, lo hace a costa de la velocidad. Al ser un mapeador, el ORM no sólo tiene que obtener registros de la base de datos, sino también instanciar los objetos modelo e hidratarlos (rellenarlos) con datos de columnas.
Así, si hace un simple $users = User::all()
y hay, digamos, 10.000 usuarios, el framework obtendrá 10.000 filas de la base de datos e internamente hará 10.000 new User()
y rellenará sus propiedades con los datos relevantes. Se trata de grandes cantidades de trabajo que se realizan entre bastidores, y si la base de datos es donde su aplicación se está convirtiendo en un cuello de botella, evitar el ORM es una buena idea en ocasiones.
Esto es especialmente cierto para las consultas SQL complejas, donde tendría que saltar un montón de aros y escribir cierres sobre cierres y aún así terminar con una consulta eficiente. En tales casos, es preferible hacer un DB::raw()
y escribir la consulta a mano.
A juzgar por este estudio de rendimiento, incluso para inserciones sencillas Eloquent es mucho más lento a medida que aumenta el número de registros:
Utilice la caché en la medida de lo posible
Uno de los secretos mejor guardados de la optimización de aplicaciones web es el almacenamiento en caché.
Para los no iniciados, el almacenamiento en caché significa precomputar y almacenar resultados caros (caros en términos de uso de CPU y memoria), y simplemente devolverlos cuando se repite la misma consulta.
Por ejemplo, en una tienda de comercio electrónico, puede ocurrir que de los 2 millones de productos, la mayoría de las veces la gente esté interesada en los que están recién almacenados, dentro de un determinado rango de precios y para un grupo de edad concreto. Consultar la base de datos para obtener esta información es un desperdicio — puesto que la consulta no cambia a menudo, es mejor almacenar estos resultados en algún lugar al que podamos acceder rápidamente.
Laravel tiene soporte incorporado para varios tipos de almacenamiento en caché. Además de utilizar un controlador de almacenamiento en caché y construir el sistema de almacenamiento en caché desde cero, es posible que desee utilizar algunos paquetes de Laravel que facilitan el almacenamiento en caché de modelos, almacenamiento en caché de consultas, etc.
Pero tenga en cuenta que más allá de un determinado caso de uso simplificado, los paquetes de almacenamiento en caché preconstruidos pueden causar más problemas de los que resuelven.
Prefiera el almacenamiento en caché en memoria
Cuando almacena algo en caché en Laravel, tiene varias opciones de dónde almacenar el cálculo resultante que necesita ser almacenado en caché. Estas opciones también se conocen como controladores de caché. Así, mientras que es posible y perfectamente razonable utilizar el sistema de archivos para almacenar los resultados de la caché, no es realmente lo que el almacenamiento en caché está destinado a ser.
Lo ideal es utilizar una caché en memoria (que viva enteramente en la RAM) como Redis, Memcached, MongoDB, etc., de forma que, bajo cargas más elevadas, la caché sirva para un uso vital en lugar de convertirse ella misma en un cuello de botella.
Ahora bien, podría pensar que tener un disco SSD es casi lo mismo que utilizar una memoria RAM, pero ni siquiera está cerca. Incluso los puntos de referencia informales muestran que la RAM supera a la SSD entre 10 y 20 veces en lo que a velocidad se refiere.
Mi sistema favorito cuando se trata de almacenamiento en caché es Redis. Es ridículamente rápido (100.000 operaciones de lectura por segundo son comunes), y para sistemas de caché muy grandes, puede evolucionar en un clúster fácilmente.
Almacene en caché las rutas
Al igual que la configuración de la aplicación, las rutas no cambian mucho con el tiempo y son un candidato ideal para el almacenamiento en caché. Esto es especialmente cierto si usted no puede soportar los archivos grandes como yo y terminan dividiendo su web.php
y api.
php en varios archivos. Un único comando de Laravel empaqueta todas las rutas disponibles y las mantiene a mano para futuros accesos:
php artisan route:cache
Y cuando acabe añadiendo o cambiando rutas, simplemente hágalo:
php artisan route:clear
Optimización de imágenes y CDN
Las imágenes son el alma de la mayoría de las aplicaciones web. Casualmente, también son las mayores consumidoras de ancho de banda y una de las mayores razones de la lentitud de las aplicaciones/sitios web. Si se limita a almacenar ingenuamente las imágenes cargadas en el servidor y a enviarlas de vuelta en las respuestas HTTP, está dejando escapar una gran oportunidad de optimización.
Mi primera recomendación es no almacenar las imágenes localmente — hay que lidiar con el problema de la pérdida de datos, y dependiendo de la región geográfica en la que se encuentre su cliente, la transferencia de datos puede ser dolorosamente lenta.
En su lugar, opte por una solución como Cloudinary que redimensiona y optimiza automáticamente las imágenes sobre la marcha.
Si eso no es posible, utilice algo como Cloudflare para almacenar en caché y servir las imágenes mientras están almacenadas en su servidor.
Y si ni siquiera eso es posible, ajustar un poco el software de su servidor web para comprimir los activos y dirigir el navegador del visitante para que almacene las cosas en caché, marca una gran diferencia. He aquí cómo quedaría un fragmento de configuración de Nginx:
servidor {
# archivo truncado
# configuración de compresión gzip
gzip activado
gzip_comp_level 5;;
gzip_min_length 256;
gzip_proxied any;
gzip_vary on;
# control de caché del navegador
location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
caduca 1d;
access_log off;
add_header Pragma public;
add_header Cache-Control "public, max-age=86400";
}
}
Soy consciente de que la optimización de imágenes no tiene nada que ver con Laravel, pero es un truco tan sencillo y poderoso (y se descuida tan a menudo) que no he podido evitarlo.
Optimización del autoloader
La autocarga es una característica limpia y no tan antigua de PHP que podría decirse que salvó al lenguaje de la perdición. Dicho esto, el proceso de encontrar y cargar la clase relevante descifrando una cadena de espacio de nombres dada lleva tiempo y puede evitarse en despliegues de producción en los que se desea un alto rendimiento. Una vez más, Laravel tiene una solución de un solo comando para esto:
composer install --optimize-autoloader --no-dev
Hágase amigo de las colas
Las colas son la forma de procesar las cosas cuando hay muchas de ellas, y cada una de ellas tarda unos milisegundos en completarse. Un buen ejemplo es el envío de correos electrónicos — un caso de uso muy extendido en las aplicaciones web es disparar unos cuantos correos de notificación cuando un usuario realiza algunas acciones.
Por ejemplo, en un producto recién lanzado, es posible que desee que la dirección de la empresa (unas 6-7 direcciones de correo electrónico) reciba una notificación cada vez que alguien realice un pedido por encima de un determinado valor. Suponiendo que su pasarela de correo electrónico pueda responder a su solicitud SMTP en 500 ms, estamos hablando de una buena espera de 3-4 segundos para el usuario antes de que se produzca la confirmación del pedido. Una muy mala pieza de UX, estoy seguro de que estará de acuerdo.
El remedio es almacenar los pedidos a medida que llegan, informar al usuario de que todo ha ido bien y procesarlos (unos segundos) más tarde. Si se produce un error, los trabajos en cola pueden reintentarse unas cuantas veces antes de que se declare que han fallado.
Aunque un sistema de colas complica un poco la configuración (y añade cierta sobrecarga de supervisión), es indispensable en una aplicación web moderna.
Optimización de activos (Laravel Mix)
Para cualquier activo front-end en su aplicación Laravel, asegúrese de que hay una tubería que compila y minifica todos los archivos de activos. Aquellos que se sientan cómodos con un sistema bundler como Webpack, Gulp, Parcel, etc, no necesitan molestarse, pero si usted no está haciendo esto todavía, Laravel Mix es una recomendación sólida.
Mix es una envoltura ligera (¡y encantadora, sinceramente!) alrededor de Webpack que se encarga de todos sus archivos CSS, SASS, JS, etc., para la producción. Un típico archivo .mix.js
puede ser tan pequeño como este y aún así hacer maravillas:
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
Esto se encarga automáticamente de las importaciones, la minificación, la optimización y todo el tinglado cuando esté listo para la producción y ejecute npm run production
. Mix se encarga no sólo de los archivos JS y CSS tradicionales, sino también de los componentes Vue y React que pueda tener en el flujo de trabajo de su aplicación.
Más información aquí
Conclusión
La optimización del rendimiento es más arte que ciencia — saber cómo y cuánto hacer es más importante que qué hacer. Dicho esto, no hay fin a cuánto y qué todo puede optimizar en una aplicación Laravel.
Pero haga lo que haga, me gustaría dejarle con un consejo de despedida — la optimización debe hacerse cuando hay una razón sólida, y no porque suene bien o porque esté paranoico sobre el rendimiento de la aplicación para 100.000 usuarios cuando en realidad sólo hay 10.
Si no está seguro de si necesita optimizar su aplicación o no, no hace falta que patee el proverbial avispero. Una aplicación que funciona, que parece aburrida pero que hace exactamente lo que tiene que hacer es diez veces más deseable que una aplicación que se ha optimizado hasta convertirla en una supermáquina híbrida mutante pero que se cae de vez en cuando.
Y, para los novatos que quieran convertirse en maestros de Laravel, echen un vistazo a este curso en línea.
¡Que sus aplicaciones corran mucho, mucho más rápido! 🙂