Skip to content

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

Open
francoistibo wants to merge 5 commits into
masterfrom
feat/cwv-baseline
Open

francoistibo wants to merge 5 commits 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.

Note

Medium Risk
Touche le rendu d’images (LCP, attributs) et le comportement des animations au scroll ; des régressions visuelles ou de timing sont possibles sur les thèmes qui s’appuient sur l’ancien ScrollObserver.

Overview
Introduit une base Core Web Vitals pour les thèmes BFF : helpers d’images, build, perf front et doc.

Images : get_the_image() accepte 'is_lcp' => true (opt-in) pour retirer le lazy-load et poser fetchpriority="high" ; decoding n’est plus imposé par le thème, seulement via $attributes. Les helpers refusent fetchpriority="high" avec loading="lazy". Nouveaux get_theme_static_image_markup() / the_theme_static_image() pour les assets dist/ : width/height (anti-CLS), alt="" par défaut pour le décoratif, markup via WP_HTML_Tag_Processor. Les dimensions viennent de imageDimensions dans dist/assets.json, alimenté au build par WebpackStaticImageDimensionsPlugin (sharp / parsing SVG), avec repli lecture disque si le manifeste manque. Le service Assets expose get_static_image_dimensions() et tolère mieux un manifeste partiel.

Perf : filtre vide par défaut bea_theme_framework_preload_fonts → <link rel="preload"> woff2 en wp_head. JS : Animation passe d’une boucle scroll (ScrollObserver) à des IntersectionObserver partagés par rootMargin (mêmes options/callbacks, repli si IO indisponible). Nouveau deferInitialization() (requestIdleCallback + timeout).

Build / styles : plugin webpack enregistré dans config/plugins.js. Petites mises à jour Dart Sass (map.get, sass()). README et CHANGELOG détaillent LCP, ARI/is_lcp, polices et responsabilités projet.

Reviewed by Cursor Bugbot for commit bd99d38. Bugbot is set up for automated code reviews on this repo. Configure here.

@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.

Comment thread inc/Helpers/Formatting/Image.php
Comment thread inc/Services/Performance.php
Comment thread inc/Helpers/Formatting/Image.php
Comment thread inc/Services/Performance.php Outdated
Comment thread src/js/classes/Animation.js
@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

@francoistibo

Copy link
Copy Markdown
Contributor Author

@herewithme dans ce commit, Les dimensions des images statiques sont maintenant calculées au build et stockées dans dist/assets.json (imageDimensions) ; au runtime on lit le manifeste déjà en cache, donc plus de lecture fichier en boucle sur un thème buildé. Fallback fichier une fois par chemin/requête si l’asset n’est pas dans le manifeste. URI mémoïsée, sans double résolution child/parent quand il n’y a pas de thème enfant.

@herewithme

Copy link
Copy Markdown
Member

@herewithme dans ce commit, Les dimensions des images statiques sont maintenant calculées au build et stockées dans dist/assets.json (imageDimensions) ; au runtime on lit le manifeste déjà en cache, donc plus de lecture fichier en boucle sur un thème buildé. Fallback fichier une fois par chemin/requête si l’asset n’est pas dans le manifeste. URI mémoïsée, sans double résolution child/parent quand il n’y a pas de thème enfant.

Top, un bench avant/après ? :)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b82bd4f. Configure here.

Comment thread src/js/classes/Animation.js
francoistibo and others added 3 commits October 20, 2026 09:44
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>
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