Geekflare cuenta con el apoyo de nuestra audiencia. Podemos ganar comisiones de afiliados comprando enlaces en este sitio.
Comparte en:

¿Cómo optimizar la aplicación web PHP Laravel para un alto rendimiento?

optimización de laravel
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 uno de ellos. ¡Aprendamos algunos trucos del oficio para hacerlo más rápido!

Ningún desarrollador de PHP está al margen de laravel estos días. Son desarrolladores junior o de nivel medio que aman 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, seguro, habría dejado el mundo PHP hace mucho tiempo si Laravel no estuviera allí).

Un fragmento de autoelogio (algo justificado) de Laravel

Sin embargo, dado que Laravel hace todo lo posible para facilitarle las cosas, significa que en el fondo está haciendo toneladas y toneladas de trabajo para asegurarse de que tenga 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 necesitan ser mejoradas cada vez que se ejecuta una característica. Incluso una simple excepción rastrea qué tan profundo es el agujero del conejo (observe dónde comienza el error, 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 para rastrear. Personalmente, me encontré con 40, y podría haber más fácilmente si está utilizando otras bibliotecas y complementos.

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 imposible responder a esta pregunta por varias razones.

Primero, 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 en comparación con qué? ¿Bajo que condiciones?

Segundo, 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 (ver este), ellos proveen algo marco de referencia y ayúdanos a no volvernos locos. Por lo tanto, con varias pizcas de sal listas, obtengamos una idea aproximada y errónea de la velocidad entre marcos de PHP.

Pasando por este GitHub bastante respetable fuente, así es como se alinean los frameworks PHP en comparación:

Es posible que ni siquiera notes a Laravel aquí (incluso si entrecierras los ojos con mucha fuerza) a menos que eches tu caso hasta el final de la cola. Sí, queridos amigos, ¡Laravel es el último! Ahora, por supuesto, la mayoría de estos "frameworks" no son muy prácticos o incluso útiles, pero nos dice cuán lento 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 números altos. Pero una vez que lo hacen (digamos, más de 200-500 de concurrencia), los servidores comienzan a ahogarse y morir. Es el momento en que incluso lanzar más hardware al problema no es suficiente, y las facturas de infraestructura aumentan tan rápido que sus altos ideales de la computación en la nube se derrumban.

Pero oye, anímate! Este artículo no trata sobre lo que no se puede hacer, sino sobre lo que se puede hacer. 🙂

La buena noticia es que puedes hacer mucho para que tu aplicación Laravel sea más rápida. Varias veces rápido. Sí, no es broma. Puede hacer que el mismo código base se vuelva balístico y ahorrar varios cientos de dólares en facturas de infraestructura / alojamiento cada mes. ¿Cómo? Hagámoslo.

Cuatro tipos de optimizaciones

En mi opinión, la optimización se puede hacer en cuatro niveles distintos (cuando se trata de Aplicaciones PHP, es decir):

  • Nivel de idioma: Esto significa que usa una versión más rápida del lenguaje y evita características / estilos específicos de codificación en el lenguaje que hace que su código sea lento.
  • Nivel de marco: Estas son las cosas que cubriremos en este artículo.
  • Nivel de infraestructura: Ajuste de su administrador de procesos PHP, servidor web, base de datos, etc.
  • Nivel de hardware: Pasar a un hardware mejor, más rápido y más potente proveedor de hosting.

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

Por cierto, la numeración no está justificada y no es un estándar aceptado. Yo solo los inventé. Por favor, no me cite nunca y diga: "Necesitamos optimización de tipo 3 en nuestro servidor", o el líder de su equipo lo matará, me encontrará y luego me matará a mí también. 😀

Y ahora, finalmente, llegamos a la tierra prometida.

Be aware of n+1 database queries

El problema de las consultas n + 1 es común cuando se utilizan ORM. Laravel tiene su poderoso ORM llamado Eloquent, que es tan hermoso, tan conveniente, que a menudo nos olvidamos de ver qué está pasando.

Considere un escenario muy común: mostrar la lista de todos los pedidos realizados por una lista determinada 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 así:

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 que es más importante, elegante, hermoso. 🤩🤩

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

Este es el por qué.

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

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 del controlador.

Ahora recorremos cada cliente uno por uno y recibimos 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 del pedido de 1000 clientes, el número total de consultas de la base de datos ejecutadas será 1 (para obtener todos los datos de los clientes) + 1000 (para obtener los datos del pedido de cada cliente) = 1001. Esto es de donde viene el nombre n + 1.

¿Podemos hacerlo mejor? ¡Ciertamente! Al usar lo que se conoce como carga ansiosa, podemos forzar al ORM a realizar un JOIN y devolver todos los datos necesarios en una sola consulta. Me gusta esto:

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

La estructura de datos resultante es anidada, claro, pero los datos del pedido se pueden extraer fácilmente. La consulta única resultante, en este caso, es algo como esto:

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 sucedería si hubiera 10,000 clientes para procesar! ¡O Dios no lo quiera si también quisiéramos mostrar los elementos contenidos en cada pedido! Recuerde, el nombre de la técnica es carga ansiosa y casi siempre es una buena idea.

Cache the configuration!

Una de las razones de la flexibilidad de Laravel son las toneladas de archivos de configuración que forman parte del marco. ¿Quiere cambiar cómo / dónde se almacenan las imágenes?

Bueno, solo cambia el config/filesystems.php archivo (al menos en el momento de la redacción). ¿Quiere trabajar con varios controladores de cola? Siéntete libre de describirlos en config/queue.php. Acabo de contar y descubrí que hay 13 archivos de configuración para diferentes aspectos del marco, lo que garantiza que no se decepcionará sin importar lo que desee cambiar.

Dada la naturaleza de PHP, cada vez que ingresa una nueva solicitud web, Laravel se activa, inicia todo y analiza todos de estos archivos de configuración para descubrir 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 solicitud es un desperdicio que puede (en realidad, debe) evitarse, y la salida es un comando simple que ofrece Laravel:

php artisan config:cache

Lo que hace es combinar todos los archivos de configuración disponibles en uno solo y el caché está en algún lugar para una rápida recuperación. La próxima vez que haya una solicitud 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 explotar en su 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 volverán null!

Tiene sentido cuando lo piensas. Si usa el almacenamiento en caché de configuración, le está diciendo al marco: "¿Sabes qué? Creo que configuré las cosas bien y estoy 100% seguro de que no quiero que cambien". En otras palabras, espera que el entorno permanezca estático, que es lo que .env los archivos son para.

Dicho esto, aquí hay algunas reglas férreas, sagradas e inquebrantables de almacenamiento en caché de configuraciones:

  1. Hágalo solo en un sistema de producción.
  2. Hágalo solo si está realmente seguro de que desea congelar la configuración.
  3. En caso de que algo salga mal, deshaga la configuración con php artisan cache:clear
  4. ¡Ore para que el daño causado al negocio no sea significativo!

Reduce autoloaded services

Para ser útil, Laravel carga una tonelada de servicios cuando se despierta. Estos están disponibles en el config/app.php archivo como parte del '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, conté, ¡y hay 27 servicios enumerados! Ahora, es posible que los necesite todos, pero es poco probable.

Por ejemplo, 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 visualización, etc. Y como estoy haciendo algunas cosas a mi manera y no sigo los valores predeterminados del marco , También puedo desactivar el proveedor de servicios de autenticación, el proveedor de servicios de paginación, el proveedor de servicios de traducción, etc. Con todo, casi la mitad de estos son innecesarios para mi caso de uso.

Examine detenidamente su solicitud. ¿Necesita todos estos proveedores de servicios? Pero, por el amor de Dios, no comentes ciegamente estos servicios y no pongas en producción. Ejecute todas las pruebas, verifique las cosas manualmente en las máquinas de desarrollo y preparación, y sea muy, muy paranoico antes de apretar el gatillo. 🙂

Be wise with middleware stacks

Cuando necesite algún procesamiento personalizado de la solicitud web entrante, la respuesta es crear un nuevo middleware. Ahora, es tentador abrir app/Http/Kernel.php y pegue el middleware en el web or api apilar; de esa manera, 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) están presentes en cada solicitud, incluso si no hay una razón comercial para ello.

En otras palabras, tenga cuidado de dónde agrega / aplica un nuevo middleware. Puede ser más conveniente agregar algo globalmente, pero la penalización del rendimiento es muy alta a largo plazo. Sé el dolor que tendrías que sufrir si aplicaras el middleware de forma selectiva cada vez que hay un nuevo cambio, ¡pero es un dolor que tomaría y recomendaría de buena gana!

Avoid the ORM (at times)

Si bien Eloquent hace que muchos aspectos de la interacción DB sean placenteros, tiene un costo de velocidad. Al ser un mapeador, el ORM no solo tiene que buscar registros de la base de datos, sino también instanciar los objetos del modelo e hidratarlos (completarlos) con datos de columna.

Entonces, si haces un simple $users = User::all() y hay, digamos, 10,000 usuarios, el marco obtendrá 10,000 filas de la base de datos e internamente hará 10,000 new User() y llenar sus propiedades con los datos relevantes. Se trata de una enorme cantidad de trabajo entre bastidores, y si la base de datos es donde su aplicación se está convirtiendo en un cuello de botella, omitir el ORM a veces es una buena idea.

Esto es especialmente cierto para consultas SQL complejas, donde tendría que saltar muchos obstáculos y escribir cierres sobre cierres y aún así terminar con una consulta eficiente. En tales casos, hacer un DB::raw() y se prefiere escribir la consulta a mano.

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

Use caching as much as possible

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 costosos (costosos 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 encontrarse con que de los 2 millones de productos, la mayoría de las veces las personas están interesadas en los que están recién surtidos, dentro de un cierto rango de precios y para un grupo de edad en particular. Consultar la base de datos para obtener esta información es un desperdicio; dado que la consulta no cambia con frecuencia, es mejor almacenar estos resultados en algún lugar al que podamos acceder rápidamente.

Laravel tiene soporte incorporado para varios tipos de el almacenamiento en caché. Además de usar un controlador de almacenamiento en caché y construir el sistema de almacenamiento en caché desde cero, es posible que desee usar algunos paquetes de Laravel que faciliten almacenamiento en caché de modelos, caché de consultas, etc.

Pero tenga en cuenta que más allá de cierto caso de uso simplificado, los paquetes de almacenamiento en caché prediseñados pueden causar más problemas de los que resuelven.

Prefer in-memory caching

Cuando almacena en caché algo en Laravel, tiene varias opciones de dónde almacenar el cálculo resultante que debe almacenarse en caché. Estas opciones también se conocen como controladores de caché. Entonces, si bien es posible y perfectamente razonable usar el sistema de archivos para almacenar resultados de caché, no es realmente lo que se supone que es el almacenamiento en caché.

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

Ahora, podría pensar que tener un disco SSD es casi lo mismo que usar una memoria RAM, pero ni siquiera está cerca. Incluso informal los puntos de referencia muestran que la RAM supera al SSD entre 10 y 20 veces en cuanto a velocidad se refiere.

Mi sistema favorito cuando se trata de almacenamiento en caché es Redis. Sus ridículamente rápido (100,000 operaciones de lectura por segundo son comunes), y para sistemas de caché muy grandes, se puede convertir en un grupo fácilmente.

Cache the routes

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 no soportas archivos grandes como yo y terminas dividiendo tu web.php y api.php sobre varios archivos. Un solo comando de Laravel empaqueta todas las rutas disponibles y las mantiene a mano para el acceso futuro:

php artisan route:cache

Y cuando termine agregando o cambiando rutas, simplemente haga:

php artisan route:clear

Image optimization and CDN

Las imágenes son el corazón y 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 las aplicaciones / sitios web lentos. Si simplemente almacena las imágenes cargadas de forma ingenua en el servidor y las envía de vuelta en respuestas HTTP, está dejando pasar una gran oportunidad de optimización.

Mi primera recomendación es no almacenar imágenes localmente; existe el problema de la pérdida de datos con el que lidiar y, según la región geográfica en la que se encuentre su cliente, la transferencia de datos puede ser terriblemente lenta.

En su lugar, busque una solución como Cloudinary que cambia el tamaño y optimiza automáticamente las imágenes sobre la marcha.

Si eso no es posible, use algo como Cloudflare para almacenar en caché y servir imágenes mientras están almacenadas en su servidor.

Y si incluso eso no es posible, ajustar un poco el software de su servidor web para comprimir los activos y dirigir el navegador del visitante a almacenar cosas en caché, hace una gran diferencia. Así es como se vería un fragmento de la 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 simple y poderoso (y a menudo se descuida) que no pude evitarlo.

Autoloader optimization

La carga automática es una característica ordenada y no tan antigua de PHP que posiblemente 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 determinada lleva tiempo y puede evitarse en implementaciones de producción donde 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

Make friends with queues

colas es cómo procesa 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 generalizado en las aplicaciones web es enviar algunos correos electrónicos de notificación cuando un usuario realiza algunas acciones.

Por ejemplo, en un producto recién lanzado, es posible que desee que se notifique a los líderes de la empresa (unas 6-7 direcciones de correo electrónico) cada vez que alguien realiza un pedido por encima de cierto valor. Suponiendo que su puerta de enlace de correo electrónico pueda responder a su SMTP solicitud en 500ms, estamos hablando de una buena espera de 3-4 segundos para el usuario antes de que entre en vigor la confirmación del pedido. Una pieza de UX realmente mala, estoy seguro de que estará de acuerdo.

El remedio es almacenar los trabajos a medida que llegan, decirle al usuario que todo salió bien y procesarlos (unos segundos) más tarde. Si hay un error, los trabajos en cola se pueden reintentar varias veces antes de que se declare que fallaron.

Créditos: Microsoft.com

Si bien un sistema de cola complica un poco la configuración (y agrega algunos gastos generales de supervisión), es indispensable en una aplicación web moderna.

Asset optimization (Laravel Mix)

Para cualquier activo de front-end en su aplicación Laravel, asegúrese de que haya una canalización que compile y minimice todos los archivos de activos. Aquellos que se sientan cómodos con un sistema de empaquetado como Webpack, Gulp, Parcel, etc., no necesitan molestarse, pero si aún no lo está haciendo, Mezcla de Laravel es una recomendación sólida.

Mix es un contenedor ligero (y delicioso, ¡con toda honestidad!) De Webpack que se encarga de todos sus archivos CSS, SASS, JS, etc. para producción. Un típico .mix.js El archivo 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, la minificación, la optimización y todo el asunto cuando esté listo para la producción y la ejecución. 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 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 un arte que una ciencia: saber cómo y cuánto hacer es importante que qué hacer. Dicho esto, no hay límite para cuánto y qué puedes optimizar en una aplicación de Laravel.

Pero hagas lo que hagas, me gustaría dejarte un consejo de despedida: la optimización debe realizarse cuando hay una razón sólida, y no porque suene bien o porque estés paranoico con el rendimiento de la aplicación para más de 100,000 usuarios mientras estás en la realidad. solo hay 10.

Si no está seguro de si necesita optimizar su aplicación o no, no necesita patear el proverbial nido de avispas. Una aplicación funcional que se siente aburrida pero que hace exactamente lo que tiene que hacer es diez veces más deseable que una aplicación que ha sido optimizada en una supermáquina híbrida mutante pero que fracasa de vez en cuando.

Y, para que newbiew se convierta en un maestro de Laravel, mira esto curso en línea.

¡Que sus aplicaciones se ejecuten mucho, mucho más rápido! 🙂

Gracias a nuestros patrocinadores
Más lecturas interesantes sobre el desarrollo
Impulse su negocio
Algunas de las herramientas y servicios para ayudar a que su negocio crezca.
  • Invicti utiliza Proof-Based Scanning™ para verificar automáticamente las vulnerabilidades identificadas y generar resultados procesables en cuestión de horas.
    Prueba Invicti
  • Web scraping, proxy residencial, administrador de proxy, desbloqueador web, rastreador de motores de búsqueda y todo lo que necesita para recopilar datos web.
    Prueba Brightdata
  • Semrush es una solución de marketing digital todo en uno con más de 50 herramientas en SEO, redes sociales y marketing de contenido.
    Prueba Semrush
  • Intruder es un escáner de vulnerabilidades en línea que encuentra debilidades de ciberseguridad en su infraestructura, para evitar costosas filtraciones de datos.
    Intente Intruder