Symptôme signalé par une rédactrice : « l’éditeur devient poisseux dès que j’ajoute le bloc Sommaire personnalisé, comme si chaque lettre tapée traînait un métro derrière elle ». Sur un article de taille normale, la latence de frappe restait imperceptible ; sur un article de quarante blocs et plus, elle devenait franchement gênante, au point que certains rédacteurs évitaient le bloc.
Ce genre de ralentissement dans l’éditeur de blocs a presque toujours la même origine : un composant qui s’abonne aux données du store Redux-like de Gutenberg de façon trop large, et qui se re-rend à chaque changement d’état de l’éditeur, même quand rien ne le concerne directement. Voici comment on l’a diagnostiqué et corrigé, étape par étape.
Diagnostic : symptôme et hypothèse
Avant de toucher au code, on a vérifié que le ralentissement dépendait bien du nombre de blocs, pas de la taille du texte : un article de cinq blocs restait fluide, un article de quarante blocs avec le même bloc Sommaire devenait laborieux. Cette observation orientait fortement vers un problème de rendu en cascade : chaque frappe dans n’importe quel bloc déclenchait potentiellement le recalcul du Sommaire, qui doit justement parcourir tous les blocs de la page pour construire sa liste de titres.
Utiliser React DevTools Profiler
L’extension navigateur React Developer Tools embarque un onglet Profiler qui enregistre chaque rendu de composant pendant une session, avec sa durée et sa cause. La méthode : ouvrir l’éditeur de blocs, lancer un enregistrement, taper quelques caractères dans un bloc Paragraphe quelconque, arrêter l’enregistrement, puis observer la flame graph.
Sur ce projet, la flame graph a montré, à chaque frappe, un rendu du composant SommairePersonnalise alors que son contenu affiché n’avait strictement aucune raison de changer entre deux lettres tapées ailleurs dans l’article. Le survol du composant dans le Profiler affiche la raison du rendu (« Hooks changed », en général un useSelect) et son temps d’exécution — ici environ 15 ms par frappe, négligeable isolément, mais additionné à d’autres blocs similaires, cela explique la latence ressentie.
Repérer le useSelect trop large
Le code du bloc utilisait useSelect() de @wordpress/data pour récupérer l’ensemble des blocs de l’article à chaque rendu, sans sélecteur suffisamment précis :
// Avant : re-render à chaque changement du moindre attribut, sur tout bloc
const titres = useSelect( ( select ) => {
const tousLesBlocs = select( 'core/block-editor' ).getBlocks();
return extraireLesTitres( tousLesBlocs );
}, [] );
getBlocks() renvoie une référence différente à chaque changement d’attribut de n’importe quel bloc du contenu, y compris un simple caractère tapé dans un Paragraphe. Le hook useSelect considère alors que la donnée a changé et redéclenche systématiquement extraireLesTitres(), une fonction qui parcourt récursivement l’arbre entier des blocs.

Corriger : cibler et mémoïser
Deux leviers combinés ont résolu le problème. D’abord, restreindre la sélection à ce qui compte vraiment — ici, uniquement les blocs de titre (core/heading) plutôt que l’arbre complet :
// Après : sélection ciblée sur les blocs de titre uniquement
const titres = useSelect( ( select ) => {
return select( 'core/block-editor' )
.getBlocks()
.filter( ( bloc ) => bloc.name === 'core/heading' )
.map( ( bloc ) => ( {
texte: bloc.attributes.content,
niveau: bloc.attributes.level,
} ) );
}, [] );
Ce filtrage seul ne suffisait pas : la fonction se relançait toujours à chaque frappe car getBlocks() renvoie un nouveau tableau à chaque appel. On a ajouté une comparaison mémoïsée avec useMemo, en dérivant une clé stable à partir du contenu et du niveau des titres uniquement :
const cleTitres = titres.map( ( t ) => t.niveau + ':' + t.texte ).join( '|' );
const sommaire = useMemo( () => construireSommaire( titres ), [ cleTitres ] );
Résultat mesuré après correction : la latence de frappe est passée de 640 ms cumulés sur une séquence de dix frappes à moins de 60 ms, redevenant imperceptible pour les rédacteurs.
Autres pistes à vérifier en cas de lenteur persistante
- Un composant enfant non mémoïsé avec
React.memo()se re-rend même quand ses props n’ont pas changé si le parent produit de nouveaux objets à chaque rendu (attention aux objets et fonctions inline passés en props). - Un
useEffectsans tableau de dépendances correct peut relancer un traitement lourd à chaque rendu au lieu de le faire une seule fois. - Un
InspectorControlsqui recalcule une liste d’options à chaque rendu (typiquement unselectqui repeuple sesSelectControloptions sans mémoïsation) ralentit aussi la frappe si le panneau reste ouvert.
Pour aller plus loin
Le React DevTools Profiler ne remplace pas la compréhension du store de données de Gutenberg : il indique où chercher, pas pourquoi. Sur ce projet, la vraie leçon retenue par l’équipe a été de systématiquement se demander, avant d’écrire un useSelect(), quelle portion minimale de données le composant utilise réellement — et de ne jamais s’abonner à « tout » par facilité, même dans un prototype, car ces prototypes finissent presque toujours en production.