Comment optimiser l’application Web PHP Laravel pour des performances élevées ?

Laravel est beaucoup de choses. Mais rapide n’en fait pas partie. Apprenons quelques ficelles du métier pour que ça aille plus vite !

Aucun développeur PHP n’est épargné par Laravel ces jours-ci. Il s’agit soit d’un développeur junior ou intermédiaire qui aime le développement rapide qu’offre Laravel, soit d’un développeur senior qui est obligé d’apprendre Laravel en raison des pressions du marché.

Quoi qu’il en soit, il est indéniable que Laravel a revitalisé l’écosystème PHP (j’aurais certainement quitté le monde PHP il y a longtemps si Laravel n’était pas là).

Un extrait d’auto-éloge (quelque peu justifié) de Laravel

Cependant, puisque Laravel se plie en quatre pour vous faciliter la tâche, cela signifie qu’en dessous, il fait des tonnes et des tonnes de travail pour vous assurer une vie confortable en tant que développeur. Toutes les fonctionnalités « magiques » de Laravel qui semblent fonctionner ont des couches de code qui doivent être retravaillées à chaque fois qu’une fonctionnalité s’exécute. Même une simple exception trace la profondeur du terrier du lapin (notez où l’erreur commence, jusqu’au noyau principal):

Pour ce qui semble être une erreur de compilation dans l’une des vues, il y a 18 appels de fonction à tracer. J’en ai personnellement rencontré 40, et il pourrait facilement y en avoir plus si vous utilisez d’autres bibliothèques et plugins.

Le fait étant que, par défaut, ces couches sur des couches de code rendent Laravel lent.

Quelle est la lenteur de Laravel ?

Honnêtement, il est tout simplement impossible de répondre à cette question pour plusieurs raisons.

Premièrement, il n’y a pas de norme acceptée, objective et sensée pour mesurer la vitesse des applications Web. Plus rapide ou plus lent par rapport à quoi ? Sous quelles conditions?

Deuxièmement, une application Web dépend de tant de choses (base de données, système de fichiers, réseau, cache, etc.) qu’il est tout simplement idiot de parler de vitesse. Une application Web très rapide avec une base de données très lente est une application Web très lente. 🙂

Mais cette incertitude est précisément la raison pour laquelle les indices de référence sont populaires. Même s’ils ne veulent rien dire (voir cette et cette), ils fournissent un cadre de référence et nous aident à ne pas devenir fous. Par conséquent, avec quelques pincées de sel prêtes, donnons-nous une idée approximative de la vitesse parmi les frameworks PHP.

En passant par ce GitHub plutôt respectable la sourcevoici comment les frameworks PHP s’alignent lorsqu’ils sont comparés :

Vous ne remarquerez peut-être même pas Laravel ici (même si vous louchez très fort) à moins que vous ne jetiez votre étui jusqu’au bout de la queue. Oui, chers amis, Laravel vient en dernier ! Certes, la plupart de ces « frameworks » ne sont pas très pratiques ni même utiles, mais cela nous indique à quel point Laravel est lent par rapport à d’autres plus populaires.

Normalement, cette « lenteur » ne figure pas dans les applications, car nos applications Web quotidiennes atteignent rarement des chiffres élevés. Mais une fois qu’ils le font (disons, plus de 200 à 500 simultanés), les serveurs commencent à s’étouffer et à mourir. C’est le moment où même jeter plus de matériel sur le problème ne le résout pas, et les factures d’infrastructure grimpent si vite que vos idéaux élevés de cloud computing s’effondrent.

Mais bon, rassurez-vous ! Cet article ne traite pas de ce qui ne peut pas être fait, mais de ce qui peut être fait. 🙂

La bonne nouvelle est que vous pouvez faire beaucoup pour accélérer votre application Laravel. Plusieurs fois rapide. Oui, sans blague. Vous pouvez rendre la même base de code balistique et économiser plusieurs centaines de dollars sur les factures d’infrastructure/d’hébergement chaque mois. Comment? Allons-y.

Quatre types d’optimisations

À mon avis, l’optimisation peut se faire à quatre niveaux distincts (en ce qui concerne les applications PHP, c’est-à-dire) :

  • Au niveau du langage : cela signifie que vous utilisez une version plus rapide du langage et évitez les fonctionnalités/styles de codage spécifiques dans le langage qui ralentissent votre code.
  • Au niveau du framework : Ce sont les choses que nous allons couvrir dans cet article.
  • Au niveau de l’infrastructure : Optimisez votre gestionnaire de processus PHP, votre serveur Web, votre base de données, etc.
  • Au niveau du matériel : Passer à un fournisseur d’hébergement de matériel meilleur, plus rapide et plus puissant.

Tous ces types d’optimisations ont leur place (par exemple, l’optimisation PHP-fpm est assez critique et puissante). Mais l’objet de cet article sera les optimisations purement de type 2 : celles liées au framework.

Soit dit en passant, il n’y a aucune justification derrière la numérotation, et ce n’est pas une norme acceptée. Je viens de les inventer. S’il vous plaît, ne me citez jamais et ne dites jamais : « Nous avons besoin d’une optimisation de type 3 sur notre serveur », sinon votre chef d’équipe vous tuera, me trouvera, puis me tuera également. 😀

Et maintenant, enfin, nous arrivons à la terre promise.

Soyez conscient des requêtes de base de données n + 1

Le problème de requête n+1 est courant lorsque les ORM sont utilisés. Laravel a son puissant ORM appelé Eloquent, qui est si beau, si pratique, qu’on oublie souvent de regarder ce qui se passe.

Prenons un scénario très courant : afficher la liste de toutes les commandes passées par une liste donnée de clients. Ceci est assez courant dans les systèmes de commerce électronique et dans toutes les interfaces de reporting en général où nous devons afficher toutes les entités liées à certaines entités.

Dans Laravel, on pourrait imaginer une fonction de contrôleur qui fait le travail comme ceci :

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]);
    }
}

Sucré! Et plus important encore, élégant, beau. 🤩🤩

Malheureusement, c’est une façon désastreuse d’écrire du code dans Laravel.

Voici pourquoi.

Lorsque nous demandons à l’ORM de rechercher les clients donnés, une requête SQL comme celle-ci est générée :

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

Ce qui est exactement comme prévu. En conséquence, toutes les lignes renvoyées sont stockées dans la collection $customers à l’intérieur de la fonction du contrôleur.

Maintenant, nous passons en revue chaque client un par un et obtenons leurs commandes. Ceci exécute la requête suivante. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . autant de fois qu’il y a de clients.

En d’autres termes, si nous devons obtenir les données de commande pour 1000 clients, le nombre total de requêtes de base de données exécutées sera 1 (pour récupérer toutes les données des clients) + 1000 (pour récupérer les données de commande pour chaque client) = 1001. Cela c’est de là que vient le nom n+1.

Peut-on faire mieux ? Assurément! En utilisant ce qu’on appelle le chargement hâtif, nous pouvons forcer l’ORM à effectuer un JOIN et à renvoyer toutes les données nécessaires en une seule requête ! Comme ça:

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

La structure de données résultante est imbriquée, bien sûr, mais les données de commande peuvent être facilement extraites. La requête unique résultante, dans ce cas, ressemble à ceci :

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

Une seule requête vaut bien sûr mieux que mille requêtes supplémentaires. Imaginez ce qui se passerait s’il y avait 10 000 clients à traiter ! Ou Dieu nous en préserve si nous voulions également afficher les articles contenus dans chaque commande ! N’oubliez pas que le nom de la technique est chargement avide, et c’est presque toujours une bonne idée.

Cachez la configuration !

L’une des raisons de la flexibilité de Laravel est la multitude de fichiers de configuration qui font partie du framework. Vous voulez changer comment/où les images sont stockées ?

Eh bien, changez simplement le fichier config/filesystems.php (au moins au moment de l’écriture). Vous souhaitez travailler avec plusieurs pilotes de file d’attente ? N’hésitez pas à les décrire dans config/queue.php. Je viens de compter et j’ai trouvé qu’il y avait 13 fichiers de configuration pour différents aspects du framework, garantissant que vous ne serez pas déçu, peu importe ce que vous voulez changer.

Compte tenu de la nature de PHP, chaque fois qu’une nouvelle requête Web arrive, Laravel se réveille, démarre tout et analyse tous ces fichiers de configuration pour déterminer comment faire les choses différemment cette fois. Sauf que c’est con si rien n’a bougé ces derniers jours ! Reconstruire la configuration à chaque requête est un gaspillage qui peut (en fait, doit être) évité, et la solution est une simple commande proposée par Laravel :

php artisan config:cache

Cela combine tous les fichiers de configuration disponibles en un seul et le cache est quelque part pour une récupération rapide. La prochaine fois qu’il y aura une requête Web, Laravel lira simplement ce fichier unique et commencera.

Cela dit, la mise en cache de la configuration est une opération extrêmement délicate qui peut vous exploser au visage. Le plus gros piège est qu’une fois que vous avez émis cette commande, les appels de fonction env() de partout sauf les fichiers de configuration renverront null !

Cela a du sens quand on y pense. Si vous utilisez la mise en cache de la configuration, vous dites au framework : « Vous savez quoi, je pense que j’ai bien configuré les choses et je suis sûr à 100 % que je ne veux pas qu’elles changent. » En d’autres termes, vous vous attendez à ce que l’environnement reste statique, ce à quoi servent les fichiers .env.

Cela dit, voici quelques règles inviolables, sacrées et incassables de la mise en cache de la configuration :

  • Ne le faites que sur un système de production.
  • Ne le faites que si vous êtes vraiment, vraiment sûr de vouloir geler la configuration.
  • En cas de problème, annulez le paramètre avec php artisan cache:clear
  • Priez pour que les dommages causés à l’entreprise ne soient pas importants !
  • Réduire les services chargés automatiquement

    Pour être utile, Laravel charge une tonne de services lorsqu’il se réveille. Ceux-ci sont disponibles dans le fichier config/app.php dans le cadre de la clé de tableau ‘providers’. Voyons ce que j’ai dans mon cas :

    /*
        |--------------------------------------------------------------------------
        | 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...
             */        IlluminateAuthAuthServiceProvider::class,
            IlluminateBroadcastingBroadcastServiceProvider::class,
            IlluminateBusBusServiceProvider::class,
            IlluminateCacheCacheServiceProvider::class,
            IlluminateFoundationProvidersConsoleSupportServiceProvider::class,
            IlluminateCookieCookieServiceProvider::class,
            IlluminateDatabaseDatabaseServiceProvider::class,
            IlluminateEncryptionEncryptionServiceProvider::class,
            IlluminateFilesystemFilesystemServiceProvider::class,
            IlluminateFoundationProvidersFoundationServiceProvider::class,
            IlluminateHashingHashServiceProvider::class,
            IlluminateMailMailServiceProvider::class,
            IlluminateNotificationsNotificationServiceProvider::class,
            IlluminatePaginationPaginationServiceProvider::class,
            IlluminatePipelinePipelineServiceProvider::class,
            IlluminateQueueQueueServiceProvider::class,
            IlluminateRedisRedisServiceProvider::class,
            IlluminateAuthPasswordsPasswordResetServiceProvider::class,
            IlluminateSessionSessionServiceProvider::class,
            IlluminateTranslationTranslationServiceProvider::class,
            IlluminateValidationValidationServiceProvider::class,
            IlluminateViewViewServiceProvider::class,
    
            /*
             * Package Service Providers...
             */
            /*
             * Application Service Providers...
             */        AppProvidersAppServiceProvider::class,
            AppProvidersAuthServiceProvider::class,
            // AppProvidersBroadcastServiceProvider::class,
            AppProvidersEventServiceProvider::class,
            AppProvidersRouteServiceProvider::class,
    
        ],

    Encore une fois, j’ai compté, et il y a 27 services répertoriés ! Maintenant, vous pouvez avoir besoin de tous, mais c’est peu probable.

    Par exemple, je suis en train de créer une API REST pour le moment, ce qui signifie que je n’ai pas besoin du fournisseur de services de session, du fournisseur de services d’affichage, etc. , je peux également désactiver le fournisseur de services d’authentification, le fournisseur de services de pagination, le fournisseur de services de traduction, etc. Dans l’ensemble, près de la moitié d’entre eux sont inutiles pour mon cas d’utilisation.

    Examinez longuement votre candidature. A-t-il besoin de tous ces fournisseurs de services ? Mais pour l’amour de Dieu, s’il vous plaît, ne commentez pas aveuglément ces services et poussez à la production ! Exécutez tous les tests, vérifiez les choses manuellement sur les machines de développement et de mise en scène, et soyez très très paranoïaque avant d’appuyer sur la gâchette. 🙂

    Soyez prudent avec les piles middleware

    Lorsque vous avez besoin d’un traitement personnalisé de la requête Web entrante, la création d’un nouveau middleware est la solution. Maintenant, il est tentant d’ouvrir app/Http/Kernel.php et de coller le middleware dans la pile Web ou API ; de cette façon, il devient disponible dans l’application et s’il ne fait pas quelque chose d’intrusif (comme la journalisation ou la notification, par exemple).

    Cependant, à mesure que l’application se développe, cette collection d’intergiciels mondiaux peut devenir un fardeau silencieux pour l’application si tous (ou la majorité) d’entre eux sont présents dans chaque demande, même s’il n’y a aucune raison commerciale à cela.

    En d’autres termes, faites attention à l’endroit où vous ajoutez/appliquez un nouveau middleware. Il peut être plus pratique d’ajouter quelque chose globalement, mais la pénalité de performance est très élevée à long terme. Je connais la douleur que vous auriez à subir si vous deviez appliquer sélectivement le middleware à chaque fois qu’il y a un nouveau changement, mais c’est une douleur que je prendrais volontiers et que je recommanderais !

    Évitez l’ORM (parfois)

    Bien qu’Eloquent rende agréables de nombreux aspects de l’interaction DB, cela se fait au détriment de la vitesse. En tant que mappeur, l’ORM doit non seulement récupérer les enregistrements de la base de données, mais également instancier les objets du modèle et les hydrater (les remplir) avec des données de colonne.

    Donc, si vous faites un simple $users = User::all() et qu’il y a, disons, 10 000 utilisateurs, le framework récupérera 10 000 lignes de la base de données et fera en interne 10 000 nouveaux User() et remplira leurs propriétés avec les données pertinentes . Il s’agit d’une énorme quantité de travail effectué en coulisses, et si la base de données est l’endroit où votre application devient un goulot d’étranglement, contourner l’ORM est parfois une bonne idée.

    Cela est particulièrement vrai pour les requêtes SQL complexes, où vous devriez sauter beaucoup de cerceaux et écrire des fermetures sur fermetures et vous retrouver avec une requête efficace. Dans de tels cas, il est préférable de faire un DB::raw() et d’écrire la requête à la main.

    En passant cette étude des performances, même pour de simples inserts Eloquent est d’autant plus lent que le nombre d’enregistrements augmente :

    Utilisez la mise en cache autant que possible

    L’un des secrets les mieux gardés de l’optimisation des applications Web est la mise en cache.

    Pour les non-initiés, la mise en cache signifie précalculer et stocker des résultats coûteux (coûteux en termes d’utilisation du processeur et de la mémoire), et simplement les renvoyer lorsque la même requête est répétée.

    Par exemple, dans une boutique e-commerce, on peut tomber sur celui des 2 millions de produits, la plupart du temps les gens s’intéressent à ceux qui sont fraîchement approvisionnés, dans une certaine fourchette de prix, et pour une tranche d’âge particulière. Interroger la base de données pour obtenir ces informations est un gaspillage – puisque la requête ne change pas souvent, il est préférable de stocker ces résultats dans un endroit auquel nous pouvons accéder rapidement.

    Laravel a un support intégré pour plusieurs types de mise en cache. En plus d’utiliser un pilote de mise en cache et de créer le système de mise en cache à partir de zéro, vous souhaiterez peut-être utiliser certains packages Laravel qui facilitent mise en cache du modèle, mise en cache des requêtesetc.

    Mais notez qu’au-delà d’un certain cas d’utilisation simplifié, les packages de mise en cache prédéfinis peuvent causer plus de problèmes qu’ils n’en résolvent.

    Préférez la mise en cache en mémoire

    Lorsque vous cachez quelque chose dans Laravel, vous disposez de plusieurs options pour stocker le calcul résultant qui doit être mis en cache. Ces options sont également appelées pilotes de cache. Ainsi, bien qu’il soit possible et parfaitement raisonnable d’utiliser le système de fichiers pour stocker les résultats du cache, ce n’est pas vraiment ce que la mise en cache est censée être.

    Idéalement, vous souhaitez utiliser un cache en mémoire (vivant entièrement dans la RAM) comme Redis, Memcached, MongoDB, etc., de sorte que sous des charges plus élevées, la mise en cache sert une utilisation vitale plutôt que de devenir un goulot d’étranglement lui-même.

    Maintenant, vous pourriez penser qu’avoir un disque SSD est presque la même chose qu’utiliser une clé RAM, mais ce n’est même pas proche. Même informel repères montrent que la RAM surpasse le SSD de 10 à 20 fois en termes de vitesse.

    Mon système préféré en matière de mise en cache est Redis. C’est ridiculement rapide (100 000 opérations de lecture par seconde sont courantes), et pour les très grands systèmes de cache, peut être transformé en un groupe facilement.

    Cachez les itinéraires

    Tout comme la configuration de l’application, les routes ne changent pas beaucoup au fil du temps et sont un candidat idéal pour la mise en cache. Cela est particulièrement vrai si vous ne supportez pas les fichiers volumineux comme moi et que vous finissez par diviser votre web.php et api.php en plusieurs fichiers. Une seule commande Laravel rassemble toutes les routes disponibles et les garde à portée de main pour un accès futur :

    php artisan route:cache

    Et lorsque vous finissez par ajouter ou modifier des itinéraires, faites simplement :

    php artisan route:clear

    Optimisation des images et CDN

    Les images sont le cœur et l’âme de la plupart des applications Web. Par coïncidence, ils sont également les plus gros consommateurs de bande passante et l’une des principales raisons de la lenteur des applications/sites Web. Si vous stockez simplement les images téléchargées naïvement sur le serveur et que vous les renvoyez dans des réponses HTTP, vous laissez passer une énorme opportunité d’optimisation.

    Ma première recommandation est de ne pas stocker les images localement – il y a le problème de la perte de données à gérer, et selon la région géographique dans laquelle se trouve votre client, le transfert de données peut être extrêmement lent.

    Au lieu de cela, optez pour une solution comme Cloudinaire qui redimensionne et optimise automatiquement les images à la volée.

    Si ce n’est pas possible, utilisez quelque chose comme Cloudflare pour mettre en cache et servir les images pendant qu’elles sont stockées sur votre serveur.

    Et si même cela n’est pas possible, peaufiner un peu votre logiciel de serveur Web pour compresser les ressources et diriger le navigateur du visiteur vers la mise en cache des choses, fait une grande différence. Voici à quoi ressemblerait un extrait de configuration 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";
        }
    }

    Je suis conscient que l’optimisation d’image n’a rien à voir avec Laravel, mais c’est une astuce tellement simple et puissante (et si souvent négligée) que je ne pouvais pas m’en empêcher.

    Optimisation du chargeur automatique

    Le chargement automatique est une fonctionnalité intéressante et pas si ancienne de PHP qui a sans doute sauvé le langage du destin. Cela dit, le processus de recherche et de chargement de la classe appropriée en déchiffrant une chaîne d’espace de noms donnée prend du temps et peut être évité dans les déploiements de production où des performances élevées sont souhaitables. Encore une fois, Laravel a une solution à une seule commande pour cela :

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

    Faites-vous des amis avec les files d’attente

    Files d’attente sont la façon dont vous traitez les choses lorsqu’il y en a beaucoup, et chacune d’elles prend quelques millisecondes pour se terminer. Un bon exemple est l’envoi d’e-mails – un cas d’utilisation répandu dans les applications Web consiste à envoyer quelques e-mails de notification lorsqu’un utilisateur effectue certaines actions.

    Par exemple, dans un produit récemment lancé, vous souhaiterez peut-être que la direction de l’entreprise (environ 6 à 7 adresses e-mail) soit avertie chaque fois que quelqu’un passe une commande supérieure à une certaine valeur. En supposant que votre passerelle de messagerie puisse répondre à votre requête SMTP en 500 ms, nous parlons d’une bonne attente de 3 à 4 secondes pour l’utilisateur avant que la confirmation de la commande n’entre en vigueur. Un très mauvais morceau d’UX, je suis sûr que vous allez Je suis d’accord.

    Le remède consiste à stocker les travaux au fur et à mesure qu’ils arrivent, à dire à l’utilisateur que tout s’est bien passé et à les traiter (quelques secondes) plus tard. En cas d’erreur, les tâches mises en file d’attente peuvent être réessayées plusieurs fois avant qu’elles ne soient déclarées comme ayant échoué.

    Crédits : Microsoft.com

    Bien qu’un système de file d’attente complique un peu la configuration (et ajoute une surcharge de surveillance), il est indispensable dans une application Web moderne.

    Optimisation des actifs (Laravel Mix)

    Pour tous les actifs frontaux de votre application Laravel, assurez-vous qu’il existe un pipeline qui compile et minimise tous les fichiers d’actifs. Ceux qui sont à l’aise avec un système de bundle comme Webpack, Gulp, Parcel, etc., n’ont pas besoin de s’embêter, mais si vous ne le faites pas déjà, Mélange Laravel est une recommandation solide.

    Mix est un wrapper léger (et délicieux, en toute honnêteté !) autour de Webpack qui prend en charge tous vos fichiers CSS, SASS, JS, etc., pour la production. Un fichier .mix.js typique peut être aussi petit que celui-ci et faire des merveilles :

    const mix = require('laravel-mix');
    
    mix.js('resources/js/app.js', 'public/js')
        .sass('resources/sass/app.scss', 'public/css');

    Cela prend automatiquement en charge les importations, la minification, l’optimisation et tout le reste lorsque vous êtes prêt pour la production et exécutez la production npm run. Mix prend en charge non seulement les fichiers JS et CSS traditionnels, mais également les composants Vue et React que vous pourriez avoir dans le flux de travail de votre application.

    Plus d’informations ici!

    Conclusion

    L’optimisation des performances est plus un art qu’une science – savoir comment et combien faire est important que quoi faire. Cela dit, il n’y a pas de fin à combien et à tout ce que vous pouvez optimiser dans une application Laravel.

    Mais quoi que vous fassiez, j’aimerais vous donner quelques conseils d’adieu : l’optimisation doit être effectuée lorsqu’il y a une raison solide, et non parce que cela sonne bien ou parce que vous êtes paranoïaque à propos des performances de l’application pour plus de 100 000 utilisateurs alors qu’en réalité il n’y en a que 10.

    Si vous ne savez pas si vous devez optimiser votre application ou non, vous n’avez pas besoin de vous débarrasser du proverbial nid de frelons. Une application fonctionnelle qui semble ennuyeuse mais qui fait exactement ce qu’elle doit faire est dix fois plus souhaitable qu’une application qui a été optimisée dans une supermachine hybride mutante mais qui tombe à plat de temps en temps.

    Et, pour que le débutant devienne un maître de Laravel, consultez ceci Cours en ligne.

    Que vos applications fonctionnent beaucoup, beaucoup plus vite ! 🙂