vendredi 25 septembre 2026

À propos

Contact

Blocs Gutenberg

Un bloc qui ralentit la saisie dans l’éditeur : profiler et corriger

Chaque frappe au clavier déclenche un temps de latence perceptible dans l'éditeur ? React DevTools Profiler permet de trouver le useSelect coupable en quelques minutes.

Par Clément Hadrot • 6 décembre 2023 • 5 min de lecture • Aucun commentaire
Un bloc qui ralentit la saisie dans l'éditeur : profiler et corriger

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.

L'essentiel à retenir : Le Profiler React révèle les rendus en cascade invisibles à l'œil nu ; Un useSelect trop large re-render à chaque frappe, même hors du bloc concerné ; Restreindre la sélection et mémoïser corrige la majorité des cas

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 useEffect sans tableau de dépendances correct peut relancer un traitement lourd à chaque rendu au lieu de le faire une seule fois.
  • Un InspectorControls qui recalcule une liste d’options à chaque rendu (typiquement un select qui repeuple ses SelectControl options 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.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi