Accélérer WordPress, partie 1 : l’appli

WordPress est un vraiment chouette logiciel pour créer des sites – blogs, sites vitrines ou de e-commerce, il est suffisamment versatile pour qu’on puisse en faire ce qu’on veut. Il est plutôt efficace, aussi. De base. Une fois ajoutés tous les plugins dont vous aurez envie ou besoin pour différentes fonctionnalités, il est possible qu’il se traîne grave.

Ce n’est pas toujours facile à déterminer d’où viennent les ralentissements, et pour cela, de manière ironique, on va utiliser, entre autres… des plugins ! Des plugins que nous désactiverons une fois les tests terminés.

Le premier, Query Monitor, nous permettra de voir différents aspects d’un chargement de page, dont le temps de génération de la page.

Le deuxième, Code Profiler, nous permettra de voir de manière basique où le temps de génération est passé.

Dans un premier temps on va établir une base de performance : quelle est la performance de WordPress sans plugins ? C’est parti pour tout désactiver, sauf, bien sûr, nos deux plugins de perf.

Ensuite, on teste la homepage, ou une page de catégorie, ou n’importe laquelle. L’important c’est de tester toujours la même, et de tester quelques chargements pour faire une moyenne.

Sans plugin, voici les résultats sur mon site : 0.65s pour rendre la homepage. Le profiler m’informe que le plus clair du temps est passé dans le thème parent, et un peu dans mon thème enfant :

Sans plugin

On va ignorer le temps passé dans nos deux plugins de debug, bien sûr.

Maintenant on a une base de comparaison, mais… cette performance c’est une chose, mais là il nous manque nos fonctionnalités ! Chacun·e aura sa propre liste de fonctionnalités non-négociables ou facultatives. Voici la mienne :

  • Un antispam pour les commentaires
  • Un plugin e-commerce pour ma menuiserie
  • Un plugin multi-lingue pour mes posts en anglais
  • Une Lightbox pour zoomer les images
  • Un plugin social pour les boutons de partage (Vanité quand tu nous tiens…)
  • Un plugin de stats pour savoir quelles sont les posts qui intéressent les gens (idem…)

Pour ces différentes fonctionnalités, les choix « évidents » lorsqu’on les cherche (sur Google, dans l’interface d’admin de WordPress) sont :

Et j’en ajoute quelques unes pour les fonctionnalités, comme Quotes for Woocommerce (car ma boutique fait des devis, pas des achats directs), mais je ne m’étendrai pas sur celles-ci.

La partie facile : remplacement des plugins mous

On refait un test ? La homepage charge maintenant en… 2.80s ! Soit quatre fois plus lentement qu’avant… Le profiler pointe les pires coupables :

WordPress avec un plugin pour chacune des fonctionnalités que je veux

Partons dans la recherche d’alternatives.

Pour Jetpack : ça dégage, de toutes façons ça fait beaucoup plus de trucs que ce dont j’ai besoin, et ça partage beaucoup trop de données à mon goût. À la place, on met Koko Analytics, super basique, respecteux de la vie privée. Nouveau test : 2.40s, soit 4 dixièmes de seconde d’épluché.

Jetpack remplacé par Koko Analytics

Simple Lightbox, tu prends beaucoup trop de CPU pour faire rien du tout. Ça dégage, on met WP Featherlight à la place et hop : 1.80s (6 dixièmes de moins).

Simple Lightbox remplacé par WP Featherlight

Un autre « fruit bas » ? Sassy Social Share, remplacé par Minimal Share Buttons : certes, c’est moins paramétrable, mais ça me suffit largement, et on arrive à 1.60s (2 dixièmes de moins).

Sassy Social Share remplacé par Minimal Share Buttons

Là, on commence à arriver au bout des alternatives simples : Woocommerce, ça va être dur de le remplacer. Après pas mal de recherche sur internet, il semblerait que je ne sois pas le seul à le trouver mou du genou, et une piste va être… d’ajouter un plugin !? J’ajoute donc Disable Woocommerce Bloat, et on passe à 1.50s (1 dixième de moins). C’est très peu, mais c’est toujours ça.

Après ajout de Disable Woocommerce Bloat

Comme je ne sais pas trop quoi faire pour les trois grands coupables restants (Woocommerce, Polylang et le thème Boxcard), on va passer à plus bourrin.

Un peu plus loin : le profiling bas niveau

On va faire ça avec XDebug, qui se branche dans PHP et qui a le bon goût de nous filer des dumps cachegrind, un logiciel de profiling que je connais déjà.

On commence par l’installer et le configurer en mode profilage :

sudo apt install php8.1-xdebug

sudo cat /etc/php/8.1/fpm/conf.d/20-xdebug.ini
zend_extension=xdebug.so
xdebug.profiler_enable=1
xdebug.profiler_output_dir=/tmp
xdebug.log=/tmp/xdebug.log
xdebug.default_enable=1   
xdebug.mode=profile
xdebug.start_with_request=trigger

sudo systemctl restart php8.1-fpm.service

Cela va nous permettre de déclencher un profil d’une page en ajoutant un paramètre à la requête : ?XDEBUG_TRIGGER=yes

Une fois la requête tracée, on récupère un fichier /tmp/cachegrind.out.437810 sur le serveur et on l’ouvre avec KCacheGrind. Voici ce qu’on en sort :

Le profil complet de notre requête

Par défaut, la liste des fonctions à gauche est triée par « temps inclusif ». C’est à dire le temps passé dans la fonction en comptant celui passé dans celles qu’elle appelle. Il est souvent plus intéressant de trier par « temps personnel », le temps passé dans une fonction sans compter celui passé dans celles qu’elle appelle.

Une fois trié comme ça, on voit que… qu’on passe le plus clair de notre temps… à faire des traductions ?!

À peu près tous les top calls sont dans mo.php, l10n.php…

Après pas mal de recherches sur le sujet, il apparaît que WordPress a fait le choix de shipper une implémentation en PHP de gettext, basée sur POMO. C’est très bien, mais c’est lent. Il y a des tickets sur le sujet. On y découvre l’existence du plugin WP Performance Pack, qui promet, entre autres, une réimplémentation de la librairie d’internationalisation utilisant la lib gettext native de l’hôte lorsqu’elle est disponible.

C’est parti pour tester ça : on installe le plugin, on sudo apt install php8.1-gettext, on active la localisation, gettext natif et l’alternative MO reader dans les réglages du plugin :

Et on reteste : Homepage chargée en 0.93s ! (6 dixièmes de moins). Le profiler nous indique maintenant que WP Performance Pack prend du temps… Mais il en fait tellement gagner ailleurs qu’il vaut absolument le coup !

C’est WP Performance Pack qui est tenu responsable des temps de traduction maintenant.

Un nouveau profile XDebug nous montre qu’on passe effectivement largement moins de temps à traduire, et que l’implémentation native est bien en route :

Cependant, WP Performance Pack fait plein d’autres trucs dont je n’ai pas besoin, et de plus il n’a pas l’air maintenu. Quelques heures de boulot plus tard, un nouveau plugin est né : Native Gettext for WordPress ! Ce plugin fait le minimum du minimum, partant du principe que le code le plus rapide est celui qui n’est pas exécuté ni même écrit.

WP Performance Pack remplacé par mon propre Native Gettext

Il reste, j’en suis sûr, des choses à gratter dans Woocommerce, mais il faudrait se plonger dans le code. J’ai déjà pushé un petit fix qui fait gagner quelques dizaines de millisecondes, j’en ai un autre sous le coude qui j’espère sera accepté aussi.

Il reste certainement du gain sur Polylang, mais ici aussi, l’effort sera un peu haut : les auteur·rice·s du plugin semblent au courant des questions de performance.

Pour le fun, on va ajouter Redis Object Cache.

Ensuite, on peut fouiller dans les dumps Cachegrind et, partant du principe que le code le plus rapide, c’est celui qu’on n’appelle pas, trouver des candidats à la suppression. C’est au moins aussi amusant que Wordle, et ça permet potentiellement de contribuer un peu aux logiciels libres qu’on utilise ! Quelques exemples :

Une dernière chose pour la page d’accueil, et toutes les pages de listes. Les « extraits » (résumés) de posts peuvent être spécifiés manuellement dans l’interface d’édition. Lorsqu’ils ne le sont pas, le début de chaque post est extrait au runtime, coupé à 55 mots, et filtré de diverses manières pour ne donner que du texte. Comme je suis un feignant, aucun de mes posts n’a d’extrait manuellement spécifié. Du coup, le travail de nettoyage du début du post est refait à chaque affichage, et (on ne le voit pas sur les screenshots ci-dessus), cela prend un certain temps. Pour arranger cela, j’ai installé le plugin WP Excerpt Generator, et j’ai auto-généré tous les extraits pour enregistrement en base. Cela fait gagner peu (~50-100ms), mais c’est toujours bon à prendre !

Étape 3 : la chaîne HTTP

Jusqu’ici on s’est concentré sur la génération de la page. Mais ce n’est pas tout ce qui fait la performance ! Lorsqu’un·e internaute demande une page du site, la page est rendue et renvoyée, mais vient ensuite le reste : les ressources, CSS, Javascript, images, etc.

Chargeons la homepage en regardant l’onglet Network des Developers Tools de Firefox :

On voit la requête à /wordpress/ qui prend 1.389s. C’est plus que les précédents chiffres remontés par Query Monitor, car Query Monitor nous donne le temps de génération de la page côté serveur. Ici, Firefox nous donne le temps total entre la requête DNS, la connexion HTTPS, la génération et l’envoi des données. Elle est suivie de nombreuses requêtes à des scripts JS et du CSS. Essayons de grouper les CSS et les Javascript ensemble, en ajoutant le plugin Autoptimize.

Le temps total est à peine plus bas, mais cela fait moins de requêtes. C’est mieux.

Étape 4 : la stack serveur

On a déjà bien avancé, mais pour finir, il reste plusieurs pistes de gains, toutes côté serveur :

  • HTTP2 pour faire moins de connexions
  • Compression des ressources statiques
  • Envoi des images au format webp pour les clients qui le gèrent
  • Activation d’un opcache pour PHP
  • Réglages SSL
  • Insertion d’un cache extrêmement efficace, Varnish, pour tout simplement sauter toute la partie génération sur laquelle on vient de travailler.

Cela fait l’objet de deux autres posts :

mais en attendant, voici le résultat pour un·e internaute lambda à la fin :