Ein Partikelkino ohne Abhängigkeiten
Zwanzig Jahre lang habe ich Arbeit gebaut, die niemand sieht. Dies ist die sichtbare Sorte, mit derselben Disziplin auseinandergenommen.
Infrastrukturarbeit hat eine seltsame Ästhetik: gut gemacht, bemerkt sie niemand. Ein Backup läuft zwanzig Jahre still, dann eines Tages nicht mehr, und an diesem Tag erfährt jeder, dass es existiert. Das ist das Maß unsichtbarer Arbeit — sie wird nur beurteilt, wenn sie ausfällt.
Diese Website ist der Versuch, diese Gewohnheit an die Oberfläche zu holen. Was folgt, ist keine Dekoration, sondern das auseinandergenommene Ding. Was da ist, warum es da ist, und was es kostet.
Die Einschränkung kommt zuerst
Eine einzige Regel entschied alles: keine externen Anfragen.
Das ist kein Slogan, das ist ein Response-Header. Die Sicherheitsrichtlinie der Seite lautet:
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
In dem Moment, in dem Sie default-src 'self' schreiben, schließen sich Ihre Optionen. Kein GSAP, kein three.js, kein Lenis, nicht einmal eine einzige Schrift aus einem CDN. Wollen Sie eine Animationsbibliothek, werden Sie sie schreiben. Wollen Sie Scroll-Glättung, werden Sie sie schreiben. Wollen Sie eine Partikel-Engine — ja, auch die.
Die Regel bewies sich einmal unerwartet selbst. Cloudflare injiziert sein eigenes Analyseskript am Edge. Die Richtlinie der Seite blockierte es:
Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/…'
violates the following Content Security Policy directive: "script-src 'self'"
Auf einem lokalen Server gibt Lighthouse für Best Practices 100; live gibt es 93. Diese sieben Punkte sind genau das. Die Richtlinie zu lockern wäre einfach gewesen. Stattdessen wurde das Beacon abgeschaltet — denn eine Regel bedeutet erst dann etwas, wenn sie auch im unbequemen Moment gilt.
Wo die Partikel leben
Die gesamte Bewegung auf der Seite wird auf einem einzigen <canvas> mit WebGL2 gezeichnet. Positionen und Geschwindigkeiten der Partikel liegen nicht auf der CPU; sie leben in zwei Texturen, und in jedem Frame bewegt ein Fragment-Shader sie weiter. Das Ergebnis wird in eine Textur geschrieben, und im nächsten Frame wird diese Textur zur Eingabe. Der älteste Trick in GPGPU: die Rechnung von Textur zu Textur, nie auf den Bildschirm.
Praktisch heißt das: Die Partikelzahl kümmert die CPU nicht. Auf dem Desktop läuft das Feld in voller Dichte, auf dem Telefon sinkt es auf 12.100 Partikel — aber in beiden Fällen ist die Arbeit pro Frame derselbe Shader-Aufruf.
Zwei Texturen zu halten ist ein kleines, aber tragendes Detail. Man kann dieselbe Textur nicht in einem Durchgang lesen und beschreiben; das ist undefiniertes Verhalten. Also wechseln sich zwei Puffer ab: dieser Frame liest A und schreibt B, im nächsten tauschen die Rollen. Dieses Ping-Pong ist die Grundhygiene von GPGPU.
Farbe ist eine eigene Rechnung. Die Szene wird in HDR gezeichnet, helle Bereiche werden extrahiert und weichgezeichnet (selektives Bloom), dann bringt eine ACES-Kurve alles zurück in den darstellbaren Bereich. Diese drei Schritte verhindern den leichtesten Fehler beim additiven Zeichnen: dass alles zu Weiß ausbrennt. Die Verifikationssuite misst genau das — die Spitzen-Luma pro Frame muss unter dem Schwellenwert bleiben.
Geräteunterschiede regelt eine vierstufige Leiter:
| Stufe | Was abgeschaltet wird |
|---|---|
| 0 | Nichts — volles Kino |
| 1 | Kein Zoom-Blur, Bloom auf zwei Durchgänge |
| 2 | Partikelzahl fällt auf 55% |
| 3 | WebGL1-Fallback — GPGPU ganz aus |
Stufe 3 ist wichtig: In einem Browser ohne WebGL2 hört die Seite nicht auf zu funktionieren, sie verliert nur die Simulationsschicht. Text, Navigation, Kontakt — alles bleibt.
Acht Sprachen, eine Quelle der Wahrheit
Die Seite erscheint in acht Sprachen: Türkisch an der Wurzel, die anderen sieben in eigenen Verzeichnissen. Aber acht Seiten werden nicht von Hand geschrieben.
Die türkische Seite ist der Master. Jeder übersetzbare Knoten trägt eine Markierung:
<span data-i="about.e1t">Kurumsal altyapı</span>
Ein Generator liest diese Markierungen und extrahiert i18n/tr.json — derzeit 102 Schlüssel. Die anderen sieben Sprachen füllen dieselben Schlüssel mit eigenem Text, und der Generator druckt die Seiten. Die übersetzbaren Felder des JSON-LD laufen durch dieselbe Pipeline: Berufsbezeichnung, Ort, Fachgebiete, Dienstbeschreibung.
Die härteste Regel dieses Aufbaus: ein fehlender Schlüssel stoppt den Build. Kein Fallback, kein Ausweichen auf Englisch. Hinkt eine Sprache hinterher, geht nichts live. Besser anhalten, als eine still halb übersetzte Seite zu veröffentlichen.
Dass die türkische Seite nicht generiert wird, ist ebenfalls Absicht. Der Master wird von Hand geschrieben, weil er Messkommentare, Maskenränder und optische Ausrichtungsnotizen trägt — verschiebt man die in einen Generator, werden sie unlesbar.
Das Ergebnis ist messbar: Über die acht Seiten hinweg sind die hreflang-Verweise in 64 von 64 Richtungen wechselseitig, x-default ist auf allen acht korrekt.
Schriften sind der schwerste Teil
Die ganze Seite umfasst 172 KB über elf Ressourcen. Die Aufteilung überrascht:
| Posten | Gewicht |
|---|---|
| Schriften (woff2) | 116 KB |
| JavaScript | 56 KB |
| Bilder | 0 KB |
| CSS | 0 KB — inline |
Es gibt kein einziges <img> auf der Seite. Die gesamte visuelle Last liegt auf Canvas und SVG. Dagegen stehen fünfzehn woff2-Dateien: vier Familien für Latein, eigene Ersatzschriften für Kyrillisch. Die kyrillischen Dateien hängen über unicode-range an denselben Familiennamen — die russische Seite lädt also nie eine lateinische Datei; der Browser wählt selbst.
Die @font-face-Deklarationen sind ins HTML eingebettet. Das ist eine gemessene Entscheidung: Ein separates Stylesheet war die einzige renderblockierende Ressource, und das Einbetten hob den mobilen Lighthouse-Wert von 99 auf 100.
Echter Transfer nach Kompression:
| Ressource | Roh | Brotli | Verhältnis |
|---|---|---|---|
| HTML | 87.890 B | 20.675 B | 76,5% |
app.js | 186.011 B | 55.354 B | 70,2% |
Ohne Messung heißt nichts „fertig"
Das Repository hält zwei Verifikationssuiten: 72 Tests für Szenenphysik, 100 für Übersetzung und die Discovery-Schicht. Beide laufen in einem echten Browser, und keine schaut auf einen Screenshot — sie lesen Luma vom Canvas, fragen Zustandskanäle ab und prüfen Schwellen numerisch.
Der Grund ist einfach: Visuelle Verifikation lügt. Ein Panel kann von selbst scrollen, ein Tab in den Hintergrund fallen, requestAnimationFrame nie getickt haben. Der Screenshot sagt „funktioniert", während nichts begonnen hat.
Wie Messung lügt
Das Zeitaufwendigste hier war nicht der Code. Es war zu lernen, der Messung selbst nicht zu trauen.
Ein Beispiel. Eine Seite öffnete sich in einem Automatisierungsbrowser, und der Inhalt blieb siebzig Sekunden unsichtbar. Schwarzer Bildschirm, der Eröffnungsvorhang eingefroren. Die naheliegende Lesart: Die Seite hängt.
Drei Dinge wurden gemessen, bevor dieser Schluss gezogen wurde:
document.visibilityState → "hidden"
rAF-Frames in 2 Sekunden → 0
Eröffnungsfortschritt → 0
Der Tab war im Hintergrund. requestAnimationFrame tickt in Hintergrund-Tabs nicht; die Sequenz hatte nie begonnen. Auf der Seite gab es keinen Fehler — der Fehler lag in der Messumgebung.
Das war einen Schritt davon entfernt, als „Befund" in einen Bericht einzugehen. Das Repository hat dafür eine geschriebene Regel: nichts ohne Messung „fertig" nennen — und auch messen, was man gesehen zu haben glaubt. Ein Screenshot ist kein Beweis; Beweis ist die vom Canvas gelesene Luma oder der Zustandskanal selbst.
Zwei stille Fehler
Das Interessanteste, das ein Audit zutage fördert, ist das, wonach es nicht gesucht hat.
Erstens: Das Feld lastmod in der Sitemap las sein Datum aus der vorherigen Sitemap. Der Generator nahm seine eigene Ausgabe als Eingabe. Einmal geschrieben, würde der Wert nie vorrücken — wie sehr sich der Inhalt auch änderte, die Datei zeigte für immer denselben Tag.
Ein eingefrorenes Frische-Signal wird zur Lüge. Ein veraltetes lastmod ist schlimmer als keines: Es sagt einem Crawler, diese Seite ändere sich nicht. Das Datum lebt jetzt an genau einer Stelle, und die Sitemap folgt ihm.
Zweitens: Die Liste der übersetzbaren JSON-LD-Felder war im Generator an zwei Stellen dupliziert — eine fürs Extrahieren, eine fürs Schreiben. Was passiert, wenn ein neues Feld nur zu einer hinzugefügt wird? Das Feld wird still nicht übersetzt. Türkischer Text wird wörtlich in die strukturierten Daten von sieben Sprachseiten kopiert, und keiner der hundert Tests bemerkt es.
Das war eine Falle, die genau das gebrochen hätte, was sie beheben sollte: Wer das fehlende description-Feld ergänzt, hätte unbemerkt Türkisch in sieben Sprachen geschrieben. Die Liste ist jetzt eine einzige Quelle.
Die gemeinsame Lehre: die gefährlichsten Fehler eines Systems sind die, bei denen kein Test fehlschlägt.
Der abgelehnte Trick
Die ehrlichste Zeile dieses Projekts steht nicht in einem Fehlerbericht. Sie steht in einem Entscheidungsprotokoll.
Auf Mobilgeräten kann Lighthouse für die Startseite keinen Largest-Contentful-Paint-Wert erzeugen. Ursache ist die Eröffnungssequenz: vier Sekunden Vorhang, dann das Einfrieren beim Kontakt, dann der Ausbruch, dann der Tintenübergang. Vorher ist kein DOM-Text sichtbar, und Lighthouse schließt seine Spur viel früher. Ergebnis: NO_LCP — die Metrik ist nicht berechenbar.
Eine Korrektur wurde gemessen. Setzt man die Anfangsopazität der Inhaltsschicht auf 0.004 statt 0, fällt LCP von 9.052 ms auf 164 ms, und die volle Hero-Überschrift wird als LCP-Kandidat gestempelt. Es gibt keine visuellen Kosten: 0,004 Opazität trägt höchstens 1/255 einer Einheit bei, also nichts.
Genau deshalb wurde er abgelehnt.
LCP ist definiert als der größte im Viewport sichtbare Textblock. Eine Überschrift mit 0,004 Opazität ist nicht sichtbar. Chromiums opacity: 0-Ausnahme existiert, um genau dieses Muster zu blockieren; 0,004 schlüpft an ihrer Kante vorbei. Die Zahl würde besser, die Wahrheit nicht: Der Besucher sähe die Überschrift weiterhin neun Sekunden später.
Die Entscheidung ging so in die Dokumentation ein: dieses Muster nicht zurückbringen.
Warum diese Seite anders ist
Die Seite, die Sie lesen, lädt die oben beschriebene Engine nicht. Das Partikelfeld dahinter ist ein separates, sehr kleines Skript; der Text ist beim ersten Paint vollständig sichtbar und wartet hinter keinem opacity-Gatter.
Der Grund ist der Abschnitt darüber. Ein Text existiert, um gelesen und zitiert zu werden. Die kinematische Eröffnung ist die Signatur der Startseite, aber auf einer Textseite würde dieselbe Sequenz nicht nur die Messung verzögern, sondern das Lesen. Für diesen Bereich gibt es ein geschriebenes Budget — LCP unter 2,5 Sekunden, Seiten-JavaScript unter 60 KB, kein Opazitätsgatter auf dem Inhalt — und die Verifikationssuite prüft es bei jedem Lauf.
Dieselbe Disziplin, anderes Ergebnis. Wenn die Ästhetik unsichtbarer Arbeit darin besteht, dass sie nie ausfällt, dann besteht die Ästhetik sichtbarer Arbeit darin, dass sie messbar ist.