Laravel est un outil polyvalent, mais la rapidité n’est pas son point fort. Découvrons quelques astuces pour accélérer son fonctionnement !
Aujourd’hui, aucun développeur PHP n’échappe à l’attraction de Laravel. Que ce soit un débutant appréciant sa rapidité de développement, ou un expert contraint par les exigences du marché, Laravel est omniprésent.
Indéniablement, Laravel a dynamisé l’écosystème PHP. Sans lui, il est fort probable que j’aurais abandonné le monde PHP depuis longtemps.
Un petit éloge (justifié) de Laravel :
Laravel met tout en œuvre pour simplifier votre tâche, ce qui signifie qu’il effectue un travail considérable en arrière-plan afin de vous garantir une expérience de développement agréable. Toutes ces fonctionnalités « magiques » apparentes cachent des couches de code qui doivent être traitées à chaque exécution. Même une simple erreur révèle la complexité interne (observez la profondeur du suivi d’erreur, du point de départ à son cœur) :
Pour une erreur de compilation dans une vue, il y a 18 appels de fonction à analyser. Personnellement, j’en ai vu jusqu’à 40, et ce nombre pourrait augmenter si vous utilisez d’autres bibliothèques et extensions.
En bref, ces multiples couches de code font que Laravel est lent par défaut.
Quelle est la lenteur de Laravel ?
Il est difficile de répondre à cette question avec précision pour plusieurs raisons.
Premièrement, il n’existe pas de critère standard, objectif et pertinent pour mesurer la vitesse d’une application web. Plus rapide ou plus lent par rapport à quoi ? Dans quelles conditions ?
Deuxièmement, la performance d’une application web dépend de nombreux facteurs (base de données, système de fichiers, réseau, cache, etc.), il est donc réducteur de parler de vitesse en termes absolus. Une application web rapide avec une base de données lente sera une application lente. 🙂
Malgré cette incertitude, les benchmarks restent populaires. Bien qu’ils soient limités (voir ceci et cela), ils fournissent une base de comparaison et nous aident à nous repérer. Ainsi, en relativisant leur portée, examinons un classement approximatif des frameworks PHP en termes de vitesse.
Selon ce benchmark GitHub, voici comment se positionnent les frameworks PHP :
Vous remarquerez que Laravel se trouve en bas du classement ! Bien que la plupart de ces frameworks ne soient pas d’une grande utilité, cela souligne le fait que Laravel est comparativement lent par rapport à d’autres plus populaires.
En pratique, cette lenteur passe souvent inaperçue car les applications web ne sont généralement pas sollicitées à l’extrême. Cependant, dès que le nombre de requêtes simultanées augmente (disons, entre 200 et 500), les serveurs commencent à avoir des difficultés et s’effondrent. À ce moment-là, l’ajout de ressources matérielles ne résout plus le problème, et les coûts d’infrastructure explosent.
Mais rassurez-vous, cet article porte sur les solutions, pas les problèmes !
La bonne nouvelle est qu’il existe de nombreuses pistes pour accélérer votre application Laravel. Oui, sans plaisanter. Vous pouvez augmenter considérablement sa performance et réduire vos coûts d’hébergement de plusieurs centaines d’euros par mois. Comment ? C’est ce que nous allons voir.
Quatre axes d’optimisation
Selon moi, l’optimisation d’une application PHP se décline en quatre niveaux distincts :
- Niveau du langage : Adopter une version plus rapide du langage et éviter certaines pratiques de codage qui ralentissent l’exécution.
- Niveau du framework : Optimisations spécifiques au framework, qui sont le sujet de cet article.
- Niveau de l’infrastructure : Optimiser le gestionnaire de processus PHP, le serveur web, la base de données, etc.
- Niveau matériel : Migrer vers une infrastructure d’hébergement plus performante.
Chacun de ces types d’optimisation est pertinent. L’optimisation de PHP-fpm, par exemple, est essentielle. Toutefois, cet article se concentrera sur les optimisations de type 2, liées au framework.
Notez qu’il n’y a aucune justification à la numérotation de ces niveaux, il s’agit d’une classification personnelle. Ne me citez jamais et ne dites jamais : « Nous avons besoin d’une optimisation de type 3 sur notre serveur », car votre chef d’équipe pourrait bien me tuer. 😀
Entrons maintenant dans le vif du sujet.
Évitez les requêtes N+1
Le problème des requêtes N+1 est fréquent avec les ORM. Laravel possède son propre ORM, Eloquent, qui est si bien conçu et pratique qu’on en oublie parfois ce qui se passe en arrière-plan.
Prenons un scénario courant : afficher la liste de toutes les commandes passées par un groupe de clients. C’est une situation fréquente dans les systèmes de commerce électronique et les interfaces de reporting, où il faut afficher des entités liées.
Dans Laravel, une fonction de contrôleur pourrait ressembler à ceci :
class OrdersController extends Controller { // ... public function getAllByCustomers(Request $request, array $ids) { $customers = Customer::findMany($ids); $orders = collect(); // nouvelle collection foreach ($customers as $customer) { $orders = $orders->merge($customer->orders); } return view('admin.reports.orders', ['orders' => $orders]); } }
C’est simple, élégant et beau. 🤩🤩
Malheureusement, c’est une manière inefficace de coder dans Laravel.
Voici pourquoi.
Lorsque l’on demande à l’ORM de rechercher les clients, une requête SQL comme celle-ci est générée :
SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);
C’est conforme à nos attentes. Les résultats sont stockés dans la collection $customers.
Ensuite, pour chaque client, on récupère ses commandes. Cela déclenche 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 voulons obtenir les commandes de 1000 clients, le nombre total de requêtes sera de 1 (pour les clients) + 1000 (pour les commandes), soit 1001. C’est de là que vient le nom N+1.
Y a-t-il une meilleure solution ? Absolument ! En utilisant le chargement hâtif, nous pouvons forcer l’ORM à faire une jointure et à retourner toutes les données en une seule requête ! Par exemple :
$orders = Customer::findMany($ids)->with('orders')->get();
Bien que la structure de données résultante soit imbriquée, les données de commande sont facilement accessibles. La requête unique ressemble à ceci :
SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);
Une seule requête est clairement préférable à mille requêtes. Imaginez l’impact avec 10 000 clients ! Sans parler des articles dans chaque commande ! N’oubliez jamais, le chargement hâtif est presque toujours une bonne idée.
Mettez en cache votre configuration !
La flexibilité de Laravel repose sur une pléthore de fichiers de configuration. Vous souhaitez modifier le stockage des images ? Modifiez simplement le fichier config/filesystems.php. Vous voulez utiliser plusieurs drivers de file d’attente ? Définissez-les dans config/queue.php. Au total, il y a 13 fichiers de configuration qui permettent d’ajuster presque tous les aspects du framework.
Compte tenu de la nature de PHP, à chaque nouvelle requête web, Laravel se réveille, démarre tous les composants et analyse ces fichiers de configuration. Cette approche est inefficace si rien n’a changé. Reconstruire la configuration à chaque requête est un gaspillage de ressources qui doit être évité. La solution ? Une simple commande Laravel :
php artisan config:cache
Cette commande combine tous les fichiers de configuration et les stocke en cache. Laravel lira ce fichier unique pour chaque requête suivante.
Attention : la mise en cache de la configuration peut être une opération délicate. Le piège principal est que les appels à la fonction `env()` en dehors des fichiers de configuration renverront la valeur `null`.
Cela est logique si l’on y réfléchit. En utilisant la mise en cache de configuration, vous indiquez au framework que vous êtes certain que la configuration est correcte et qu’il n’est pas nécessaire de la modifier. Les fichiers `.env` servent à gérer les variables dynamiques, mais lors de la mise en cache, vous vous engagez à une configuration statique.
Par conséquent, voici quelques règles fondamentales de la mise en cache de configuration :
- Ne l’appliquez qu’en production.
- Faites-le seulement si vous êtes sûr à 100% de vouloir figer la configuration.
- En cas de problème, annulez la mise en cache avec `php artisan cache:clear`.
- Priez pour que les dégâts soient limités !
Réduisez les services chargés automatiquement
Pour fonctionner, Laravel charge de nombreux services au démarrage. Ceux-ci sont définis dans le fichier `config/app.php` sous la clé `providers`. Voici un exemple :
/* |-------------------------------------------------------------------------- | Fournisseurs de services à chargement automatique |-------------------------------------------------------------------------- | | Les fournisseurs de services listés ici seront automatiquement chargés à la | demande de votre application. N'hésitez pas à ajouter vos propres services à | ce tableau afin d'étendre les fonctionnalités de vos applications. | */ 'providers' => [ /* * Fournisseurs de services du framework Laravel ... */ 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, /* * Fournisseurs de services des packages ... */ /* * Fournisseurs de services de l'application ... */ AppProvidersAppServiceProvider::class, AppProvidersAuthServiceProvider::class, // AppProvidersBroadcastServiceProvider::class, AppProvidersEventServiceProvider::class, AppProvidersRouteServiceProvider::class, ],
J’ai compté 27 services listés ! Il est peu probable que vous ayez besoin de tous ces services.
Par exemple, si vous développez une API REST, vous n’avez pas besoin du service de session, ni du service d’affichage. De même, vous pouvez désactiver le service d’authentification, de pagination, de traduction, etc. Dans ce cas, près de la moitié de ces services sont inutiles.
Examinez attentivement votre application. A-t-elle réellement besoin de tous ces services ? Attention à ne pas les désactiver aveuglément avant de tester minutieusement votre application sur vos environnements de développement et de staging. Soyez très prudent avant de déployer vos modifications en production. 🙂
Utilisez les middlewares avec parcimonie
Pour effectuer des traitements personnalisés sur les requêtes web, les middlewares sont la solution. Il est tentant de définir un middleware dans `app/Http/Kernel.php` pour qu’il soit disponible globalement. Cependant, au fur et à mesure que l’application évolue, cette collection de middlewares peut devenir un fardeau silencieux, surtout si ces traitements ne sont pas toujours nécessaires.
Soyez donc attentif à l’endroit où vous appliquez un middleware. L’approche globale peut sembler plus pratique, mais peut nuire à la performance à long terme. Il est préférable d’appliquer les middlewares de manière sélective, même si cela implique une gestion plus rigoureuse des changements.
Évitez l’ORM (parfois)
Bien qu’Eloquent simplifie les interactions avec la base de données, il a un coût en termes de vitesse. En tant que mappeur, l’ORM doit non seulement récupérer les données, mais aussi instancier les objets modèles et les hydrater (remplir les propriétés avec les données).
Par exemple, si vous faites un simple `$users = User::all()` et qu’il y a 10 000 utilisateurs, le framework récupérera 10 000 lignes de la base de données et créera en interne 10 000 objets `User()` en remplissant leurs propriétés. Cela représente un travail conséquent en arrière-plan. Si la base de données est un goulot d’étranglement, contourner l’ORM est parfois judicieux.
C’est particulièrement vrai pour les requêtes SQL complexes, où il est difficile d’obtenir une requête efficace avec l’ORM. Dans ce cas, l’utilisation de `DB::raw()` et l’écriture de la requête à la main sont préférables.
Cette étude de performance montre que pour des insertions simples, l’ORM Eloquent est d’autant plus lent que le nombre d’enregistrements augmente :
Utilisez le cache autant que possible
La mise en cache est l’une des techniques les plus efficaces pour optimiser les applications web.
La mise en cache consiste à précalculer et stocker des résultats coûteux (en termes de ressources CPU et mémoire) afin de les retourner directement lors des requêtes suivantes.
Par exemple, dans une boutique en ligne, parmi des millions de produits, les utilisateurs sont principalement intéressés par les nouveautés, les produits dans une certaine fourchette de prix ou les articles adaptés à une tranche d’âge. Interroger la base de données à chaque fois est un gaspillage de ressources. Il est plus judicieux de mettre en cache ces résultats, car ils ne changent pas souvent.
Laravel offre plusieurs mécanismes de mise en cache. En plus d’utiliser un pilote de cache et de créer votre propre système de cache, vous pouvez utiliser des packages Laravel pour faciliter la mise en cache de modèles, la mise en cache des requêtes, etc.
Cependant, dans certains cas complexes, les packages de mise en cache peuvent causer plus de problèmes qu’ils n’en résolvent.
Privilégiez le cache en mémoire
Lorsque vous mettez en cache des données dans Laravel, vous disposez de plusieurs options pour stocker les résultats. Ces options sont appelées pilotes de cache. Bien qu’il soit possible d’utiliser le système de fichiers, ce n’est pas une approche optimale.
Idéalement, il faut utiliser un cache en mémoire (RAM), comme Redis, Memcached, MongoDB, etc. Ainsi, en cas de forte charge, le cache peut jouer un rôle essentiel sans devenir un goulot d’étranglement.
Bien que les disques SSD soient rapides, la RAM reste beaucoup plus rapide. Des benchmarks informels montrent que la RAM est 10 à 20 fois plus rapide que le SSD.
Mon système de mise en cache préféré est Redis. Il est incroyablement rapide (100 000 opérations de lecture par seconde), et il peut être transformé en cluster facilement pour les très grands systèmes.
Mettez en cache les routes
Comme la configuration, les routes ne changent pas souvent et sont un bon candidat pour la mise en cache. C’est particulièrement vrai si vous divisez vos fichiers `web.php` et `api.php` en plusieurs fichiers. Une simple commande Laravel permet de rassembler toutes les routes disponibles :
php artisan route:cache
Lorsque vous ajoutez ou modifiez des routes, utilisez :
php artisan route:clear
Optimisez les images et utilisez un CDN
Les images sont cruciales pour de nombreuses applications web, mais elles sont aussi les plus gourmandes en bande passante et une des causes de lenteur. Si vous stockez simplement les images téléchargées et les renvoyez directement dans les réponses HTTP, vous manquez une opportunité d’optimisation.
Je vous recommande de ne pas stocker les images localement. Il y a des problèmes de perte de données à gérer, et la vitesse de transfert des données peut être très variable en fonction de l’emplacement géographique des clients.
Utilisez plutôt une solution comme Cloudinary qui redimensionne et optimise automatiquement les images. Si ce n’est pas possible, utilisez Cloudflare pour mettre en cache les images stockées sur votre serveur.
Si ces options ne sont pas envisageables, vous pouvez configurer votre serveur web pour compresser les ressources et activer le cache navigateur. Voici un exemple de configuration Nginx :
server { # fichier tronqué # Paramètres de compression gzip gzip on; gzip_comp_level 5; gzip_min_length 256; gzip_proxied any; gzip_vary on; # Contrôle du cache navigateur 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"; } }
Bien que l’optimisation des images ne soit pas spécifique à Laravel, c’est une astuce simple et puissante (souvent négligée) que je devais mentionner.
Optimisez le chargement automatique
Le chargement automatique de classes est une fonctionnalité essentielle de PHP. Cependant, le processus de recherche et de chargement des classes prend du temps. Pour les environnements de production, où la performance est essentielle, cette étape peut être optimisée :
composer install --optimize-autoloader --no-dev
Utilisez les files d’attente
Les files d’attente permettent de gérer les tâches qui prennent du temps. Un bon exemple est l’envoi d’e-mails. Il est courant de devoir envoyer des notifications par e-mail quand un utilisateur effectue certaines actions.
Par exemple, vous pouvez vouloir avertir la direction de l’entreprise (6 à 7 adresses e-mail) lorsqu’une commande dépasse un certain montant. Si l’envoi d’un email prend 500ms, cela peut prendre 3 à 4 secondes pour l’utilisateur, ce qui est une mauvaise expérience.
La solution consiste à stocker les tâches dans une file d’attente et de les traiter ultérieurement. En cas d’erreur, une tâche peut être réessayée plusieurs fois avant d’être considérée comme échouée.
Crédits : Microsoft.com
Bien que la configuration d’un système de file d’attente soit un peu plus complexe, c’est une nécessité pour une application web moderne.
Optimisez vos assets avec Laravel Mix
Pour tous les assets frontaux de votre application Laravel, utilisez un outil qui compile et minifie tous les fichiers. Si vous êtes déjà familier avec un outil comme Webpack, Gulp ou Parcel, il n’y a pas besoin de changer vos habitudes. Sinon, Laravel Mix est une excellente solution.
Mix est un wrapper léger (et agréable) autour de Webpack qui permet de gérer les fichiers CSS, SASS, JS, etc. Voici un exemple de fichier `mix.js` :
const mix = require('laravel-mix'); mix.js('resources/js/app.js', 'public/js') .sass('resources/sass/app.scss', 'public/css');
Cette configuration prend en charge les importations, la minification, l’optimisation, etc. lorsque vous lancez la production avec `npm run production`. Mix prend également en charge les composants Vue et React.
Plus d’informations ici!
Conclusion
L’optimisation des performances est plus un art qu’une science. Il est important de savoir quand et comment optimiser. Vous pouvez optimiser presque tout dans une application Laravel.
Mais n’oubliez pas que l’optimisation doit être réalisée quand il y a une raison valable. Il ne faut pas le faire juste parce que vous êtes paranoïaque à propos de la performance alors que vous n’avez que quelques utilisateurs.
Si vous n’êtes pas sûr de la nécessité d’optimiser votre application, il est préférable de ne pas chercher les problèmes là où il n’y en a pas. Une application fonctionnelle qui fait son travail est préférable à une application sur-optimisée mais instable.
Et, pour approfondir vos compétences sur Laravel, consultez ce cours en ligne.
Que vos applications soient rapides ! 🙂