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.
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 :
<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 :
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 statique | Démo client | |
|---|---|---|
| Présent dans le fichier HTML | oui | placeholder seulement |
| Lisible sans JavaScript | oui | non |
| Indexé par les moteurs | oui | sans objet |
| Coût pour qui fait défiler sans s'arrêter | zéro | zé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.



