Warum Mikro-Optimierungen nicht ausreichen, um die tatsächliche Leistung von JavaScript zu verbessern
Erklärt, warum das Verfolgen messbarer Kennzahlen wie der Bundle-Größe und erneuter Renderungen oft die wahren Ursachen für eine langsame Benutzererfahrung in JavaScript-Apps übersehen lässt.
Der Pull Request sah auf dem Papier solide aus.
Die Größe des Bundles hatte um 18 Prozent abgenommen. Einige ungenutzte Abhängigkeiten waren entfernt worden. Mehrere Komponenten waren in eine Memoisierungslogik eingebettet. Einige Array-Operationen wurden durch angeblich effizientere Schleifen ersetzt. Die Lighthouse-Bewertung hatte zugenommen. Jede Kennzahl in der Beschreibung des PRs wies in die richtige Richtung.
Das Team genehmigte ihn ohne Zögern.
Zwei Wochen später sagten die Nutzer immer noch, die App sei träge.
Nicht die Art von Trägheit, bei der „der Benchmark ein paar zusätzliche Millisekunden anzeigt“.
Nicht ein Problem, das sich nur in Zahlen auf einem Dashboard zeigt.
Sondern die Art von Langsamkeit, die tatsächlich Menschen beeinträchtigt.
Sie tippten auf einen Knopf und waren sich nicht sicher, ob er registriert wurde.
Sie öffneten ein Bildschirmfenster und warteten darauf, dass etwas Nützliches angezeigt wurde.
Sie wechselten einen Filter und sahen zu, wie die Benutzeroberfläche kurz einfroren, bevor sie wieder reagierte.
Das Team hatte echte Anstrengungen in die Leistungsverbesserung investiert.
Aber sie richteten diese Bemühungen auf die falschen Ziele.
Dieses Muster tritt heute ständig in JavaScript-Projekten auf.
Trotz besserer Profiler, schnellerer Laufzeiten, intelligenterer Bundler, leistungsfähigerer Frameworks sowie immer mächtigerer Browser fallen Entwickler weiterhin in denselben Fehler:
Die Optimierung dessen, was am leichtesten messbar ist, anstatt dessen, was die Nutzer tatsächlich wahrnehmen.
JavaScript bietet zufälligerweise eine endlose Liste von Dingen, die man optimieren könnte.
Ein Komponent wird viertausendmal neu gerendert – also behebt man das Problem.
Ein Bundle wiegt 300 KB – also versucht man, es zu verkleinern.
Eine Funktion benötigt 12 Millisekunden – also schreibt man sie um.
Eine Abhängigkeit verbraucht 40 KB – also entfernt man sie.
Ein Mikrobenchmark zeigt, dass Ansatz A Ansatz B um 7 Prozent übertrifft.
Daher wählt man A.
Jedes davon kann eine legitime Lösung sein.
Aber keines davon führt automatisch zu einem besseren Erlebnis für die Person, die Ihre App verwendet.
Mannchmal löst der schnellste Code, den man schreibt, ein Problem, das niemanden bisher belastet hat.
Gleichzeitig kosten langsame Datenbankabfragen, überflüssige Netzwerkaufrufe, ungeschickte Ladeabläufe, überdimensionierte API-Datenmengen oder schlecht gestaltete Interaktionen den Nutzern still und heimlich wertvolle Zeit.
Dieser Unterschied ist das eigentliche Problem, das es zu untersuchen gilt.
Leistung ist nicht dasselbe wie Geschwindigkeit
Eine der nützlichsten Erkenntnisse aus der Arbeit an Produktivsystemen ist, dass Leistung nicht auf eine einzige Kennzahl reduziert werden kann.
Eine Anwendung kann bis ins Mikrosekundenbereich rechnerisch effizient sein und dennoch sehr schwer zu bedienen sein.
Stellen Sie sich eine Seite vor, die diesen Ablauf durchläuft:
- Lade die App-Shell.
- Lade mehrere JavaScript-Bausteine herunter.
- Starte das Framework.
- Lade Konfigurationsdaten herunter.
- Lade den aktuellen Benutzer herunter.
- Lade Berechtigungsdaten herunter.
- Lade den Inhalt des Dashboards herunter.
- Gestalte das Dashboard dar.
- Lade Benachrichtigungen herunter.
- Gestalte die Benachrichtigungen dar.
Jeder einzelne Schritt kann für sich genommen äußerst schnell sein.
Das eigentliche Problem liegt in der gesamten Abfolge der Schritte.
Es ist den Benutzern egal, ob jede Komponente einzeln gut optimiert wurde.
Ihnen ist wichtig, dass sie zunächst auf einem leeren Bildschirm sitzen, bevor etwas erscheint.
Deshalb sollte jede echte Leistungsuntersuchung mit einer Frage beginnen:
Worauf wartet die Person am anderen Ende eigentlich?
Nicht:
Wie kann ich Zeit in dieser Funktion sparen?
Es handelt sich dabei um grundlegend unterschiedliche Ansätze.
Ein Entwickler könnte drei Stunden damit verbringen, eine Funktion von 8 ms auf 3 ms zu optimieren.
In der Zwischenzeit bleibt die Seite 700 ms lang untätig und wartet auf eine Anfrage, die eigentlich gar nicht notwendig war.
Das ist keine echte Optimierung.
Es ist, als würde man das Mobiliar aufräumen, während in der Wand dahinter ein Rohr platzt.
Die Besessenheit von 5 Millisekunden
JavaScript-Entwickler haben eine Schwäche für Mikrooptimierungen.
Ein Teil dieser Neugierde ist tatsächlich lohnenswert.
Menschen streiten über for-Schleifen im Vergleich zu map().
Sie diskutieren Strategien zur Speicherallokation.
Sie erforschen versteckte Klassen, das Verhalten der Müllsammlung, Closures, Kosten bei Dekonstruktion, Overhead bei Funktionsaufrufen sowie JIT-Optimierungen.
Hinter all dem steckt wertvolles, echtes Wissen.
Das Problem liegt nicht darin, diese Mechanismen zu verstehen.
Das Problem ist, danach zu greifen, ohne jeglichen Beweis dafür, dass sie hier relevant sind.
Nehmen wir an, eine Funktion wird während einer einzigen Benutzerinteraktion 100 Mal ausgeführt.
Derzeit dauert jede Aufruf 2 ms.
Man verbringt einen halben Tag damit, die Laufzeit auf 1 ms zu reduzieren.
Gesamteinsparung: 100 Millisekunden.
Das könnte etwas wert sein.
Aber nehmen wir an, dieselbe Interaktion löst außerdem einen unnötigen API-Aufruf aus, der 600 ms zur Beendigung benötigt.
Den Entfernen dieses Aufrufs würde fünf Minuten dauern und mehr Latenz einsparen als der ganze Nachmittag des Optimierens zusammen.
Einfach ausgedrückt, das scheint offensichtlich zu sein.
Doch Code-Review-Gespräche konzentrieren sich auf die erste Art von Problem, weil es direkt im Diff sichtbar ist.
Der unnötige Netzwerkaufruf hingegen könnte drei Ebenen der Abstraktion tiefer versteckt sein.
Dies führt zu einem vorhersehbaren und gefährlichen Bias:
Die Teams optimieren letztendlich den Code, der ihnen sichtbar ist, und nicht das System, das ihre Nutzer tatsächlich nutzen.
Der Browser ist nicht Ihre Funktion
Ein weiterer häufiger Fehler besteht darin, anzunehmen, dass die Ausführungszeit von JavaScript die gesamte Leistungsbilanz ausmacht.
Das ist bei weitem nicht der Fall.
Eine browserbasierte Anwendung ist ein ganzes System, nicht nur eine einzelne Funktionsaufrufung.
Dieses System umfasst:
- DNS-Auflösung
- Verbindungsherstellung
- TLS-Handshake
- Serverseitige Verarbeitung
- Datenbankabfragen
- Serialisierung der API-Antworten
- Übertragungszeit im Netzwerk
- HTML-Parserung
- JavaScript-Parserung
- Ausführung von JavaScript
- Bildschirmdarstellung
- Berechnung des Layouts
- Ausmalen des Bildschirms
- Komposition der Bilder
Jede dieser Phasen kann beeinflussen, wie schnell etwas erscheint.
Nehmen wir an, es gelingt Ihnen, die Renderzeit eines React-Komponenten von 15 ms auf 8 ms zu verkürzen.
Das ist wirklich gute Arbeit.
Aber wenn der Backend-Server 900 ms braucht, um die Daten zu liefern, die die Komponente benötigt, ist Ihre Verbesserung kaum wahrnehmbar.
Oder vielleicht antwortet der Server schnell, sendet aber 2 MB JSON für eine Ansicht, die nur 20 KB benötigt.
Dann verbrauchen Sie CPU-Zyklen und Bandbreite, um Daten zu übertragen, die in dieser Form überhaupt nicht vorhanden sein sollten.
Oder vielleicht lädt die Seite eine riesige Client-Seiten-Bibliothek herunter, bevor sie irgendetwas Sinnvolles darstellen kann.
Niemandes davon sind Probleme der Komponenten selbst.
Sie sind architektonische Probleme.
Deshalb sieht ernsthafte Leistungsarbeit oft weniger nach „das JavaScript schneller machen“ aus und eher nach Ermittlungsarbeit.
Man verfolgt die Verzögerung bis zu ihrer Ursache, wohin auch immer sie führt.
Die teuerste Operation ist oft die, die man überspringen sollte
Es gibt eine nützliche Prioritätenreihenfolge, wenn man über Leistungsverbesserungen nachdenkt.
Die Beschleunigung einer Operation ist ein gutes Ergebnis.
Sie ganz wegzulassen ist in der Regel noch besser.
Betrachten Sie diesen Auszug:
const results = expensiveTransform(items);
Das Profilieren zeigt, dass diese Transformation 40 ms in Anspruch nimmt.
Ihr Instinkt könnte sein, sie zu optimieren.
Möglicherweise fügen Sie einen Cache ein.
Möglicherweise ersetzen Sie sie durch einen intelligenteren Algorithmus.
Möglicherweise verlagern Sie die Aufgabe ganz woanders hin.
Aber bevor Sie irgendetwas davon tun, fragen Sie sich:
Warum findet diese Transformation überhaupt statt?
Möglicherweise ändert sich die Ausgabe erst, wenn ein Filter angepasst wird.
Möglicherweise führen Sie es bei jeder Darstellung erneut aus, unabhängig von allem anderen.
Möglicherweise könnte die Backend-Schicht Ihnen die bereits verarbeitete Version liefern.
Möglicherweise benötigt die Benutzeroberfläche tatsächlich nicht alle 10.000 Zeilen.
Möglicherweise holen Sie Daten ab, die der Benutzer niemals tatsächlich öffnen wird.
Die eigentliche Lösung liegt vielleicht gar nicht in expensiveTransform().
Es könnte genügen, den Aufruf einfach wegzulassen.
Dieser Denkansatz lässt sich gut auf viele weitere Fälle übertragen.
Überspringen Sie Anfragen, die Sie nicht senden müssen.
Überspringen Sie die Darstellung von Benutzeroberflächen, die weiterhin versteckt bleiben.
Überspringen Sie die Berechnung von Werten, die niemand verwenden wird.
Überspringen Sie Codepfade, die Benutzer niemals auslösen werden.
Überspringen Sie die Verarbeitung von Daten, die Sie bereits vorher filtern konnten.
Überspringen Sie die Wiederholung von Arbeiten, deren Ergebnis sich tatsächlich nicht geändert hat.
Die schnellste mögliche Operation ist weiterhin die, die überhaupt nicht ausgeführt wird.
Die Größe des Bundles ist nicht das ganze Bild
Der Umfang des Bundles verdient Aufmerksamkeit. Das stimmt schon.
Aber da es zu einer so weit verbreiteten Messgröße geworden ist, beginnen Teams manchmal, es als Ersatz für die Leistung selbst zu betrachten.
Ein Team kürzt 50 KB aus seinem JavaScript-Bundle und hält das für einen Erfolg.
Gleichzeitig sendet die App weiterhin sechs Anfragen nacheinander, bevor der Benutzer etwas damit tun kann.
Sicher, das Bundle ist kleiner geworden.
Das bedeutet aber nicht automatisch, dass die Benutzererfahrung verbessert wurde.
Nichts davon deutet darauf hin, dass die Größe des Bundles unwichtig ist.
Das bedeutet vielmehr, dass man herausfinden muss, wann dieser spezielle JavaScript-Code tatsächlich verwendet wird.
Ein 100 KB großer Script, der die anfängliche Interaktivität blockiert, kann weitaus wichtiger sein als ein 300 KB großer Script, das später geladen wird – für eine Funktion, die der Benutzer vielleicht einmal im Monat nutzt.
Die Zeit spielt eine Rolle.
Auch wann der Code ausgeführt wird, ist wichtig.
Es zählt, auf welchem Gerät es läuft.
Es zählt, über welches Netzwerk es sich bewegt.
Es zählt, ob Inhalte im Cache gespeichert sind.
Und genauso wichtig ist, was der Benutzer tatsächlich versucht zu tun.
Falls eine bestimmte Funktion nur selten genutzt wird, kann das Vorausladen aller benötigten Ressourcen beim Start ein schlechter Kompromiss sein.
Code Splitting hilft dabei.
Ebenso das Lazy Loading.
Aber beides kann zu einem sinnlosen Ritual werden, wenn man es anwendet, ohne wirklich zu verstehen, wie die Seite in der Praxis lädt.
Die richtige Frage lautet nicht:
Wie können wir dieses Paket noch weiter verkleinern?
Sondern:
Was braucht dieser spezielle Benutzer gerade, und wie schnell können wir genau diese Sache nutzbar machen?
Diese Herangehensweise bringt einen viel weiter.
Das Komponente, auf das Sie starren, ist vielleicht nicht schuld
Jeder, der mit React gearbeitet hat, kennt diesen Zyklus.
Eine Komponente wird öfter neu gerendert, als nötig wäre.
Jemand greift zu useMemo.
Ein zusätzlicher Render verschwindet.
Alle sind zufrieden.
Dann erhält die nächste Komponente dieselbe Behandlung.
Bald schon ist der Code voller Memoisierungsaufrufe:
const filtered = useMemo(
() => expensiveFilter(items, query),
[items, query]
);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
const value = useMemo(
() => ({ user, permissions }),
[user, permissions]
);
Manchmal ist das tatsächlich der richtige Schritt.
Andernfalls wird der Code nur schwerer verständlich, ohne im Grunde etwas zu lösen.
Optimierungen sind nicht kostenlos.
Memoisierung hat insbesondere echte Kosten.
Sie verursacht Speichernutzung, Abhängigkeitsarrays, die gepflegt werden müssen, zusätzliche kognitive Belastung sowie neue Orte, an denen sich feine Fehler verstecken können.
Messen Sie vorher, bevor Sie dazu greifen.
Falls eine Berechnung 0,2 ms kostet und nur selten ausgeführt wird, bringt ihre Optimierung nichts.
Falls sie 50 ms kostet und bei jeder Tastenbetätigung abläuft, ist das eine völlig andere Situation.
Es geht nicht darum, jede erneute Darstellung zu verfolgen.
Es geht darum, sicherzustellen, dass die wichtigen Interaktionen schnell genug erscheinen.
Diese beiden Ziele sind nicht identisch.
Konzentrieren Sie sich auf Interaktionen, nicht auf einzelne Komponenten
Hier könnten viele Teams von einer Denkweiseänderung profitieren.
Benutzer wahrnehmen Komponenten nicht als getrennte Einheiten.
Sie erleben das, was sie tun.
Sie tippen eine Suchanfrage ein.
Sie füllen Felder aus.
Sie wechseln zwischen Seiten.
Sie senden ein Formular ab.
Sie öffnen ein Dropdown-Menü.
Sie wechseln zwischen Tabellenblättern.
Sie laden eine Datei hoch.
Sie scrollen durch einen Feed.
Sie warten einfach darauf, dass etwas erscheint.
Anstatt zu fragen:
Ist dieses Komponente gut optimiert?
Probieren Sie es mit folgender Frage:
Fühlt sich das Eingeben in dieses Suchfeld sofortig an?
Anstelle von:
Wird diese Liste effizient dargestellt?
Fragen Sie lieber:
Kann jemand diese Liste flüssig scrollen, ohne dass die Benutzeroberfläche ruckelt?
Anstelle von:
Haben wir die Anzahl der React-Renderungen reduziert?
Fragen Sie stattdessen:
Gibt das Drücken dieser Schaltfläche dem Benutzer sofortige, sinnvolle Rückmeldung?
Diese zweite Gruppe von Fragen bezieht sich viel genauer auf das, was wirklich für das Produkt wichtig ist.
Eine kluge Optimierung, bei der eine wichtige Interaktion genauso langsam bleibt wie zuvor, ist nicht unbedingt wertvolle Ingenieursarbeit – egal wie elegant sie in einem Diff aussieht.
Die Netzwerkansicht ist in der Regel besser als die Schleife, die man neu schreibt
Bevor Sie auch nur eine einzige Schleife anfassen, öffnen Sie das Netzwerkpanel Ihres Browsers.
Das ist kein oberflächlicher Ratschlag.
Oft finden Sie dort mehr Verbesserungsmöglichkeiten als in Hunderten von Zeilen handoptimiertem JavaScript.
Achten Sie auf Dinge wie:
- dieselbe Anfrage wird mehrmals abgesendet
- Anfragen werden nacheinander ausgeführt, obwohl sie parallel laufen könnten
- Anfragen werden gestartet, bevor ihre Daten tatsächlich benötigt werden
- Antworten sind weitaus umfangreicher, als angezeigt wird
- Caching, das vorhanden sein sollte, ist es aber nicht
- Polling findet häufiger statt, als notwendig
Eine einzige überflüssige Anfrage kann die Auswirkungen von Dutzenden kleiner JavaScript-Anpassungen überwiegen.
Nehmen wir die Suche als Beispiel.
Hier ist ein naiver Ansatz:
User types "j"
→ request
User types "ja"
→ requestUser types "jav"
→ requestUser types "java"
→ request
Ein Team könnte dann echte Anstrengungen unternehmen, um die Darstellung der Ergebnisse zu beschleunigen.
Doch das eigentliche Engpass könnte darin liegen, dass die Anwendung vier separate Anfragen sendet, obwohl eine einzige ausreichen würde.
Durch Hinzufügen von Verzögerungsfunktionen, Anfragenstopp, Caching sowie besser gestalteten Abfragen erzielt man in der Regel einen weitaus größeren Erfolg.
Das ist die Art von Verbesserung, die auf Systemebene stattfindet – nicht innerhalb einer einzelnen Funktion.
Optimieren Sie nicht das falsche Gerät
Das Durchführen eines Benchmarks auf Ihrem eigenen Entwicklungsrechner sagt Ihnen fast nichts darüber, wie sich eine App tatsächlich auf einem alten Telefon mit eingeschränkter CPU und instabiler Verbindung anfühlt.
Das ist für JavaScript von großer Bedeutung.
Modernes Hardware kann eine erstaunliche Menge an Code verarbeiten, ohne dass der Entwickler eine Verlangsamung bemerkt.
Ein leistungsstarker Laptop kann Probleme verbergen.
Ein Flagship-Telefon kann Probleme verbergen.
Eine schnelle Büronetzwerkverbindung kann Probleme verbergen.
Arbeiten lokal kann Probleme verbergen.
Dann öffnet eine echte Person die App auf einem günstigen Gerät mit schwacher Verbindung.
Auf einmal wirkt die sorgfältig abgestimmte App langsam und unreaktiv.
Deshalb ist es so wichtig, unter realistischen Bedingungen zu testen.
Ihre Aufgabe ist es nicht, dies ständig zu tun.
Aber Sie müssen es oft genug tun, damit das Team ein echtes Verständnis dafür entwickelt, wie sich die App außerhalb einer Entwicklungsumgebung verhält.
Die richtige Frage lautet nicht:
Fühlt sich das auf meiner Konfiguration schnell an?
Sondern:
Ist das schnell genug für die tatsächlichen Nutzer?
Die Architektur ist in der Regel besser als Mikro-Optimierungen
Das ist wohl die wichtigste Lektion überhaupt.
Die Architektur bestimmt die Leistungsgrenze.
Falls Ihre App fünf API-Aufrufe nacheinander ausführen muss, bevor sie ihren Hauptbildschirm anzeigen kann, wird kein cleveres Umgang mit Arrays diese Problematik lösen.
Falls jede Route den gesamten Anwendungs-Paket herunterlädt, wird das Weglassen einiger Hilfsfunktionen das eigentliche Problem nicht beeinflussen.
Falls der Client ein riesiges Datensatzset herunterlädt und es anschließend im Browser filtert, lohnt es sich vermutlich weniger, die Filterlogik zu optimieren, als vielmehr den API-Vertrag selbst zu reparieren.
Falls eine Benutzeraktion einen großen Teil des gespeicherten Zustands löscht, wird das Einhüllen von Komponenten in Mechanismen zur Merkhilfe eine fehlerhafte Validierungsstrategie nicht beheben.
Falls Sie gleichzeitig Tausende von DOM-Node rendern, wird ein Wechsel in der Verwendung von map() Ihnen nicht helfen.
Die Verbesserungen, die tatsächlich eine Wirkung zeigen, beinhalten in der Regel eine Veränderung davon, wo die Arbeit erledigt wird, wann sie erledigt wird oder ob sie überhaupt notwendig ist.
Das sind architektonische Entscheidungen, keine Anpassungen auf Codeebene.
Was ich bei einer Leistungsuntersuchung tatsächlich betrachte
Wenn etwas langsam erscheint, sollte man dem Impuls widerstehen, direkt in den Code einzutauchen.
Fangen Sie damit an, das Problem nachzustellen.
Fragen Sie anschließend genau, worauf der Benutzer dort wartet.
Von da an folgt die Untersuchung in der Regel einer bestimmten Reihenfolge.
1. Laden
Was muss zuerst geschehen, damit der Benutzer die wichtigen Teile der Seite tatsächlich sehen und nutzen kann?
Verfolgen Sie den kritischen Pfad.
Welche Ressourcen sind wirklich erforderlich?
Welche Anfragen behindern den Fortschritt?
Was wird heruntergeladen, obwohl es nicht nötig ist?
2. Netzwerk
Öffnen Sie das Netzwerkkontrollfeld.
Achten Sie auf Muster im Ablauf der Anfragen.
Ketten von sequenziellen Anfragen erfordern besondere Aufmerksamkeit.
Ebenso duplizierte Aufrufe sowie Payloads, die größer sind als nötig.
3. Darstellung
Schauen Sie sich anschließend an, was der Browser selbst tut.
Lädt die Seite eine enorme Menge an DOM-Daten?
Sind die Schritte zur Layout-Erstellung und Darstellung aufwändig?
Wird aufgrund der Interaktion mit dem Benutzer kostspielige Arbeit ausgeführt?
4. JavaScript
Nur in dieser Phase spielen Details auf Funktionsebene eine Rolle.
Welche Operationen verbrauchen tatsächlich CPU-Zeit?
Welche davon laufen wiederholt ab?
Welche hängen mit etwas zusammen, was der Benutzer gerade getan hat?
5. Speicher
Falls sich die Leistung der App mit zunehmender Laufzeit verschlechtert, lohnt es sich, den Speicher zu überprüfen.
Lecks sowie unkontrolliertes Wachstum können sich als allgemeine Trägheit tarnen.
6. Tatsächlicher Einfluss auf den Benutzer
Zuletzt sollte man alles, was man herausgefunden hat, mit der tatsächlichen Benutzererfahrung in Verbindung bringen.
Ist der Startvorgang schneller geworden?
Fühlt sich die Suche reaktionsfähiger an?
Hat sich die Navigation verbessert?
Ist ein bestimmter Arbeitsablauf weniger zeitaufwendig geworden?
Falls sich nichts davon geändert hat, lohnt es sich zu hinterfragen, ob das eigentliche Problem überhaupt erkannt wurde.
Leistungsbudgets sind nützlich – wenn sie mit der Realität verbunden sind
Es ist üblich, dass Teams Regeln wie folgende festlegen:
Halten Sie das JavaScript-Bundle unter 300 KB.
Das ist ein vernünftiger Ausgangspunkt – besser als völlig ohne Richtlinien zu arbeiten.
Aber ein Budget, das auf echter Erfahrung beruht, ist in der Regel nützlicher:
- Der Hauptinhalt sollte schnell angezeigt werden
- Die Suche sollte nicht endlos dauern
- Die Navigation sollte sofortige Rückmeldungen liefern
- Wichtige Seiten sollten nicht von einer Abfolge sequenzieller Anfragen abhängen
- Das für die erste Interaktion benötigte JavaScript sollte auf ein Minimum beschränkt werden
- Große Datensätze sollten nicht auf einmal auf dem Bildschirm angezeigt werden
Diese Kriterien lassen sich schwer auf eine einzige Zahl reduzieren.
Aber sie entsprechen viel genauer dem, was Nutzer tatsächlich bemerken und worum es ihnen geht.
Metriken sind nützlich, wenn sie dabei helfen, zu verstehen, was tatsächlich vor sich geht.
Sie werden gefährlich, sobald das Erreichen bestimmter Zahlen zum eigentlichen Ziel wird.
Die Optimierungsfalle
Es gibt einen subtilen psychologischen Druck, der Entwickler auf diesen Weg führt.
Das Optimieren von etwas wirkt wie Fortschritt.
Man kann auf eine Commit-Nachricht zeigen und sagen:
Die Größe des Bundles wurde um 14 % reduziert.
Oder auf einen Benchmark zeigen und sagen:
Diese Funktion läuft jetzt 32 % schneller.
Oder jemandem ein Screenshot des Profilers als Beweis geben.
Diese Erfolge fühlen sich gut an.
Aber einige der wirksamsten Leistungsverbesserungen sind ehrlich gesagt langweilig.
Das Weglassen einer unnötigen API-Aufruf macht keine spannende Demonstration aus.
Auch die Umgestaltung einer Backend-Antwort ist nicht besonders beeindruckend.
Auch das Kürzen einer Abhängigkeitskette zählt nicht dazu.
Auch das Beheben einer Abfrage, die 5.000 Zeilen abruft, obwohl 50 ausreichen würden, zählt nicht dazu.
Auch das Hinzufügen eines Caches zählt nicht dazu.
Arbeit, die man vermeidet, bleibt in der Regel unsichtbar.
Deshalb wird sie so leicht übersehen.
Der größte Leistungsvorteil kann darin bestehen, weniger Code zu haben, weniger Anfragen, weniger Berechnungen sowie insgesamt weniger laufende Prozesse.
Möglicherweise gibt es nichts, wofür es sich lohnt, ein Screenshot zu machen.
Die Anwendung fühlt sich einfach besser in der Nutzung an.
Optimierung sollte mit Beweisen beginnen
Hier ist eine Regel, die man grundsätzlich befolgen sollte:
Optimieren Sie keinen Code. Optimieren Sie nur bestätigte Probleme.
Dafür ist es nicht notwendig, um jedes veröffentlichte Feature einen aufwändigen Leistungsengineering-Prozess zu entwickeln.
Es bedeutet lediglich, genügend Beweise zu sammeln, um herauszufinden, wohin die Zeit tatsächlich geht.
Schließen Sie sich Profilierungstools an.
Nutzen Sie die integrierten Leistungsanzeigen des Browsers.
Aufzeichnen Sie Netzwerkprotokolle.
Importieren Sie Produktivitätsdaten aus Telemetriediensten.
Richten Sie eine Überwachung echter Benutzer ein, wo es sinnvoll ist.
Testen Sie auf Geräten, die Ihrer tatsächlichen Zielgruppe entsprechen.
Und vor allem: Reproduzieren Sie den Fehler genau so, wie er gemeldet wurde.
Falls ein Interessengruppenmitglied sagt „Das Dashboard wirkt träge“, widerstehen Sie dem Drang, direkt in den Komponentencode einzutauchen und Renderings wegzulassen.
Finden Sie zunächst heraus, was genau mit „träge“ gemeint ist.
Ist die anfängliche Serverantwort langsam?
Ist ein bestimmter API-Aufruf das Engpassproblem?
Ist die JavaScript-Bundel zu groß?
Dauert das Parsen zu lange?
Ist das Rendering der aufwändige Teil?
Gibt es eine lange Aufgabe, die den Hauptthread blockiert?
Läuft eine Datenbankabfrage unzureichend schnell?
Gibt es Aufträge, die Verzögerungen durch Stapelung verursachen?
Läuft der Browser untätig und wartet auf etwas, das schon früher hätte beginnen können?
Oder ist die App technisch reaktiv, liefert dem Benutzer aber keine visuelle Rückmeldung?
Das Debuggen von Leistung ist wie Detektivarbeit.
Es geht nicht darum, wer am meisten JavaScript löschen kann.
Die beste JavaScript-Optimierung könnte weniger JavaScript sein
Das mag für einen JavaScript-Entwickler wie etwas Seltsames klingen.
Aber je größer die Anwendungen werden, desto wichtiger wird das.
Jeder Codeabschnitt, der auf der Client-Seite ausgeführt wird, hat Kosten.
Er muss heruntergeladen werden.
Er muss möglicherweise analysiert werden.
Er muss möglicherweise kompiliert werden.
Er muss ausgeführt werden.
Er verbraucht Speicher.
Er konkurriert mit dem Browser um die Darstellungzeit.
Er kann bei Benutzerinteraktionen zu Verzögerungen führen.
Nichts davon bedeutet, dass JavaScript von Natur aus schlecht ist.
Das bedeutet vielmehr, dass jede auf der Client-Seite durchgeführte Berechnung ihre Notwendigkeit rechtfertigen muss.
Mannchmal ist die bessere Lösung das Rendern auf dem Server.
Mannchmal besteht sie darin, Inhalte zu streamen anstatt darauf zu warten.
Mannchmal bedeutet sie, die Logik vollständig in den Backend zu verlagern.
Mannchmal geht es darum, einen Cache einzuführen.
Mannchmal besteht sie darin, das zu reduzieren, was die API zurückgibt.
Mannchmal bedeutet sie, Dinge schrittweise zu laden anstatt alles auf einmal.
Mannchmal geht es einfach darum, eine Funktion zu entfernen, die niemand tatsächlich verwendet.
Und manchmal ist das bereits vorhandene JavaScript völlig in Ordnung, wie es ist.
Die Schlussfolgerung ist, nicht mehr automatisch davon auszugehen, dass die Optimierung unbedingt innerhalb des JavaScript selbst stattfinden muss.
Was erfahrene Entwickler letztendlich lernen
Zu Beginn der Karriere eines Entwicklers bedeutet Optimierung in der Regel, dass bestehender Code schneller ausgeführt wird.
Mit mehr Erfahrung richtet sich der Fokus darauf, Arbeiten zu beseitigen, die eigentlich nicht notwendig waren.
Schließlich geht das Nachfragen tiefer und befasst sich damit, warum diese Arbeiten überhaupt notwendig sind.
Das ist eine sinnvolle Entwicklung im Denken.
Anstelle der Frage:
Kann diese Schleife schneller ausgeführt werden?
wird die Frage lauten:
Warum werden überhaupt 20.000 Datensätze im Browser durchlaufen?
Anstelle der Frage:
Wie kann verhindert werden, dass dieses Komponente neu gerendert wird?
wird die Frage lauten:
Warum löst diese eine Interaktion eine Änderung im gesamten Seitenzustand aus?
Anstelle der Frage:
Wie kann dieses Paket kleiner gemacht werden?
Die Frage lautet dann:
Warum muss der Benutzer diesen Code herunterladen, bevor er irgendetwas Sinnvolles tun kann?
Anstatt zu fragen:
Wie kann dieser Antrag effizienter gestaltet werden?
Stellt sich die Frage:
Ist dieser Antrag überhaupt notwendig?
Durch das Stellen solcher tiefergehender Fragen entsteht eine wirklich bessere Architektur.
Ziel ist kein schneller Code
Das ist die Lektion, die am längsten zu verinnerlichen ist.
Arbeit an der Leistungsfähigkeit bedeutet nicht, den schnellsten möglichen JavaScript-Code zu erstellen.
Es geht darum, ein Produkt zu entwickeln, das für die tatsächlich Nutzer ausreichend schnell wirkt.
Diese beiden Ziele sind nicht dasselbe.
Ein wunderbar optimierter Algorithmus ist nutzlos, wenn der Benutzer zwei Sekunden lang warten muss, bis die Anfrage gestartet wird.
Ein perfekt gememorisierter Komponente hilft nicht, wenn die Seite 40 Komponenten rendernt, die der Benutzer niemals zu Gesicht bekommt.
Ein kleineres Paket ist nicht automatisch vorteilhaft, wenn die App weiterhin die Hauptinteraktion durch sinnlose Aufgaben blockiert.
Und eine verbesserte Benchmark-Zahl bedeutet nichts, wenn kein echter Benutzer den Unterschied spürt.
JavaScript-Entwickler haben heute Zugang zu mehr Optimierungstechniken als je zuvor.
Deshalb ist es umso wichtiger zu wissen, was sich nicht lohnt, zu optimieren.
Fokussieren Sie sich zunächst auf den Benutzer.
Erkennen Sie die tatsächlichen Verzögerungen.
Messen Sie diese ordnungsgemäß.
Aufspüren Sie sie im gesamten System, von Anfang bis Ende.
Beseitigen Sie alle unnötigen Aufgaben.
Nur dann kann man daran arbeiten, das Verbleibende zu beschleunigen.
Diese Abfolge ist wichtig.
Denn die wertvollste Optimierung ist nicht unbedingt die technisch beeindruckendste.
Sondern die, durch die der Benutzer gar nicht mehr bemerkt, dass die Anwendung ursprünglich langsam war.
Verwandte Artikel
- Native Browser APIs ersetzen 2026 beliebte npm-Pakete — Erklärt, wie native JavaScript- und CSS-Funktionen wie Signals, der Pipeline-Operator, Temporal sowie Anchor Positioning gängige npm-Pakete ersetzen.