L’alerte est venue d’une rédactrice du magazine en ligne d’un client de Kaolin : « l’éditeur rame, l’ordinateur chauffe, même sans rien taper ». Le premier réflexe a été de suspecter une extension tierce ou un plugin de cache mal configuré. Le vrai coupable était un bloc « articles liés » installé deux semaines plus tôt, dont le composant d’édition contenait un useSelect mal écrit.
Ce type de bug est particulièrement traître : il ne provoque aucune erreur dans la console, aucun message rouge. L’éditeur continue de fonctionner, juste de plus en plus lentement, jusqu’à devenir inutilisable sur les pages les plus longues.
Symptôme
Le bloc en question affichait une liste des trois derniers articles de la même catégorie que l’article en cours. Rien d’inhabituel visuellement : la liste s’affichait correctement, mais le processeur du navigateur restait bloqué à 100 % tant que l’éditeur était ouvert. Le Gestionnaire des tâches de Chrome montrait l’onglet de l’admin WordPress consommant plus de ressources qu’un onglet de lecture vidéo.
Diagnostic avec React DevTools Profiler
Le Profiler de React DevTools, activé sur l’onglet de l’éditeur, a montré un enregistrement saturé de rendus du même composant, plusieurs centaines par seconde, sans qu’aucune interaction utilisateur ne les déclenche. Le code du bloc contenait ceci :
function Edit({ attributes }) {
const { categorieId } = attributes;
const articlesLies = useSelect((select) => {
return select('core').getEntityRecords('postType', 'post', {
categories: [categorieId],
exclude: [wp.data.select('core/editor').getCurrentPostId()],
per_page: 3,
});
}, [categorieId]);
return ( /* ... */ );
}

Le tableau exclude: [wp.data.select('core/editor').getCurrentPostId()] est recréé à chaque exécution du sélecteur, avec une nouvelle référence à chaque fois, même si l’identifiant de l’article ne change jamais. Or getEntityRecords traite les paramètres de requête comme faisant partie de la clé de cache : une nouvelle référence de tableau, même avec un contenu identique, est interprétée comme une requête différente. Le store core déclenche donc un nouveau chargement, ce qui déclenche un nouveau rendu, qui recrée le tableau, qui déclenche un nouveau chargement, indéfiniment.
Pourquoi le tableau de dépendances ne protégeait pas
Le tableau de dépendances passé à useSelect, [categorieId], ne contenait que l’identifiant de catégorie, qui ne changeait pas. Mais ce tableau ne protège que contre le re-calcul du sélecteur lui-même côté React ; il n’empêche pas getEntityRecords de recevoir un objet de requête différent en interne à chaque appel, puisque cet objet est construit dans le corps de la fonction passée à useSelect, exécutée à chaque rendu du composant.
Correctif
La correction consiste à sortir l’identifiant de l’article courant du tableau de requête, en le récupérant une seule fois à l’extérieur de la fonction de sélection, ou à défaut à s’assurer que la structure de la requête reste strictement identique entre deux rendus :
function Edit({ attributes }) {
const { categorieId } = attributes;
const idArticleCourant = useSelect(
(select) => select('core/editor').getCurrentPostId(),
[]
);
const articlesLies = useSelect((select) => {
if (!idArticleCourant) return null;
return select('core').getEntityRecords('postType', 'post', {
categories: [categorieId],
exclude: [idArticleCourant],
per_page: 3,
});
}, [categorieId, idArticleCourant]);
return ( /* ... */ );
}
Le tableau exclude reste recréé à chaque rendu, mais son contenu ne varie plus tant que idArticleCourant ne change pas : la requête générée est strictement identique d’un rendu à l’autre, et getEntityRecords retourne la donnée en cache sans redéclencher de requête.
Prévention
- Ne jamais appeler un autre
select()imbriqué à l’intérieur d’un sélecteur déjà en cours d’exécution sans le sortir dans unuseSelectséparé. - Se méfier de tout tableau ou objet littéral construit directement dans les paramètres d’une requête à une entité.
- Ouvrir le Profiler React DevTools au moindre ralentissement inexpliqué de l’éditeur : une boucle de rendu s’y voit immédiatement sous forme de bande de rendus continue.
Une requête aux entités WordPress doit toujours pouvoir être comparée trait pour trait au rendu précédent, sans quoi le cache ne sert à rien.
Notre verdict
Ce genre de boucle ne se voit pas dans une revue de code rapide : le tableau de dépendances de useSelect paraît correct, l’erreur se cache dans la construction de la requête elle-même. Sur ce projet, le correctif a fait chuter la consommation processeur de l’onglet éditeur de plus de 400 rendus par seconde à un rendu unique au chargement du bloc.