Startseite / Artikel / Jenseits der Paketgröße: Was tatsächlich Ihre Webanwendung verlangsamt

Jenseits der Paketgröße: Was tatsächlich Ihre Webanwendung verlangsamt

Warum das Entfernen von Kilobyten selten dazu führt, dass eine langsame Anwendung schneller wird, und wie man die tatsächliche Wartezeit über Server, Workflows, Verarbeitungsschritte, Drittanbieter-Skripte und Bilder nachverfolgen kann.

3024 Wörter

In den Frontend-Teams spielt sich ein bekanntes Szenario ab: Wochenlang wird daran gearbeitet, 40 KB aus einem JavaScript-Bundle zu entfernen, während eine 900-Millisekunden-dauernde Datenbankabfrage auf dem kritischen Anfragenpfad unverändert bleibt. Eine Icon-Bibliothek wird ausgetauscht, eine Abhängigkeit ersetzt, ein weiteres Bundler-Plugin konfiguriert – und es entbrennt eine Debatte darüber, ob ein Paket 18 KB oder 12 KB wiegt. Dann lädt jemand die App auf einem echten Telefon über ein echtes Netzwerk und sie wirkt weiterhin langsam.

Dieser Artikel erklärt, warum die Größe des Bundles so oft das falsche Ziel ist, woher das Warten tatsächlich stammt und wie man einen Optimierungsprozess durchführt, der zuerst die größten Probleme behebt. Kleinere Bundles helfen tatsächlich – manchmal sogar sehr stark. Doch wenn die Seite langsam ist, weil sie auf den Server wartet, das Rendering blockiert, unnötige Arbeit leistet, zu viele Anfragen sendet oder einen riesigen Komponentenbaum initialisiert, rettet das Weglassen weiterer 20 KB sie nicht.

Warum die Größe des Bundles zum Standardleistungsziel wurde

Die Größe des Bundles ist attraktiv, weil es sich um eine Zahl handelt. Ihre Build-Ausgabe sieht etwa so aus:

main.js       842 KB
vendor.js     611 KB
styles.css     94 KB

Jemand schlägt vor, die JavaScript-Größe auf unter 500 KB zu reduzieren – plötzlich hat das Team ein Ziel. Man kann dieses in CI durchsetzen, es bei Pull Requests überwachen und jedes Prozentpunkte-Einsparung feiern. Das wirkt wie technischer Fortschritt, und manchmal ist es das tatsächlich auch.

Probleme entstehen, wenn die Zahl nicht mehr nur ein Symptom ist, sondern zum eigentlichen Ziel wird. Teams neigen dazu, jenen Teil der Leistung zu optimieren, den sie am deutlichsten sehen können, anstatt jenen Teil, der den Nutzern die meisten Zeit kostet. Um zu verstehen, warum das wichtig ist, vergleichen wir zwei hypothetische Anwendungen.

Anwendung A: kleines Bundle, alles andere langsam

Die erste Anwendung liefert ein kompaktes Bundle:

JavaScript: 250 KB

Doch alles darumherum ist aufwändig in der Verarbeitung:

Server response: 1.2s
Database query: 700ms
API calls before rendering: 5
Main-thread work: 900ms

Anwendung B: großer Bundle, schneller Zugriff auf den Inhalt

Die zweite Anwendung enthält fast dreimal so viel JavaScript:

JavaScript: 700 KB

Doch sowohl der Server als auch der Client erledigen deutlich weniger Arbeit, bevor die Seite nutzbar ist:

Server response: 150ms
Database query: 40ms
API calls before rendering: 1
Main-thread work: 180ms

Anwendung A gewinnt nicht automatisch. Eine Anwendung im Stil von B wirkt in den meisten Fällen schneller, weil sie etwa eine Sekunde weniger Zeit auf Serverausführungen, Datenbankabfragen, sequenzielle Anfragen und Arbeit auf der Hauptthread verbringt – was bei den meisten Nutzern mit einer vernünftigen Verbindung die zusätzliche Downloadzeit überwiegt. Das Prinzip, auf dem jede Leistungsdiskussion beruhen sollte, ist einfach: Nutzer nehmen Kilobyte nicht wahr, sie nehmen Wartezeiten wahr.

Die Seitenladung ist ein langer Prozess, keine drei Schritte

Viele Entwickler haben ein vereinfachtes Modell der Ladenprozesse im Kopf:

Download JavaScript
        ↓
Execute JavaScript
        ↓
Page appears

Ein echter Anfrageverlauf durchläuft viele weitere Schritte, von denen jeder zum Stillstand kommen kann:

DNS
 ↓
Connection
 ↓
TLS
 ↓
Request
 ↓
Server processing
 ↓
Database
 ↓
Response
 ↓
HTML parsing
 ↓
CSS processing
 ↓
JavaScript download
 ↓
JavaScript parsing
 ↓
JavaScript execution
 ↓
Hydration
 ↓
API requests
 ↓
Rendering
 ↓
Layout
 ↓
Paint

Die DNS-Aufrufe, die Verbindungsherstellung sowie die TLS-Verhandlung finden statt, bevor der Server irgendetwas wahrnimmt. Anschließend erfolgt die Verarbeitung durch den Server sowie die Arbeit mit der Datenbank. Danach analysiert der Browser das HTML, verarbeitet den CSS-Code, lädt JavaScript herunter, analysiert und führt es aus, füllt die Inhalte ein, sendet API-Aufrufe und erst danach rendernt, legt das Layout fest und zeichnet die Seite aus. Wenn der Benutzer klickt, wiederholt sich ein Großteil dieses Zyklus.

Mit so vielen Schritten ist das Bundle nur einer der Orte, an denen Zeit verloren gehen kann. „Das Bundle kleiner machen“ ist ein schlechter erster Schritt, denn damit wird bereits eine Lösung angenommen, bevor man überhaupt weiß, wo die Zeit verschwindet.

Der Server könnte der langsamste Bestandteil Ihrer Frontend-Anwendung sein

Frontend-Entwickler betrachten Leistung naturgemäß als ein Problem des Browsers. Man öffnet die DevTools, inspiziert das Network-Fenster sowie die JavaScript-Blöcke und führt Lighthouse aus. Doch ein großer Teil der wahrgenommenen Frontend-Geschwindigkeit wird bereits entschieden, bevor der Browser irgendetwas Nützliches erhält.

Nehmen wir einen Anfrage an ein Dashboard:

GET /dashboard

Hinter den Kulissen könnte der Server all das vor dem Antworten erledigen:

→ authenticate user
→ fetch organization
→ fetch permissions
→ query projects
→ query project statistics
→ query notifications
→ calculate recommendations
→ render response

Falls diese Abfolge 1,4 Sekunden dauert, verändert eine Verringerung der Dateigröße von 600 KB auf 500 KB die Benutzererfahrung kaum, denn die ersten sinnvollen Bytes erreichen den Browser weiterhin nach 1,4 Sekunden. Der Browser kann keine Daten darstellen, die er noch nicht erhalten hat.

Ein häufiger Übeltäter ist Code im Backend, der unabhängige Operationen nacheinander abwartet. Es beginnt mit dem Abrufen des Benutzers:

const user = await getUser();

und setzt sich mit einer Kette weiterer Wartevorgänge für Organisationen, Projekte und Benachrichtigungen fort, wobei jeder Vorgang auf das Abschließen des vorherigen wartet:

const organization = await getOrganization(user.orgId);const projects = await getProjects(organization.id);const notifications = await getNotifications(user.id);

Einige dieser Schritte sind tatsächlich voneinander abhängig: Die Abfrage der Organisation benötigt user.orgId, und die Projekte benötigen die Organisations-ID. Doch alles, was nicht von einem früheren Ergebnis abhängt, zahlt unnötigerweise seine Latenz nacheinander. Wenn die Operationen unabhängig voneinander sind, kann ihr gemeinsames Starten und Warten die Reaktionszeit erheblich verkürzen:

const [user, notifications] = await Promise.all([
  getUser(),
  getNotifications()
]);

Beachten Sie, dass die parallele Version getNotifications() ohne Benutzer-ID aufruft. Das funktioniert nur, wenn Benachrichtigungen aus bereits verfügbaren Daten wie der Sitzung abgerufen werden können; andernfalls muss dieser Aufruf weiterhin auf den Benutzer warten. Die allgemeine Regel besteht darin, das tatsächliche Abhängigkeitsdiagramm abzubilden und jede Ebene davon gleichzeitig auszuführen. Weitere Informationen zur Auswahl zwischen diesen Mustern finden Sie in unserem Leitfaden zu Promise.all, Promise.race und sequenziellen awaits. Eine solche Änderung kann leicht eine bessere Leistung erbringen als jede Art von Komprimierungsarbeit.

Waterfall-Methoden kosten mehr als nur Bytes

Einer der produktivsten Orte, um mit einer Untersuchung zu beginnen, ist die Registerkarte „Netzwerk“ und nicht der Bundle-Analysator. Ein sehr häufiges Muster sieht wie folgt aus:

HTML
 ↓
JavaScript
 ↓
API A
 ↓
API B
 ↓
API C
 ↓
API D

Jeder Schritt wartet auf den vorherigen, und jede Übertragung verursacht zusätzliche Latenz durch Hin- und Rückweg. Vergleichen Sie das mit einem Design, bei dem die erste Antwort bereits alles enthält, was die Seite benötigt:

HTML
 ↓
API response containing everything required

Die zweite Version kann zwar mehr Bytes übertragen, ist dennoch erheblich schneller. Bytes und Latenz sind unterschiedliche Probleme. Eine 100 KB große Antwort, die sofort ankommt, kann einer 20 KB großen Antwort überlegen sein, die vier aufeinanderfolgende Übertragungen benötigt, bevor die Seite etwas Nützliches tun kann – insbesondere in Mobilnetzen, wo jede Übertragung kostspielig ist.

Wenn Sie also feststellen, dass ein Endpunkt 300 KB zurückgibt, ist der erste Impuls, die Datenmenge zu verringern. Das mag sinnvoll sein, doch eine bessere erste Frage ist, warum der Benutzer diese Antwort überhaupt benötigt, bevor er mit der Seite interagieren kann. Die Antwort zeigt oft Aufgaben auf, die verschoben oder ganz weggelassen werden können.

Die Leistungsfähigkeit von JavaScript ist wichtiger als seine Größe

Eine damit verbundene Falle besteht darin, die Größe des JavaScript-Codes mit der von ihm erledigten Arbeit zu verwechseln. Ein 500 KB großer Bundle ist noch lange kein Desaster. Entscheidend ist vielmehr, was der Browser mit ihm tun muss:

  1. Den Code herunterladen.
  2. Ihn analysieren.
  3. Ihn kompilieren.
  4. Ihn ausführen.
  5. Den Anwendungsstatus aufbauen.
  6. Komponentenbäume erstellen.
  7. Ereignishandler zuweisen.
  8. Serverseitig generierten Markup-Code aktivieren.
  9. Die Anordnung neu berechnen.
  10. Das Ergebnis darstellen.

Zwei Apps mit ähnlichen Paketgrößen können sich in Bezug auf die Ausführungskosten erheblich unterscheiden. Betrachten wir eine Tabelle mit 5.000 Zeilen – das Problem liegt selten in den Daten selbst, sondern meist darin, 5.000 interaktive DOM-Node-Zeilen anzuzeigen. Die Lösung besteht nicht darin, 50 KB an Skriptcode zu reduzieren, sondern nur die etwa 30 Zeilen anzuzeigen, die derzeit sichtbar sind. Diese Technik, die Virtualisierung, behält dieselbe Anwendung und dieselben Daten bei, während sie potenziell den größten Teil der Arbeit des Browsers entfernt.

Wenn die Hydratierung zum Engpass wird

Dieser Unterschied ist insbesondere in React und anderen Komponenten-Frameworks, die auf dem Server rendern, deutlich. Das Server-Rendering bringt den HTML-Inhalt schnell auf den Bildschirm, doch der Browser muss anschließend möglicherweise einen großen Komponentenbaum hydratisieren, bevor etwas auf Eingaben reagiert:

HTML arrives quickly
        ↓
User sees content
        ↓
Browser starts hydration
        ↓
Large amount of JavaScript executes
        ↓
Page becomes interactive

Die Seite wirkt bereits viel früher fertig, als sie tatsächlich bereit ist. Deshalb kann die Messung nur zum Zeitpunkt des ersten Auftretens des Inhalts zu Fehleinschätzungen führen. Ein Dashboard mit 200 interaktiven Komponenten kann durchaus ein vernünftiges HTML erzeugen und dennoch während des Hydratierungsprozesses erhebliche CPU-Ressourcen verbrauchen, wodurch Klicks unbeantwortet bleiben.

Die wichtige Frage ist hier nicht, ob das Bundle zu groß ist, sondern warum so viel Code sofort interaktiv sein muss. Einige Komponenten benötigen überhaupt kein Client-JavaScript. Manche Interaktionen können in kleine Einheiten isoliert werden. Einige Widgets können später geladen werden, und einige serverseitig gerenderte Komponenten benötigen möglicherweise gar keine Hydratierung. Techniken wie die, die in unserer Übersicht zu teilweiser Vorerstellung und gleichzeitiger Darstellung erläutert werden, bringen weitaus mehr Vorteile als nur Streitigkeiten über eine 30 KB große Abhängigkeit.

Scripts Dritter überwiegen oft Ihren eigenen Code

Vor dem Start einer Kampagne in Bundle-Größe sollten Sie prüfen, wie viel des mitgelieferten Codes von jemand anderem geschrieben wurde. Typische Beispiele sind:

  • Analytics-Tools
  • Chat-Widgets
  • Heatmaps
  • A/B-Testing
  • Werbemittel
  • Tools für Kundensupport
  • Session-Recording
  • Soziale Einbettungen
  • Marketing-Pixel
  • Consent-Management

Jedes dieser Elemente kann Anfragen, Skriptausführungen, Layoutanpassungen sowie Netzwerkaktivitäten hinzufügen. Ironischerweise werden diese Scripts oft kaum überprüft, während Ingenieure Stunden damit verbringen, den Anwendungscode zu optimieren. Eine Seite kann solche Komponenten wie folgt laden:

app.js
analytics.js
chat.js
tracking.js
experimentation.js
heatmap.js

Das Team feiert eine Reduzierung von 70 KB in app.js, obwohl die Seite weiterhin Hunderte von Kilobyte an Code von Drittanbietern ausführt. Deshalb benötigen Leistungsbudgets einen breiteren Ansatz. Statt zu fragen, wie groß das Bundle ist, sollte man fragen, wie viel Code das Gerät des Benutzers vor dem Nutzen dieser Seite verarbeiten muss. Die beiden Fragen haben völlig unterschiedliche Antworten.

Bilder können das gesamte JavaScript-Budget übersteigen

Überdimensionierte Bilder sind ein weiterer Blindpunkt. Ein einzelnes Hero-Bild kann den Wert eines ganzen optimierten JavaScript-Teils übertreffen:

main.js          180 KB
hero.webp        1.4 MB
product.jpg      900 KB
background.png   2.1 MB

Falls ein Pull Request zur Entfernung einer 12 KB großen Abhängigkeit eingereicht wird, während ein 2,1 MB großes Hintergrundbild unverändert bleibt, betreibt das Team eher Priorisierungstheater statt Optimierung. Bilder verdienen dieselbe Sorgfalt wie Code:

  • Wählen Sie moderne Formate wie WebP oder AVIF, sofern sie unterstützt werden und geeignet sind.
  • Bieten Sie responsives Format an statt einer festen Größe.
  • Senden Sie niemals Bilder im Desktop-Format an Handys.
  • Laden Sie Bilder, die unterhalb des Sichtbereichs liegen, verzögert.
  • Laden Sie im Voraus nur die wirklich entscheidenden Bilder.
  • Ersetzen Sie riesige Hintergrundbilder, wenn ein kleineres Asset ausreicht.
  • Wählen Sie die Komprimierungsstufen entsprechend der tatsächlichen Anzeige des Bildes aus.
  • 15 KB an gespartem JavaScript bedeuten wenig, wenn ein Handy trotzdem 2 MB für ein Bild herunterlädt, das der Nutzer kaum wahrnimmt.

    Viele Leistungsprobleme sind Architekturprobleme

    Höher man blickt, desto klarer wird: Viele Leistungsprobleme haben überhaupt nichts mit der Optimierung des Codes zu tun. Sie resultieren aus der Struktur der Anwendung. Stellen Sie sich eine Produktseite vor, die all das benötigt:

    Product
    Reviews
    Recommendations
    Inventory
    Shipping estimate
    User preferences
    Related products
    

    Falls jedes Element nach dem Laden der Seite separat abgerufen wird, kann man zwar jede Anfrage optimieren, doch die Seite bleibt dennoch langsam. Besser ist es, zunächst zu entscheiden, was der Benutzer zuerst sehen muss. Die anfängliche Antwort kann nur Folgendes enthalten:

    Product
    Price
    Availability
    Primary image
    

    Bewertungen, Empfehlungen sowie verwandte Produkte können anschließend streamen. Zu diesem Zeitpunkt optimiert man nicht mehr die Implementierung, sondern definiert neu, was für die Seite als „bereit“ gilt – und genau hier liegen oft die größten Verbesserungen.

    Messen Sie die Meilensteine, die Benutzer tatsächlich wahrnehmen

    Echte Leistungsverbesserungen ersetzen die Frage „Wie groß ist das Paket?“ durch die Frage „Wann kann der Benutzer etwas Nützliches tun?“. Diese Frage führt zu besseren Metriken.

    Zeit bis zum ersten nützlichen Inhalt

    Wann sieht der Benutzer das, wofür er gekommen ist? Das hängt oft von Ihrem Produkt ab: der Kontostand, die Suchergebnisse, das Produktfoto.

    Zeit bis zur Interaktion

    Wann kann der Benutzer zuverlässig interagieren, ohne dass Klicks durch laufende Aufgaben überlagert werden? Neuere Versionen von Lighthouse berücksichtigen TTI nicht mehr in ihrer Bewertung, doch die zugrunde liegende Frage bleibt für Ihre eigenen Seiten von Bedeutung.

    Largest Contentful Paint

    Wann ist der Hauptinhalt sichtbar und vollständig dargestellt?

    Interaktion bis zum nächsten Render

    Wie schnell reagiert die Benutzeroberfläche visuell nach der Interaktion des Benutzers?

    Cumulative Layout Shift

    Verändert sich das Layout, während der Benutzer versucht zu lesen oder zu klicken?

    Gesamte Blockierzeit

    Wie lange wird der Hauptthread durch Aufgaben blockiert, die verhindern, dass der Browser auf Eingaben reagiert?

    Niemand dieser Punkte erzählt für sich allein die ganze Geschichte, doch zusammen beschreiben sie das Erlebnis weitaus besser als eine einzige Zeile wie diese:

    bundle.js = 487 KB
    

    Eine Bundle-Definition beschreibt ein Asset. Leistungsmetriken beschreiben, was der Benutzer erlebt.

    Ein vertrauenswürdiger Optimierungszyklus

    Wenn eine langsame Anwendung repariert werden muss, sollte das Löschen von Abhängigkeiten nicht der erste Schritt sein. Ein disziplinierter Zyklus funktioniert besser.

    1. Unter realistischen Bedingungen nachstellen

    Testen Sie auf repräsentativen Geräten und Netzwerken – nicht nur auf einem schnellen Laptop über das Firmen-Wi-Fi. Viele Ihrer Nutzer haben weder das eine noch das andere.

    2. Messen, um den langsamen Schritt zu finden

    Ermitteln Sie, wo die Zeit verloren geht. Ist das Hauptproblem eines dieser Faktoren?

    server response?
    network?
    rendering?
    JavaScript execution?
    layout?
    images?
    third-party scripts?
    

    3. Den größten Kostenfaktor identifizieren

    Wehren Sie sich dagegen, gleich fünf Dinge zu reparieren. Finden Sie den größten Einzelbeitrag.

    4. Einen Punkt ändern

    Vernichten Sie die kleinste architektonische oder implementierungsbezogene Änderung, die dieses Engpassproblem behebt, damit Sie das Ergebnis zuordnen können.

    5. Messen Sie erneut

    Falls sich die Verbesserung in den Zahlen nicht zeigt, gehen Sie nicht davon aus, dass sie gewirkt hat.

    6. Sichern Sie den Gewinn durch eine Regressionstestung ab

    Verbesserungen verschwinden schnell wieder. Jemand fügt eine Abhängigkeit hinzu, ein Produktteam ein Widget, ein Komponent wird aktiviert, eine Abfrage wird sequenziell – und drei Monate später sind Sie wieder am Ausgangspunkt. Leistung erfordert automatisierte Schutzmechanismen in CI und Monitoring, nicht nur gelegentliche heroische Aufräumarbeiten.

    Die Größe des Bundles ist weiterhin wichtig, nur an anderer Stelle

    Nichts davon macht die Größe des Pakets unwichtig. Große Pakete erhöhen die Kosten für Download, Parsen, Kompilieren und Ausführen, wobei der Nachteil bei langsamen Geräten und Netzwerken am größten ist. Code Splitting, Tree Shaking, lazy Loading sowie das Entfernen ungenutzter Abhängigkeiten sind alle nützlich. Es geht darum, sie anzuwenden, wenn die Erfahrungen zeigen, dass sie das größte Problem darstellen.

    Eine gründliche Leistungsüberprüfung, sortiert nach dem jeweiligen Einfluss, könnte so aussehen: Zunächst eine langsame Serverantwort:

    Problem:
    900ms server response
    

    Gebeilt durch parallele Ausführung von Backend-Anfragen:

    Action:
    parallelize backend requestsResult:
    -420ms
    

    Dann eine aufwändige Initialisierung des Dashboards:

    Problem:
    large dashboard hydration
    

    Lösung durch Aufschieben von Komponenten, die nicht sofort interaktiv sein müssen:

    Action:
    defer non-critical interactive componentsResult:
    -280ms main-thread work
    

    Danach ein zu großes Hero-Bild:

    Problem:
    hero image is 1.8 MB
    

    Lösung durch responsives Bereitstellen in modernen Formaten:

    Action:
    responsive WebP/AVIF deliveryResult:
    -1.2 MB transferred
    

    Nur danach erreicht eine umfangreiche JavaScript-Abhängigkeit den Anfang der Liste:

    Problem:
    large JavaScript dependency
    

    Auch ihr Ersatz spart immer noch eine beträchtliche Menge:

    Action:
    replace dependencyResult:
    -60 KB
    

    Diese Korrektur ist dennoch eine gute Leistung. Sie gehört einfach an vierter Stelle in die Warteschlange, nicht an erster.

    Haupterkenntnisse

    • Optimieren Sie nach der Wartezeit, nicht nach dem kleinstmöglichen Dateivolumen. Die Größe des Bundles ist nur ein von vielen Indikatoren.
    • Betrachten Sie vor dem Bundle-Analysewerkzeug den Server sowie die Anfragenabfolge; Latenz und Hin- und Rückwege kosten oft mehr als Bytes.
    • Messen Sie JavaScript an der Arbeit, die es erledigt – Parsing, Ausführung, Rendering und Hydratierung – und nicht nur an seiner Größe.
    • Auditen Sie Drittanbieter-Skripte und Bilder mit derselben Sorgfalt wie Ihren eigenen Code.
    • Definieren Sie für jede Seite neu, was „bereit“ bedeutet, damit der kritische Inhalt zuerst ankommt und alles Weitere folgt.
  • Arbeiten Sie in einem Zyklus aus Wiederholung, Messen, Änderung einer einzigen Sache, erneutem Messen und Schutz jedes Erreichten durch eine Regressionstestung.
  • Die wertvollste Frage bei der Leistungsverbesserung ist, was den Benutzer zum Warten zwingt und warum.
  • Verwandte Artikel