Vanessa Depraute

Ingénieure IA Agentique & Développeuse Full-Stack

tape les mots soulignés !
← Toutes les notes
3 min de lecture

Des démos interactives qui survivent au rendu statique

Le HTML statique et les démos WebGL live tirent dans deux directions opposées. Voici la frontière que j'ai fixée, et la seule règle qui met tout le monde d'accord.

  • #react
  • #ssg
  • #webgl
  • #mdx

Chaque article de ce site est écrit en MDX et pré-rendu en HTML statique au moment du build. C'est bon pour les lecteurs et bon pour les moteurs de recherche : le texte est déjà dans le fichier avant qu'une seule ligne de JavaScript ne s'exécute.

C'est aussi, a priori, incompatible avec ce que je veux réellement publier. Un tutoriel sur les courbes d'easing ne vaut pas grand-chose sans une courbe qu'on peut manipuler. Un article sur les shaders a besoin du shader qui tourne.

Les deux exigences se contredisent :

  • Le texte doit être dans le HTML. Rendu une fois, au build, dans Node.
  • Les démos ne peuvent pas y être. Elles ont besoin d'un canvas, d'un GPU, d'un pointeur, d'une frame d'animation.

Ce qui casse, précisément

Pendant le build, React rend chaque page avec renderToString. Il n'y a pas de window, pas de document, pas de requestAnimationFrame. Tout ce qui va les chercher pendant le rendu ne se dégrade pas gentiment : ça lève une erreur, et ça emporte le build entier.

L'échec le plus vicieux est pire, parce qu'il est silencieux :

// 📖 Ça a l'air inoffensif. Ça s'exécute pendant le rendu. Ça casse le build.
const [isDark, setIsDark] = useState(() =>
  document.documentElement.classList.contains('dark-mode'),
);

Et même le code qui ne plante pas peut être faux. Tout ce qui est non déterministe, Math.random(), new Date(), produit une valeur dans Node et une autre dans le navigateur. Le markup que React hydrate ne correspond alors plus à celui qu'il trouve. React recolle les morceaux, prévient dans la console, et la page clignote.

La frontière

La règle sur laquelle j'ai atterri tient en une phrase :

Le texte se rend côté serveur. Les démos se montent dans le navigateur. Rien ne chevauche la ligne.

Chaque démo passe par un seul composant qui applique cette règle :

un-article.mdx
<Demo
  title="Courbes d'easing"
  height={340}
  load={() => import('@/content/demos/EasingPlayground')}
/>

Ce composant fait trois choses qui méritent d'être explicitées.

Il réserve la place avant que la démo existe. La scène a une hauteur fixe, donc le placeholder présent dans le HTML statique occupe exactement la même boîte que la démo vivante. Rien ne bouge au montage, et la mise en page du fichier pré-rendu est déjà la bonne.

Il diffère le montage. Le vrai composant n'est construit qu'une fois dans un navigateur. Node ne voit jamais le contexte WebGL, donc il n'a jamais l'occasion de s'en plaindre.

Il découpe le bundle. load est un import dynamique, donc Three.js part dans son propre chunk. Un lecteur qui décroche à l'introduction ne paie jamais pour une bibliothèque qu'il n'a pas vue.

Voici le résultat, intégré dans cet article exactement comme n'importe quel autre composant :

Courbes d'easing
Interactive demo

Ce que ça rapporte

Le compromis mérite d'être reformulé, parce qu'on perd vite de vue ce que la contrainte protège réellement.

Texte statiqueDémo client
Présent dans le fichier HTMLouiplaceholder seulement
Lisible sans JavaScriptouinon
Indexé par les moteursouisans objet
Coût pour qui fait défiler sans s'arrêterzérozéro, elle ne charge pas

L'absence de la démo dans le HTML n'est pas une perte. Personne ne cherche à indexer une courbe d'easing. Ce qui compte, c'est que cette absence ne coûte rien : les mots autour sont tous là, la page est correctement disposée, et la partie interactive arrive quand, et seulement quand, quelqu'un peut s'en servir.