Skip to content

perf(theme): add a core web vitals baseline - #520

Open
francoistibo wants to merge 1 commit into
masterfrom
feat/cwv-baseline
Open

francoistibo wants to merge 1 commit into
masterfrom
feat/cwv-baseline

Conversation

@francoistibo

@francoistibo francoistibo commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Dotez les projets d'images LCP « opt-in », d'images statiques redimensionnées, d'un logo à faible priorité, d'une initialisation JavaScript en mode veille et d'animations sans défilement, afin que les nouveaux thèmes démarrent avec des scores CWV plus proches de la moyenne.

Résumé

  • Ajoute un mode LCP opt-in sur get_the_image() (is_lcp) : fetchpriority="high", decoding="sync", pas de loading="lazy". fetchpriority="high" n'est jamais combiné avec loading="lazy".
  • Ajoute get_theme_static_image_markup() / the_theme_static_image() : width et height lus dans le fichier du thème (SVG ou raster) pour limiter le CLS. Un alt vide est rendu alt="" (image décorative) ; false ou null retire l'attribut.
  • Conserve alt="" quand l'appelant le demande sur une image de la médiathèque, pour que les technologies d'assistance ignorent l'image.
  • Passe le logo du header en fetchpriority="low", pour qu'il ne concurrence pas la vraie image LCP.
  • Précharge les fonts uniquement via le filtre bea_theme_framework_preload_fonts. La liste est vide par défaut : seuls les fichiers réellement affichés au premier rendu doivent y figurer.
  • Remplace la boucle de scroll de Animation par un IntersectionObserver partagé. Les options (start, end, playOnce, hideOnReachEnd) et les callbacks (onInit, onShow, onHide, onDestroy) restent les mêmes.
  • Ajoute deferInitialization() pour lancer le JavaScript non critique au repos du navigateur (INP).
  • Documente ce socle et ce qui reste à configurer par projet (WP Rocket, robots.txt, cache, fonts, embeds tiers) dans le README.

Plan de test

  • the_image() avec 'is_lcp' => true produit fetchpriority="high", decoding="sync" et aucun loading="lazy".
  • the_image() avec 'alt' => '' produit alt="".
  • the_theme_static_image() sur un SVG qui a width et height reprend ces attributs et alt="".
  • Le logo du header a fetchpriority="low".
  • Sans le filtre bea_theme_framework_preload_fonts, aucun <link rel="preload" as="font"> n'est imprimé.
  • Un élément .js-animation-opacity reçoit is-visible en entrant dans le viewport, et reste masqué avant.
  • prefers-reduced-motion et l'impression désactivent toujours les animations.

Give projects opt-in LCP images, sized static images, a low-priority logo, idle JS init, and scroll-free animations so new themes start closer to good CWV scores.

Co-authored-by: Cursor <cursoragent@cursor.com>
@herewithme

Copy link
Copy Markdown
Member

Revue perf backend

Très bon socle CWV dans l'ensemble (LCP / CLS / INP bien ciblés, README honnête sur ce qui reste par projet). Mon seul vrai point de vigilance est backend, et il est concentré dans get_theme_static_image_dimensions() / get_theme_static_image_markup().

I/O fichier répété, non caché

Aujourd'hui, chaque appel de the_theme_static_image() paye, à chaque requête, sans mémoïsation :

  • get_theme_file_uri() → résolution child/parent (contient des file_exists())
  • get_theme_static_image_dimensions() → re-résout le même fichier via get_theme_file_path() (encore des file_exists())
  • is_readable() (stat)
  • puis file_get_contents() + preg_match (SVG) ou wp_getimagesize() (raster)

Soit ~3-4 syscalls fichier + un read + de la regex par image. En boucle (ex. archive de 80 cartes avec placeholder), c'est un pattern N+1 sur le filesystem.

Important : sur une infra où le web root / le thème est monté en réseau (NFS, EFS, montage partagé multi-front), chaque stat/open/read devient un aller-retour réseau de l'ordre du ms → potentiellement des centaines de ms de I/O invisibles en dev local SSD. Ni le realpath cache ni l'OPcache ne couvrent file_get_contents/wp_getimagesize.

Pistes (par ordre de préférence)

  1. Dimensions calculées au build et stockées dans le manifest assets.json (déjà mis en cache depuis 486448a2) → au runtime, zéro accès fichier. Option la plus propre et robuste sur FS réseau.
  2. Sinon cache persistant (object cache, clé = version de build) + mémoïsation par requête (static $cache). La mémoïsation seule ne sauve pas le 1er hit par requête sur FS réseau ; le cache persistant si.
  3. A minima : résoudre le chemin une seule fois (éviter le double get_theme_file_uri() + get_theme_file_path()) et mémoïser par requête.

Secondaire : new WP_HTML_Tag_Processor() instancié par image (pur CPU, négligeable à l'unité, s'additionne à N élevé) — à ne regarder qu'après l'I/O.

Benchmark avant/après souhaité

Avant merge, j'aimerais un A/B master vs cette branche, même jeu de données / même infra :

  • Template de stress (généré par WP-CLI) rendant the_theme_static_image() pour N ∈ {1, 20, 50, 100}, mix SVG / raster + un gros SVG, plus une page de contrôle sans le helper.
  • Blackfire + wp profile hook : temps inclusif et nombre d'appels de get_theme_static_image_dimensions, file_get_contents, wp_getimagesize, is_readable, get_theme_file_path/uri.
  • Runs cache objet froid vs chaud, et SSD local vs web root monté réseau (latence injectée) → mesurer le delta par N.

Critères : sur cache chaud, le coût doit être ~indépendant de N et file_get_contents/wp_getimagesize appelés 0 fois.

Point annexe à mesurer

decoding="sync" sur l'image LCP : à objectiver (WebPageTest) avant de le garder par défaut — async est souvent préférable pour ne pas bloquer le paint.

'before' => '',
'after' => '',
'default' => false,
'is_lcp' => false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour une utilisation avec ARI, il faut bien le laisser à false car c'est ARI qui gère automatiquement ce fetchpriority via la déclaration de sa taille d'image au bon format.

Le documenter ? gérer un warning ou autre ? ou bien le tester avec ARI et s'assurer que ça ne casse rien si c'est à true ?

*
* @return void
*/
public function preload_fonts(): void {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ça fonctionne bien sur les fonts du dossier dist ? donc fonctionne dans un contexte fontsource ou font custom ? les fontsources sont aussi copiées dans le dossier dist

continue;
}

// Empty alt marks a decorative image; omitting the attribute is not equivalent for AT.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y avait déjà un fix dans la fonction the_image pour avoir un alt à vide si on le force, sinon l'attribut était supprimé.

Ici c'est le même principe, sauf que c'est une nouvelle fonction pour appeler une image venant du thème c'est ça (avec les attributs width/height etc...) ?

*/
add_action( 'wp', [ $this, 'register_assets' ] );
add_action( 'wp_head', [ $this, 'preload_fonts' ], 1 );
add_filter( 'get_custom_logo_image_attributes', [ $this, 'deprioritize_custom_logo' ], 20 );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Utile ? car notre logo est généralement un svg, soit inline, soit venant du thème ou alors une image venant d'un champ ACF. Ce filtre se base sur quoi ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Car on est pas en FSE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Voir avec @n-langle vu que ça touche à oneloop ?

@cedric07

Copy link
Copy Markdown
Contributor

Good job, merci @francoistibo :)

@francoistibo

Copy link
Copy Markdown
Contributor Author

@asadowski10 voilà une PR qui reprend ce que j'ai pu faire de générique en terme de perf sur Liora. y'a quasi la meme sur BBT. on va repasser sur tout ce qui touche a nos pratiques TD en réunion d'équipe, et je veux bien une petite review plugin dev ensuite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants