Ce site est la démonstration.
Cette page présente nos outils, notre façon de travailler et les technologies qui font tourner ce site. On l'a écrite pour quelqu'un qui n'a pas besoin qu'on lui explique ce qu'est un CDN, et qui préfère vérifier. Presque tout ce qui suit se lit dans le code, ou se mesure dans votre navigateur pendant que vous lisez.
- images par seconde
- 60
- durée d'une image
- 16,7ms
- GPU (UNMASKED_RENDERER)
- non détecté
- triangles par image
- n.d.
- draw calls par image
- n.d.
- chargement (Navigation Timing)
- n.d.s
- devicePixelRatio
- n.d.
Relevé dans votre navigateur : renderer.info de la scène ci-contre, extension WEBGL_debug_renderer_info, entrée navigation de la Performance API.
Une scène 3D qui doit tenir sur un GPU intégré
On ne vise pas la carte graphique d'un joueur. La machine de référence, c'est un portable de bureau avec sa puce graphique intégrée. Le reste du pipeline découle de ce choix.
- 01
D'où sort la scène ?
La salle et l'introduction sont modélisées dans Blender, puis exportées en glTF binaire. Le code ne fouille pas la scène à l'aveugle : il retrouve l'eau, le sol ou le socle d'une station par son nom, et la hiérarchie de l'export lui sert d'index. Qu'un nom change d'un côté sans l'autre, et une station se retrouve sans socle. C'est arrivé cette semaine, après un simple changement de libellé.
Blender · GLB · nœuds nommés - 02
Compresser sans perdre les ancres
La géométrie part en Draco ; quatre workers la décodent au chargement. Pour les textures, l'export de Blender laisse traîner des copies PNG et JPEG à côté du WebP, qu'un petit script maison retire. Sur la scène d'introduction, le fichier passe de 11,4 Mo à un peu plus de 6.
On ne lance jamais gltf-transform optimize. Il aplatit la hiérarchie et supprime les nœuds vides, alors que ces nœuds vides sont justement nos points d'ancrage.
Draco · EXT_texture_webp · 4 workers - 03
Pourquoi la passe GTAO est-elle éteinte ?
Le post-traitement tient en peu de passes : rendu sur une cible HalfFloat, UnrealBloom à mi-résolution, OutputPass. Sur GPU intégré, on abandonne le MSAA pour un FXAA en fin de chaîne.
La GTAO est câblée, à 0,4 de la résolution et recalculée une image sur deux. À sa première mise en route, les titres des stations sont ressortis en pavés noirs. La passe redessine la scène avec un matériau de normales imposé, et nos quads de texte n'ont pas de normales. On masque maintenant le texte le temps de la passe. Elle reste quand même coupée en production.
Three.js · UnrealBloomPass · FXAA - 04
Comment un titre reste-t-il net de près ?
Les libellés des stations viennent d'un atlas MSDF de Chakra Petch. Le shader prend la médiane des trois canaux et applique un anticrénelage fixe, sans passer par fwidth. Par-dessus, on a ajouté une aberration chromatique canal par canal, un balayage de révélation piloté par l'uniform uReveal, et une petite déchirure quand la souris passe dessus.
Approchez la caméra d'un titre : il ne bave pas.
atlas 117 glyphes · ShaderMaterial - 05
Près de mille arbres posés au lancer de rayons
La forêt de l'introduction compte 993 arbres et 2 375 touffes d'herbe ; avant élagage, on semait 3 340 touffes. Chacun est posé par un rayon tiré vers le terrain, à partir d'une graine fixe, si bien que la forêt est la même à chaque visite. Sans BVH, ces tirs gelaient le démarrage pendant une seconde et demie. Avec three-mesh-bvh, le dernier relevé donne 10,8 ms. Le tout part ensuite en InstancedMesh : 26 maillages pour 8 166 placements.
three-mesh-bvh · raycast · instanciation - 06
Quel budget pour une image ?
Sur GPU intégré, devicePixelRatio reste à 1 et l'image ne dépasse pas 2,2 mégapixels. Pendant l'introduction, la salle se prépare en coulisses. compileAsync compile ses shaders, puis on la rend par lots de trois objets sous un calque transparent, qu'on ne lève qu'une fois une image complète obtenue. Personne ne voit jamais une salle à moitié chargée.
Quand une rubrique s'ouvre, l'horloge s'arrête et la boucle ne rend plus rien. Dans le tunnel, la visite guidée et la signalétique de la salle, une image ne compte jamais pour plus de 50 ms (40 pour la déchirure de l'introduction) : après un gel du navigateur, l'animation reprend où elle en était au lieu de sauter.
Le bug qui nous a coûté le plus cher tenait en une ligne. Un requestAnimationFrame placé après un return : au premier passage dans la condition, la boucle mourait pour de bon, sans la moindre erreur en console. L'appel est devenu la toute première instruction de la boucle de la salle.
compileAsync · delta ≤ 50 ms · ?off= / ?on=
Mesurer avant de toucher
On ne règle rien au jugé. Un réglage de rendu entre avec une mesure avant et une mesure après, ou il n'entre pas. Un score Lighthouse isolé ne prouve rien, deux passages sur la même page peuvent s'écarter de plusieurs points ; on garde la médiane de trois passages et le rapport qui va avec.
Les captures de la scène se font dans une vraie fenêtre Chrome. En headless, Chrome bascule sur SwiftShader, un rendu logiciel, et tout ce qui touche au GPU devient faux sans prévenir personne.
Pourquoi aucun framework ?
- Une page, deux lectures
Chaque rubrique est un fichier HTML autonome, avec son titre, sa description et son URL canonique. C'est ce que lit un moteur de recherche, ou quelqu'un qui arrive par un lien partagé.
Dans la salle 3D, overlay.js va chercher ce même fichier, en extrait l'article avec DOMParser et l'injecte au-dessus de la scène, feuilles et scripts compris. Un script injecté expose une fonction d'initialisation qui renvoie son destroy() : à la fermeture, la rubrique ne laisse rien derrière elle.
Le robot et le visiteur lisent donc la même source. FuzeKit le vérifie à chaque campagne, au caractère près, et au dernier passage les neuf rubriques étaient identiques.
DOMParser · data-css / data-js / data-init · anti-cloaking- Pas de build, et c'est voulu
Le dépôt du site n'a pas de package.json. Three.js, GSAP et three-mesh-bvh sont copiés dans un dossier vendor avec leur licence, et une import map les résout. Un framework réglerait ici un problème que le site n'a pas.
Le seul outil de construction est un script Node qui ajoute à chaque CSS et à chaque JS une empreinte SHA-256 tronquée. En production, ces fichiers restent un an en cache sans revalidation ; le HTML, lui, n'y reste jamais. Une empreinte en retard sur son fichier, et le script de déploiement refuse le commit.
import map · ?v=<sha256> · Cache-Control immutable- Aucune requête vers un tiers
Les polices sont sous-ensemblées avec pyftsubset et servies en woff2 depuis notre domaine. Aucun script, aucune feuille de style, aucune image ne vient d'ailleurs. On l'a mesuré le 13 octobre 2026 avec un profil Chrome vierge, trois passages par page : pas un domaine tiers, pas un cookie, sur l'accueil comme sur la page des offres.
woff2 sous-ensemblés · 0 tiers · 0 cookie- Quand renonce-t-on à la 3D ?
Mieux vaut renoncer tôt que laisser un téléphone chauffer pour rien. boot-3d.js abandonne la scène dès que l'un de ces signaux tombe :
- prefers-reduced-motion activé ;
- Save-Data demandé par le navigateur ;
- réseau annoncé en 2G ;
- pas de WebGL2 ;
- rendu logiciel détecté ;
- MAX_TEXTURE_SIZE sous 4096 pixels ;
- moins de 4 Go de mémoire annoncés ;
- moins de 4 cœurs ;
- sonde de 18 images, les trois premières écartées, médiane au-dessus de 34 ms ;
- contexte WebGL perdu en route ;
- salle toujours pas prête au bout de 25 secondes.
La sonde de 18 images, elle, ne compte pas les trois premières : c'est la médiane des suivantes qui doit rester sous 34 ms. Le visiteur garde le menu 2D et exactement les mêmes pages.
WebGL2 · sonde 18 images · médiane 34 ms
Deux bancs d'essai, écrits au studio
FuzeKit audite les sites. FuzeScan s'attaque aux applications et mesure les traceurs.
L'idée de départ tenait en une phrase : un seul bilan par campagne, comparable à celui de la campagne d'avant.
FuzeKit
Que fait une campagne FuzeKit ?
FuzeKit est une application Python, avec une interface CustomTkinter, empaquetée par PyInstaller. Une campagne enchaîne les outils suivants et rend un seul bilan en Markdown :
- Lighthouse, en profils mobile et desktop, sur le canal chrome : chrome-headless-shell rend en SwiftShader, et ce rendu changeait le verdict d'axe sur 2 pages sur 59.
- Playwright, pour le test de fumée : la page répond sous 400, porte un titre et n'écrit aucune erreur dans la console.
- axe-core, par @axe-core/playwright : l'accessibilité de la page rendue.
- ESLint : le JavaScript.
- html-validate : le HTML.
- linkinator : chaque lien et chaque ressource du site.
- Semgrep, hors ligne : 584 règles OWASP Top 10 et PHP, capturées localement avec manifeste et empreinte, métriques coupées.
- gitleaks, en deux passes : l'historique Git, puis l'arbre de travail, fichiers ignorés compris.
- En-têtes HTTP : CSP (mode rapport compris), HSTS d'au moins six mois, nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy, bannières Server et X-Powered-By.
- WPScan, dans une image Docker épinglée par sha256 ; les versions relevées sont croisées avec un miroir local de Wordfence Intelligence.
- Conformité : la charte du projet (palette, invariants, référencement), en Python pur et hors réseau.
- Anti-cloaking : le textContent de la page servie seule contre celui du même article injecté dans l'overlay, espaces normalisés, égalité stricte.
- Captures, en plusieurs largeurs, JavaScript coupé, mouvement réduit.
Un écart nous a occupés une journée entière. Lighthouse et axe ne tournaient pas sur le même Chrome : axe passait par chrome-headless-shell, donc par SwiftShader, et rendait un autre verdict sur 2 pages sur 59. Les deux pages WebGL, évidemment. Tous les outils tournent maintenant sur le Chrome du système.
En mode passif, FuzeKit ne fait que lire : des GET et des HEAD sur les pages du périmètre et sur trois URL de convention, sans suivre un seul lien d'action. Suivre un lien « ajouter au panier », ce serait remplir le panier du site qu'on audite.
Pour passer en actif, il faut convaincre un gardien de localité. Il n'admet une cible que si toutes ses adresses DNS sont privées ou de bouclage, si c'est la production déclarée du projet, ou si une autorisation écrite nomme cet hôte précis. Dans le doute, il la traite comme une production.
Chaque outil se compare à son propre passage précédent : constats apparus ou disparus, pages entrées ou sorties du périmètre. Pour Lighthouse, on tolère huit points de bruit en performance et deux ailleurs, faute de quoi chaque campagne crierait au loup.
L'outil se teste lui-même avec 2 668 tests et 1 333 mutations déclarées, chacune censée casser le test qui la nomme.
Le bilan s'accompagne d'un summary.json où chaque constat neutralisé figure avec sa raison. Pour un client, on en tire un rapport de deux pages. Celui que vous voyez ici porte sur un site fictif.
FuzeScan
Comment attaquer une application sans abîmer ses données ?
FuzeScan pilote OWASP ZAP par son Automation Framework, dans l'image zap-stable branchée sur le réseau du conteneur web DDEV de l'application. Les plans sont générés en JSON à chaque passage, personne ne les écrit à la main. Une adresse qui n'est pas locale est refusée.
En mode rapide, le spider explore sans envoyer de formulaire, puis on attend la fin du scan passif. Le mode complet remplit les formulaires et lance le scan actif, en renouvelant les jetons anti-CSRF au fil de l'eau.
Avec connexion, on fait un passage par compte et par rôle, pour que les alertes d'un administrateur ne se mêlent pas à celles d'un simple membre. Le lien magique est lu dans Mailpit et rejoué avec son jeton CSRF ; toutes les vingt requêtes, ZAP vérifie qu'il est encore connecté. Derrière une double authentification TOTP, on ne stocke aucun secret. Quelqu'un se connecte à la main dans un Chrome piloté par Playwright, et FuzeScan transmet le cookie de session à ZAP. Si la session tombe, le scan s'arrête.
Le scan complet écrit dans la base. On en prend un instantané DDEV avant de commencer, on la restaure avant chaque rôle et à la fin dans un bloc finally, puis on contrôle le jeu de données. Le 3 octobre 2026, un compte super admin s'est désactivé lui-même en plein scan, à force de cliquer consciencieusement sur tout ce qu'il trouvait. Depuis, les URL qui désactiveraient le compte qui scanne sont exclues.
Chaque passage se termine sur un état nommé : ok, jamais connecté, session perdue, interrompu, infrastructure perdue. Un passage interrompu ne se présente jamais comme complet. La version livrée tient sur 465 tests et 328 mutations, sans survivante.
Avant accord, le module RGPD de FuzeScan, charge une page comme le ferait un premier visiteur qui ne touche à rien. Chrome du système, profil vierge à chaque passage, fuseau America/Martinique, réseau au calme puis cinq secondes d'attente. Il relève alors :
- les domaines tiers contactés, au sens eTLD+1, hors de la chaîne de redirection ;
- les cookies déposés ;
- les cookies posés sous le domaine du site par un script tiers ;
- les sous-domaines délégués par CNAME à un autre service ;
- les requêtes en échec et les erreurs HTTP ;
- la présence d'un bandeau, repérée par un texte qui parle de cookies et un bouton accepter ou refuser, dans tous les cadres et le shadow DOM.
Le rapport aligne des faits datés et chiffrés, et n'écrit jamais « conforme » : juger, c'est le travail d'un juriste. Le 13 octobre 2026, trois vitrines martiniquaises contactaient deux, cinq et cinq domaines tiers avant le moindre accord. Ce site, aucun.
Aucun banc automatisé ne remplace un test d'intrusion mené à la main. Pour une application qui manipule des données sensibles, on le recommande, confié à un cabinet spécialisé ; nos bancs lui laissent ce qui ne se détecte pas automatiquement.
Où tourne le code, et qui le garde ?
- Sur le poste de développement
Les sites statiques et PHP tournent sous Laragon, avec un domaine en .test et son certificat local. Les applications vivent dans Docker, avec DDEV et des certificats émis par mkcert ; c'est dans ce même réseau que FuzeScan lance ZAP. Le code est rangé dans des dépôts Git privés sur GitHub. Sur celui du site, Dependabot surveille les dépendances, alertes et mises à jour de sécurité comprises.
Laragon · Docker · DDEV · mkcert · Git · Dependabot- Des serveurs en Europe
Les fichiers publics passent par le CDN mondial de l'hébergeur, servi sur notre domaine, et l'hébergeur fait une sauvegarde quotidienne. HTTPS partout, HSTS d'un an, sous-domaines compris.
Europe · CDN · HSTS- Combien de copies existe-t-il ?
À l'atelier, un NAS en RAID 1. Hors de l'atelier, GitHub et un drive cloud gardent le code versionné, et seulement le code. Le site en ligne et ses données sont couverts par la copie quotidienne de l'hébergeur.
NAS RAID 1 · GitHub · drive cloud- Ce que le serveur annonce de lui-même
Il pose ses propres en-têtes : HSTS, nosniff, Referrer-Policy en strict-origin-when-cross-origin, X-Frame-Options en SAMEORIGIN, et une Permissions-Policy qui ferme caméra, micro, géolocalisation, paiement et USB. X-Powered-By a disparu, le listage des répertoires aussi.
La CSP tourne en mode rapport, avec les empreintes SHA-256 des scripts en ligne. Les secrets sont lus hors de la racine web et ne sont jamais versionnés. Chaque dépôt passe Semgrep et gitleaks, historique Git compris.
CSP-Report-Only · HSTS · gitleaks · Semgrep- Que se passe-t-il quand vous nous écrivez ?
Le formulaire contrôle l'origine de la requête, exige un jeton signé en HMAC à durée limitée et cache un champ piège. Au-delà de cinq envois par heure, ou de dix par jour depuis la même adresse, le serveur refuse. Ses compteurs sont indexés par une empreinte HMAC de l'IP : l'adresse elle-même n'entre jamais dans ces compteurs. Pas de captcha tiers.
HMAC · honeypot · 5/h · 10/j
Parlons de votre pile
Un logiciel existant à brancher, une application métier à écrire, une scène 3D en tête ou un site en production à auditer ? Décrivez-nous le contexte technique. On vous répond sur la faisabilité, le délai et le coût.