MT Écrits

Un cinéma de particules sans dépendances

Publié — 9 min de lecture

Vingt ans durant j'ai bâti un travail que personne ne voit. Voici la part visible, démontée avec la même discipline.

Le travail d'infrastructure a une esthétique étrange : bien fait, personne ne le remarque. Une sauvegarde tourne vingt ans en silence, puis un jour elle ne tourne plus, et ce jour-là tout le monde apprend qu'elle existe. Telle est la mesure du travail invisible : on ne le juge que lorsqu'il lâche.

Ce site est une tentative de faire remonter cette habitude à la surface. Ce qui suit n'est pas de l'ornement, c'est la chose démontée : ce qu'il y a, pourquoi c'est là, et ce que cela coûte.

La contrainte vient d'abord

Une seule règle a tout décidé : aucune requête externe.

Ce n'est pas un slogan, c'est un en-tête de réponse. La politique de sécurité du site dit :

Content-Security-Policy: default-src 'self'; script-src 'self';
  style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;
  connect-src 'self'; object-src 'none'; base-uri 'self';
  form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests

À l'instant où vous écrivez default-src 'self', vos options se ferment. Pas de GSAP, pas de three.js, pas de Lenis, pas même une police tirée d'un CDN. Vous voulez une bibliothèque d'animation ? Vous l'écrirez. Vous voulez un défilement adouci ? Vous l'écrirez. Vous voulez un moteur de particules — oui, celui-là aussi.

La règle s'est prouvée une fois, de façon inattendue. Cloudflare injecte son propre script d'analyse en périphérie. La politique du site l'a bloqué :

Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/…'
violates the following Content Security Policy directive: "script-src 'self'"

Sur un serveur local, Lighthouse donne 100 en Bonnes pratiques ; en ligne, 93. Ces sept points, c'est exactement cela. Assouplir la politique aurait été facile. On a plutôt éteint la balise — car une règle ne veut dire quelque chose que lorsqu'elle tient aussi au moment gênant.

Où vivent les particules

Tout le mouvement de la page est dessiné sur un seul <canvas> en WebGL2. Les positions et vitesses des particules ne sont pas sur le processeur ; elles vivent dans deux textures, et à chaque image un nuanceur de fragments les fait avancer. Le résultat est écrit dans une texture, et à l'image suivante cette texture devient l'entrée. La plus vieille astuce du GPGPU : faire le calcul de texture à texture, jamais vers l'écran.

En pratique, cela veut dire que le nombre de particules ne concerne pas le processeur. Sur ordinateur le champ tourne à pleine densité, sur téléphone il descend à 12 100 particules — mais dans les deux cas le travail par image est le même appel de nuanceur.

Tenir deux textures est un détail petit mais porteur. On ne peut pas lire et écrire la même texture en une passe ; c'est un comportement indéfini. Deux tampons alternent donc : cette image lit A et écrit B, la suivante inverse les rôles. Ce ping-pong est l'hygiène de base du GPGPU.

La couleur est un calcul à part. La scène est dessinée en HDR, les zones lumineuses sont extraites et floutées (halo sélectif), puis une courbe ACES ramène le tout dans la plage affichable. Ces trois étapes évitent l'erreur la plus facile du dessin additif : que tout brûle vers le blanc. La suite de vérification mesure exactement cela — la luminance de crête par image doit rester sous le seuil.

Les différences d'appareils sont gérées par une échelle à quatre degrés :

DegréCe qui s'éteint
0Rien — cinéma complet
1Pas de flou de zoom, halo à deux passes
2Le nombre de particules tombe à 55 %
3Repli WebGL1 — GPGPU entièrement coupé

Le degré 3 compte : dans un navigateur sans WebGL2, la page ne cesse pas de fonctionner, elle perd seulement la couche de simulation. Texte, navigation, contact — tout reste.

Huit langues, une seule source de vérité

Le site paraît en huit langues : le turc à la racine, les sept autres dans leurs répertoires. Mais huit pages ne s'écrivent pas à la main.

La page turque est le master. Chaque nœud traduisible porte une marque :

<span data-i="about.e1t">Kurumsal altyapı</span>

Un générateur lit ces marques et extrait i18n/tr.json — 102 clés aujourd'hui. Les sept autres langues remplissent les mêmes clés avec leur propre texte, et le générateur imprime les pages. Les champs traduisibles du JSON-LD passent par le même pipeline : intitulé de poste, ville, domaines d'expertise, description du service.

La règle la plus dure de ce montage : une clé manquante arrête la construction. Pas de repli, pas de bascule vers l'anglais. Si une langue prend du retard, rien ne sort. Mieux vaut s'arrêter que publier une page traduite à moitié en silence.

Que la page turque ne soit pas générée est aussi délibéré. Le master s'écrit à la main parce qu'il porte des commentaires de mesure, des marges de masque et des notes d'alignement optique — les déplacer dans un générateur les rendrait illisibles.

Le résultat est mesurable : entre les huit pages, les liens hreflang sont réciproques dans 64 directions sur 64, et x-default est correct sur les huit.

Les polices sont la partie la plus lourde

La page entière pèse 172 Ko sur onze ressources. La répartition surprend :

PostePoids
Polices (woff2)116 Ko
JavaScript56 Ko
Images0 Ko
CSS0 Ko — en ligne

Il n'y a pas une seule <img> sur le site. Toute la charge visuelle est en canvas et en SVG. En face, quinze fichiers woff2 : quatre familles pour le latin, des substituts distincts pour le cyrillique. Les fichiers cyrilliques sont rattachés aux mêmes noms de famille via unicode-range — la page russe ne télécharge donc jamais un fichier latin ; le navigateur choisit seul.

Les déclarations @font-face sont intégrées dans le HTML. C'est une décision mesurée : une feuille de style séparée était la seule ressource bloquant le rendu, et l'intégration a fait passer le score Lighthouse mobile de 99 à 100.

Transfert réel après compression :

RessourceBrutBrotliRatio
HTML87 890 o20 675 o76,5 %
app.js186 011 o55 354 o70,2 %

Sans mesure, rien ne s'appelle « fini »

Le dépôt tient deux suites de vérification : 72 tests pour la physique de la scène, 100 pour la traduction et la couche de découverte. Les deux tournent dans un vrai navigateur et aucune ne regarde une capture d'écran — elles lisent la luminance sur le canvas, interrogent les canaux d'état et contrôlent les seuils par les chiffres.

La raison est simple : la vérification visuelle ment. Un panneau peut défiler tout seul, un onglet passer en arrière-plan, requestAnimationFrame n'avoir jamais battu. La capture dit « ça marche » alors que rien n'a commencé.

Comment la mesure ment

Ce qui a pris le plus de temps ici, ce n'est pas le code. C'est d'apprendre à ne pas faire confiance à la mesure elle-même.

Un exemple. Une page s'est ouverte dans un navigateur d'automatisation et le contenu est resté invisible soixante-dix secondes. Écran noir, le rideau d'ouverture figé. La lecture évidente : la page est bloquée.

Trois choses ont été mesurées avant de conclure :

document.visibilityState  →  "hidden"
images rAF en 2 secondes  →  0
progression d'ouverture   →  0

L'onglet était en arrière-plan. requestAnimationFrame ne bat pas dans les onglets d'arrière-plan ; la séquence n'avait jamais démarré. Il n'y avait aucune faute sur la page — la faute était dans l'environnement de mesure.

Il s'en fallait d'un pas pour que cela entre dans un rapport comme « constat ». Le dépôt a une règle écrite pour cela : ne dis pas que c'est fait sans mesurer — et mesure aussi ce que tu crois avoir vu. Une capture n'est pas une preuve ; la preuve, c'est la luminance lue sur le canvas, ou le canal d'état lui-même.

Deux pannes silencieuses

Ce qu'un audit révèle de plus intéressant, c'est ce qu'il ne cherchait pas.

Premièrement : le champ lastmod du plan de site lisait sa date dans le plan de site précédent. Le générateur prenait sa propre sortie en entrée. Une fois écrite, la valeur n'aurait jamais avancé — quel que soit le changement de contenu, le fichier aurait montré à jamais le même jour.

Un signal de fraîcheur figé devient un mensonge. Un lastmod périmé est pire que rien : il dit au robot que cette page ne change pas. La date vit désormais à un seul endroit, et le plan de site la suit.

Deuxièmement : la liste des champs JSON-LD traduisibles était dupliquée dans le générateur à deux endroits — un pour l'extraction, un pour l'écriture. Que se passe-t-il si un nouveau champ n'est ajouté qu'à l'un des deux ? Le champ n'est pas traduit, en silence. Le texte turc est recopié tel quel dans les données structurées de sept pages, et aucun des cent tests ne le voit.

C'était un piège qui aurait cassé précisément ce qu'il devait réparer : celui qui ajoute le champ description manquant aurait écrit du turc dans sept langues sans s'en apercevoir. La liste est maintenant une source unique.

La leçon commune : les pannes les plus dangereuses d'un système sont celles où aucun test n'échoue.

L'astuce refusée

La ligne la plus honnête de ce projet n'est pas dans un rapport de bogue. Elle est dans un procès-verbal de décision.

Sur mobile, Lighthouse ne parvient pas à produire de valeur Largest Contentful Paint pour la page d'accueil. La cause est la séquence d'ouverture : quatre secondes de rideau, puis le gel au contact, puis l'explosion, puis la transition d'encre. Avant cela aucun texte du DOM n'est visible, et Lighthouse ferme sa trace bien plus tôt. Résultat : NO_LCP — la métrique n'est pas calculable.

Un correctif a été mesuré. En portant l'opacité initiale de la couche de contenu à 0.004 au lieu de 0, le LCP tombe de 9 052 ms à 164 ms, et le titre complet est estampillé candidat LCP. Il n'y a aucun coût visuel : 0,004 d'opacité apporte au plus 1/255 d'unité, c'est-à-dire rien.

C'est précisément pour cela qu'il a été refusé.

Le LCP se définit comme le plus grand bloc de texte visible dans la fenêtre. Un titre à 0,004 d'opacité n'est pas visible. L'exception opacity: 0 de Chromium existe pour bloquer exactement ce schéma ; 0,004 se glisse le long de son bord. Le chiffre s'améliorerait, la vérité non : le visiteur verrait toujours le titre neuf secondes plus tard.

La décision est entrée dans la documentation ainsi : ne ramenez pas ce schéma.

Pourquoi cette page est différente

La page que vous lisez ne charge pas le moteur décrit plus haut. Le champ de particules derrière elle est un script séparé et très petit ; le texte est entièrement visible dès la première peinture et n'attend derrière aucune porte d'opacity.

La raison est la section précédente. Un texte existe pour être lu et cité. L'ouverture cinématographique est la signature de la page d'accueil, mais sur une page de texte la même séquence retarderait non seulement la mesure mais la lecture. Cette section a un budget écrit — LCP sous 2,5 secondes, JavaScript de page sous 60 Ko, aucune porte d'opacité sur le contenu — et la suite de vérification le contrôle à chaque exécution.

Même discipline, autre résultat. Si l'esthétique du travail invisible est de ne jamais lâcher, l'esthétique du travail visible est de pouvoir être mesuré.