Geekflare recibe el apoyo de nuestra audiencia. Podemos ganar comisiones de afiliación de los enlaces de compra en este sitio.
En Desarrollo Última actualización: 14 de septiembre de 2023
Compartir en:
Escáner de seguridad de aplicaciones web Invicti - la única solución que ofrece verificación automática de vulnerabilidades con Proof-Based Scanning™.

Laravel es muchas cosas. Pero rápido no es una de ellas. ¡Aprendamos algunos trucos para hacerlo más rápido!

Ningún desarrollador de PHP se libra de Laravel hoy en día. O bien son desarrolladores junior o de nivel medio a los que les encanta el rápido desarrollo que ofrece Laravel, o bien son desarrolladores senior que se ven obligados a aprender Laravel debido a las presiones del mercado.

En cualquier caso, no se puede negar que Laravel ha revitalizado el ecosistema PHP (yo, sin duda, habría abandonado el mundo PHP hace mucho tiempo si Laravel no estuviera ahí).

Un fragmento de autoelogio (algo justificado) de Laravel

Sin embargo, dado que Laravel hace todo lo posible para facilitarte las cosas, significa que por debajo está haciendo toneladas y toneladas de trabajo para asegurarse de que tengas una vida cómoda como desarrollador. Todas las características "mágicas" de Laravel que parecen funcionar tienen capas y capas de código que necesitan ser creadas cada vez que una característica se ejecuta. Incluso una simple Excepción rastrea lo profunda que es la madriguera del conejo (fíjate donde 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ás usando otras librerías y plugins.

El punto es que, por defecto, estas capas sobre capas de código, hacen que Laravel sea lento.

¿Cómo de lento es Laravel?

Sinceramente, es imposible responder a esta pregunta por varias razones.

En primer lugarno existe una norma aceptada, objetiva y sensata para medir la velocidad de las aplicaciones web. ¿Más rápidas o más lentas en comparación con qué? ¿En qué condiciones?

Segundouna 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 este), proporcionan algunos marco de referencia y ayudarnos 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.

Según este respetable GitHub fuenteAsí es como se comparan los frameworks PHP:

Puede que ni siquiera notes Laravel aquí (incluso si entrecierras los ojos muy fuerte) a menos que eches tu caso hasta el final de la cola. Sí, queridos amigos, ¡Laravel es el último! Ahora, concedido, la mayoría de estos "frameworks" no son muy prácticos o incluso útiles, pero nos dice lo lento que es Laravel en comparación 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 el que ni siquiera basta con añadir más hardware al problema, y las facturas de infraestructura suben tan rápido que tus grandes ideales de computación en nube se vienen abajo.

Pero bueno, ¡ánimo! Este artículo no va de lo que no se puede hacer, sino de lo que sí se puede hacer 🙂 .

La buena noticia es que puedes hacer mucho para que tu aplicación Laravel vaya más rápido. Varias veces más rápido. Sí, no es broma. Puedes hacer que el mismo código base vaya como una bala y ahorrar 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 PHPes decir):

  • Nivel de lengua: Esto significa que utilizas una versión más rápida del lenguaje y evitas características/estilos específicos de codificación en el lenguaje que hacen que tu código sea lento.
  • A nivel de marco: Estos son los temas que trataremos en este artículo.
  • A nivel de infraestructuras: Ajuste de su gestor de procesos PHP, servidor web, base de datos, etc.
  • A nivel de hardware: Cambiar a un hardware mejor, más rápido y más potente proveedor de alojamiento.

Todos estos tipos de optimizaciones tienen su lugar (por ejemplo, Optimización PHP-fpm es bastante crítico y potente). Pero el foco de este artículo serán las optimizaciones puramente de tipo 2: las relacionadas con el framework.

Por cierto, la numeración no tiene ningún fundamento ni es una norma aceptada. Me los acabo de inventar. Por favor, no me cites nunca y digas: "Necesitamos optimización de tipo 3 en nuestro servidor", o el jefe de tu equipo te matará, me encontrará y me matará a mí también 😀.

Y ahora, por fin, llegamos a la tierra prometida.

Tenga en cuenta las consultas a la base de datos n+1

El problema de la consulta n+1 es común cuando se utilizan ORMs. Laravel tiene su potente ORM llamado Eloquent, que es tan bonito, tan cómodo, 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 hace el trabajo de esta manera:

class OrdersController extends Controller 
{
    // ... 

    public function getAllByCustomers(Request $request, array $ids) {
        $customers = Customer::findMany($ids);        
        $orders = collect(); // new collection
        
        foreach ($customers as $customer) {
            $orders = $orders->merge($customer->orders);
        }
        
        return view('admin.reports.orders', ['orders' => $orders]);
    }
}

¡Dulce! Y lo más importante, elegante, preciosa. 🤩🤩

Por desgracia, es un desastroso forma de escribir código en Laravel.

He aquí por qué.

Cuando pedimos al ORM que busque los clientes dados, se genera una consulta SQL como ésta:

SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);

Que es exactamente como se esperaba. Como resultado, todas las filas devueltas se almacenan en la colección $customers dentro de la función de controlador.

Ahora hacemos un bucle sobre cada cliente uno por uno y obtenemos sus pedidos. Esto ejecuta la siguiente consulta . . .

SELECT * FROM orders WHERE customer_id = 22;

. . tantas veces como clientes.

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 que se ejecutarán 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? Por supuesto. Utilizando lo que se conoce como eager loading, podemos forzar al ORM a realizar un JOIN y devolver todos los datos necesarios en una única consulta. Así:

$orders = Customer::findMany($ids)->with('orders')->get();

La estructura de datos resultante es anidada, claro, pero los datos de orden pueden extraerse fácilmente. La consulta única resultante, en este caso, es algo así:

SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);

Una sola consulta es, por supuesto, mejor que mil consultas adicionales. Imagínese lo que pasarí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! Recuerda, el nombre de la técnica es eager loading, y casi siempre es una buena idea.

Guarda 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. Quieres cambiar cómo/dónde se almacenan las imágenes?

Bueno, sólo cambia el config/filesystems.php (al menos en este momento). ¿Quieres trabajar con varios controladores de cola? No dude en 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 te decepcionará independientemente de lo que quieras cambiar.

Dada la naturaleza de PHP, cada vez que llega una nueva petición Web, Laravel se despierta, arranca todo y analiza todos de 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 explotarte en la cara. El mayor problema es que una vez que haya emitido este comando, el env() las llamadas a funciones desde cualquier lugar excepto los archivos de configuración devolverán null!

Tiene sentido si lo piensas. Si usas caché de configuración, le estás 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ás esperando que el entorno permanezca estático, que es lo que .env son para.

Dicho esto, he aquí algunas reglas férreas, sagradas e inquebrantables del almacenamiento en caché de la configuración:

  1. Hágalo sólo en un sistema de producción.
  2. Hazlo sólo si estás muy, muy seguro de que quieres congelar la configuración.
  3. En caso de que algo vaya mal, deshaz el ajuste con php artisan cache:clear
  4. Rezad para que el daño causado al negocio no haya sido importante.

Reducir los servicios de carga automática

Para ser útil, Laravel carga un montón de servicios cuando se despierta. Estos están disponibles en el directorio config/app.php como parte del archivo 'providers' clave de matriz. Echemos un vistazo a lo que tengo en mi caso:

/*
    |--------------------------------------------------------------------------
    | Autoloaded Service Providers
    |--------------------------------------------------------------------------
    |
    | The service providers listed here will be automatically loaded on the
    | request to your application. Feel free to add your own services to
    | this array to grant expanded functionality to your applications.
    |
    */

    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        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,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

    ],

Una vez más, he contado, ¡y hay 27 servicios en la lista! Puede que los necesites todos, pero es poco probable.

Por ejemplo, resulta que estoy construyendo una API REST en este momento, lo que significa que no necesito el Session Service Provider, View Service Provider, 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 comentes ciegamente estos servicios y los pases a producción! Ejecuta todas las pruebas, comprueba las cosas manualmente en las máquinas de desarrollo y ensayo, y sé muy muy paranoico antes de apretar el gatillo 🙂 .

Sea prudente con las pilas de middleware

Cuando se necesita un procesamiento personalizado de la petición Web entrante, la solución es crear un nuevo middleware. Ahora, es tentador abrir app/Http/Kernel.php y pegar el middleware en el web o api stack; de ese modo, 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, ten cuidado con dónde añades/aplicas un nuevo middleware. Puede ser más cómodo añadir algo globalmente, pero la penalización de rendimiento es muy alta a largo plazo. Sé el dolor que tendrías que sufrir si tuvieras que aplicar middleware selectivamente cada vez que hay un nuevo cambio, ¡pero es un dolor que aceptaría de buen grado y recomendaría!

Evitar el ORM (a veces)

Aunque Eloquent hace que muchos aspectos de la interacción con la base de datos 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.

Por lo tanto, si usted 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. Esto es una gran cantidad de trabajo que se hace detrás de las escenas, y si la base de datos es donde su aplicación se está convirtiendo en un cuello de botella, evitando el ORM es una buena idea a veces.

Esto es especialmente cierto en el caso de las consultas SQL complejas, en las que tendrías que saltar muchos obstáculos y escribir cierres sobre cierres para obtener una consulta eficiente. En estos casos, hacer una DB::raw() y es preferible escribir la consulta a mano.

Pasando por este incluso para inserciones sencillas, Eloquent es mucho más lento a medida que aumenta el número de registros:

Utilizar 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 precalcular 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 derroche. Como 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 caché y construir el sistema de caché desde cero, es posible que desee utilizar algunos paquetes de Laravel que facilitan caché de modelos, caché de consultaetc.

Pero tenga en cuenta que, más allá de un determinado caso de uso simplificado, los paquetes de almacenamiento en caché predefinidos pueden causar más problemas de los que resuelven.

Preferir caché en memoria

Cuando cacheas algo en Laravel, tienes varias opciones de dónde almacenar el cálculo resultante que necesita ser cacheado. Estas opciones también se conocen como controladores de caché. Así que, aunque es posible y perfectamente razonable utilizar el sistema de archivos para almacenar los resultados de la caché, no es realmente lo que se pretende con la caché.

Lo ideal es utilizar una caché en memoria (que viva por completo en la RAM) como Redis, Memcached, MongoDB, etc., de modo que bajo cargas más altas, el almacenamiento en caché sirva para un uso vital en lugar de convertirse en un cuello de botella en sí mismo.

Ahora, puede que pienses que tener un disco SSD es casi lo mismo que usar una memoria RAM, pero ni siquiera se acerca. Incluso los puntos de referencia muestran que la RAM supera a las SSD entre 10 y 20 veces en cuanto a velocidad.

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 a un grupo fácilmente.

Almacenar 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 archivos de gran tamaño 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 tenga que añadir o cambiar 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 los mayores consumidores de ancho de banda y una de las principales razones de la lentitud de las aplicaciones/sitios web. Si te limitas a almacenar ingenuamente las imágenes cargadas en el servidor y las devuelves en respuestas HTTP, estás dejando escapar una gran oportunidad de optimización.

Mi primera recomendación es no almacenar las imágenes localmente: existe el problema de la pérdida de datos y, dependiendo de la región geográfica en la que se encuentre el cliente, la transferencia de datos puede ser terriblemente 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, utiliza algo como Cloudflare para almacenar en caché y servir las imágenes mientras están almacenadas en tu 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í un ejemplo de configuración de Nginx:

server {

   # file truncated
    
    # gzip compression settings
    gzip on;
    gzip_comp_level 5;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;

   # browser cache control
   location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
         expires 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 potente (y se descuida tan a menudo) que no he podido evitarlo.

Optimización del cargador automático

La autocarga es una característica 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 ser evitado en despliegues de producción donde el alto rendimiento es deseable. Una vez más, Laravel tiene una solución de un solo comando para esto:

composer install --optimize-autoloader --no-dev

Hazte amigo de las colas

Colas son cómo se procesan las cosas cuando hay muchas 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 consiste en disparar unos cuantos correos de notificación cuando un usuario realiza alguna acción.

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 valor determinado. Suponiendo que su pasarela de correo electrónico pueda responder a sus SMTP en 500ms, estamos hablando de una espera de 3-4 segundos para el usuario antes de que la confirmación del pedido se active. Estoy seguro de que estarás de acuerdo conmigo en que se trata de una mala experiencia de usuario.

El remedio consiste en almacenar los trabajos 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, las tareas en cola pueden reintentarse varias veces antes de declararlas fallidas.

Créditos: Microsoft.com

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 tu aplicación Laravel, por favor asegúrate de que hay un pipeline 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 no estás haciendo esto ya, Laravel Mix es una sólida recomendación.

Mix es una envoltura ligera (y deliciosa, ¡sinceramente!) de Webpack que se encarga de todos tus archivos CSS, SASS, JS, etc., para producción. Un típico .mix.js puede ser tan pequeño como este y aun 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, minificación, optimización y todo el tinglado cuando esté listo para la producción y ejecutar npm run production. Mix se encarga no solo de los archivos JS y CSS tradicionales, sino también de los componentes Vue y React que puedas tener en el flujo de trabajo de tu aplicación.

Seguir leyendo 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 lo que puede optimizar en una aplicación Laravel.

Pero hagas lo que hagas, me gustaría dejarte 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és paranoico sobre el rendimiento de la aplicación para más de 100.000 usuarios cuando en realidad sólo hay 10.

Si no estás seguro de si necesitas optimizar tu aplicación o no, no hace falta que patees 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 ha sido optimizada hasta convertirse en una supermáquina híbrida mutante pero que se cae de vez en cuando.

Y, para newbiew para convertirse en un maestro de Laravel, echa un vistazo a este curso en línea.

Que tus aplicaciones corran mucho, mucho más rápido 🙂 .

  • Ankush
    Autor
Gracias a nuestros patrocinadores
Más lecturas sobre desarrollo
Potencia tu negocio
Algunas de las herramientas y servicios que le ayudarán a hacer crecer su negocio.
  • Invicti utiliza el Proof-Based Scanning™ para verificar automáticamente las vulnerabilidades identificadas y generar resultados procesables en tan solo unas horas.
    Pruebe Invicti
  • Web scraping, proxy residencial, gestor de proxy, desbloqueador web, rastreador de motores de búsqueda, y todo lo que necesita para recopilar datos web.
    Pruebe Brightdata
  • Monday.com es un sistema operativo de trabajo todo en uno que te ayuda a gestionar proyectos, tareas, trabajo, ventas, CRM, operaciones, flujos de trabajo y mucho más.
    Prueba el lunes
  • Intruder es un escáner de vulnerabilidades en línea que encuentra puntos débiles de ciberseguridad en su infraestructura, para evitar costosas violaciones de datos.
    Prueba Intruder