Vierhundertachtzig Kilobyte JavaScript, bevor das erste Pixel steht. Nicht als Zahl aus einem Vortrag, sondern als Zeile in meinem Terminal nach einem Deploy, den ich für harmlos hielt. Ich habe sie dreimal gelesen und dann den Laptop zugeklappt, weil ich wusste, wie der Abend endet.


Der Kollege im Support hatte zeitgleich eine Beschwerde weitergeleitet: „Lädt langsam auf dem Handy". Vierhundertachtzig Kilobyte sind auf einem guten Anschluss ein Wimpernschlag. Auf einem Gerät, das vier Jahre alt ist und nebenbei ein Update herunterlädt, sind sie der Unterschied zwischen einer Seite und einem leeren Rahmen.


Die Treemap war ein Familienfoto

Ich habe den Analyzer installiert und die Map angeschaut. Sie sah aus wie ein Familienfoto, auf dem ich nur entfernte Verwandte erkenne: ein großer Block für eine Datumsbibliothek mit allen Lokalisierungen, ein zweiter für ein Icon-Set, von dem wir vierzehn Symbole benutzen, ein dritter für eine Utility-Sammlung, aus der ich genau zwei Funktionen importiere.


Das ist kein Anfängerfehler. Das ist der Standardzustand eines Projekts, das gewachsen ist, während niemand auf die Waage geschaut hat. Jede einzelne Entscheidung war vernünftig: die Bibliothek spart Arbeit, das Icon-Set ist konsistent, die Utilities sind getestet. Zusammen ergeben sie ein Gewicht, das niemand bestellt hat.


Eine Warnung zum Analyzer: die Zahlen im Bericht sind meist unkomprimiert. 480 Kilobyte gzip sind in der Map gern 1,4 Megabyte, und dann diskutierst du eine Stunde über die falsche Zahl. Miss das, was über die Leitung geht, nicht das, was im Speicher liegt.


Drei Linien, die du ziehen kannst

Die erste Linie ist das Budget. Nicht „wir sollten schneller werden", sondern eine Zahl pro Route, an der ein Build scheitert. Meine liegt bei 190 Kilobyte für die Startseite, weil darunter alles funktioniert, was ich auf dem Bahnsteig brauche.


Symbolbild: KI-generiert für diesen Artikel (Flux).
flowchart TD SH["Shell: Framework, Router 142 kB"] ST["Startseite: sofort sichtbar 48 kB"] ED["Editor, dynamisch 210 kB"] SG["Startseite gesamt 190 kB"] EG["/editor gesamt 400 kB"] SH --> SG ST --> SG SG --> EG ED --> EG

Die zweite Linie ist die Trennung zwischen „immer" und „wenn jemand es anfasst". Ein Editor, den die Hälfte der Besucher nie öffnet, gehört nicht in das Bundle, das alle laden. Dafür reicht ein dynamischer Import an der Stelle, an der der Klick passiert — kein Router-Umbau, kein zweites Projekt.


// Vorher: im Hauptbundle, fuer alle, auch fuer die, die nie editieren.
import { Editor } from "./editor";

// Nachher: der Editor kommt, wenn ihn jemand anfasst.
const { Editor } = await import("./editor");

Die dritte Linie ist unangenehmer, weil sie Arbeit bedeutet: die Abhängigkeiten selbst. Ein Icon-Set pro Symbol importieren statt als Ganzes. Prüfen, ob eine Bibliothek überhaupt tree-shakebar ist — wenn sie CommonJS exportiert, kannst du sie nicht schütteln, du kannst sie nur ersetzen. Und die Datumsbibliothek, deren Lokalisierungen wir nie ausliefern: entweder gezielt laden oder durch die Plattform ersetzen, die das seit Jahren kann.


Der Build, der das Budget prüft

Ein Budget, das nur in einem Dokument steht, ist ein Wunsch. Meines steht in der CI: ein Schritt, der die gebauten Dateien misst, gegen die Grenze pro Route vergleicht und den Pull Request rot macht, wenn die Startseite über 190 Kilobyte gzip kommt. Kein Vortrag, keine Folie, ein Exit-Code.


Ich habe den Schritt eingebaut, weil ich es satt hatte, in Reviews über Kilobyte zu diskutieren, die niemand nachgemessen hatte. Seitdem diskutieren wir über die Zahl, nicht über das Gefühl. Und weil der Schritt im Build läuft, fällt auf, wenn eine Abhängigkeit hereinkommt, die samt ihrer eigenen Abhängigkeiten mehr wiegt als die Funktion, für die sie da ist. Das passiert öfter, als du denkst, und nie in dem Pull Request, in dem du hinschaust.


Das Skelett ist keine Ladezeit

Und dann kam der Vorschlag aus dem Design: „Wir machen einfach ein Skelett." Skelette sind Ladebalken mit Selbstbewusstsein. Sie machen Warten erträglicher, sie machen es nicht kürzer. Wenn ein Skelett viermal aufblitzt, weil vier Requests nacheinander kommen, hast du nicht Performance verbessert, sondern einen Flackerteppich gelegt.


Was ein Skelett tatsächlich leisten kann, ist Layout-Stabilität: gleiche Höhe wie der fertige Inhalt, damit nichts springt, wenn die Daten einschlagen. Dazu aria-busy="true" auf dem Container und ein Text, der auch ohne Augen funktioniert — nicht nur graue Flächen. Und bei prefers-reduced-motion hört das Shimmern auf, weil ein pulsierender Gradient für manche Menschen kein Ladehinweis ist, sondern ein Problem.


Symbolbild: KI-generiert für diesen Artikel (Flux).

Mein Test dafür ist brutal einfach: Netzwerk auf „langsam" drosseln, Seite neu laden, und schauen, ob sich das Skelett vom fertigen Layout unterscheidet. Wenn ja, hast du ein zweites Layout gebaut, das niemand pflegt.


Fazit

Der Preis, es zu ignorieren, ist eine Zielgruppe mit älteren Geräten, die deine Seite verlässt, bevor dein erstes Pixel existiert. Der Preis, es zu reparieren, sind ein Analyzer, ein Budget im Build und zwei dynamische Importe — ein Nachmittag, und danach steigt die Zahl nicht wieder von selbst.


Miss zuerst. Nicht schätzen, nicht „fühlt sich okay an". Vierhundertachtzig Kilobyte fühlen sich auf meinem Laptop okay an. Das ist das Problem.


Wie viele Kilobyte lädt dein letzter Build vor dem ersten Pixel, und wer im Team hat diese Zahl je gemessen?