Informationen zu Interaction to Next Paint (INP)

Interaction to Next Paint (INP)

Informationen zu diesem Codelab

subjectZuletzt aktualisiert: Jan. 9, 2025
account_circleVerfasst von Michal Mocny, Brendan Kenny

1. Einführung

Eine interaktive Demo und ein Codelab zum Messwert Interaction to Next Paint (INP).

Ein Diagramm, das eine Interaktion im Hauptthread darstellt. Der Nutzer gibt eine Eingabe ein, während Aufgaben blockiert werden. Die Eingabe wird verzögert, bis diese Aufgaben abgeschlossen sind. Danach werden die Event-Listener für „pointerup“, „mouseup“ und „click“ ausgeführt. Anschließend werden Rendering- und Zeichenvorgänge gestartet, bis der nächste Frame präsentiert wird.

Vorbereitung

  • Kenntnisse in der HTML- und JavaScript-Entwicklung.
  • Empfohlen: Lesen Sie die INP-Dokumentation.

Lerninhalte

  • Wie sich das Zusammenspiel von Nutzerinteraktionen und der Art und Weise, wie Sie diese Interaktionen verarbeiten, auf die Reaktionsfähigkeit der Seite auswirkt.
  • So lassen sich Verzögerungen reduzieren und vermeiden, um eine reibungslose Nutzererfahrung zu ermöglichen.

Voraussetzungen

  • Ein Computer, auf dem Code von GitHub geklont und npm-Befehle ausgeführt werden können.
  • Einen Texteditor.
  • Eine aktuelle Version von Chrome, damit alle Interaktionsmessungen funktionieren.

2. Einrichten

Code abrufen und ausführen

Der Code befindet sich im Repository web-vitals-codelabs.

  1. Klonen Sie das Repository in Ihrem Terminal: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. Wechseln Sie in das geklonte Verzeichnis: cd web-vitals-codelabs/understanding-inp
  3. Abhängigkeiten installieren: npm ci
  4. Starten Sie den Webserver: npm run start
  5. Rufen Sie in Ihrem Browser http://localhost:5173/understanding-inp/ auf.

Übersicht über die App

Oben auf der Seite sehen Sie einen Zähler für die Punktzahl und die Schaltfläche Erhöhen. Ein klassisches Beispiel für Reaktivität und Reaktionsfähigkeit.

Screenshot der Demo-App für dieses Codelab

Unter der Schaltfläche sehen Sie vier Messungen:

  • INP: Der aktuelle INP-Wert, der in der Regel die schlechteste Interaktion darstellt.
  • Interaktion: Die Punktzahl der letzten Interaktion.
  • FPS: Die Frames pro Sekunde des Hauptthreads der Seite.
  • Timer: Eine laufende Timer-Animation, die dabei hilft, Ruckeln zu visualisieren.

Die Einträge für FPS und Timer sind für die Messung von Interaktionen nicht erforderlich. Sie werden nur hinzugefügt, um die Visualisierung der Reaktionsfähigkeit zu erleichtern.

Jetzt ausprobieren

Klicken Sie auf die Schaltfläche Increment und sehen Sie zu, wie der Wert steigt. Ändern sich die Werte für INP und Interaktion mit jeder Steigerung?

INP misst, wie lange es dauert, bis die Seite dem Nutzer die gerenderte Aktualisierung anzeigt, nachdem er mit ihr interagiert hat.

3. Interaktionen mit den Chrome-Entwicklertools messen

Öffnen Sie die Entwicklertools über das Menü Weitere Tools > Entwicklertools, indem Sie mit der rechten Maustaste auf die Seite klicken und Untersuchen auswählen oder indem Sie einen Tastenkürzel verwenden.

Wechseln Sie zum Steuerfeld Leistung, das Sie zum Messen von Interaktionen verwenden.

Ein Screenshot des Leistungsbereichs der Entwicklertools neben der App

Erfassen Sie als Nächstes eine Interaktion im Bereich „Leistung“.

  1. Tippen Sie auf die Schaltfläche zum Aufzeichnen.
  2. Interagieren Sie mit der Seite (drücken Sie die Schaltfläche Increment).
  3. Beenden Sie die Aufnahme.

Auf der resultierenden Zeitachse finden Sie den Track Interaktionen. Klicken Sie links auf das Dreieck, um den Bereich zu maximieren.

Eine animierte Demonstration der Aufzeichnung einer Interaktion mit dem Leistungsbereich der DevTools

Es werden zwei Interaktionen angezeigt. Zoomen Sie auf das zweite Bild heran, indem Sie scrollen oder die W-Taste gedrückt halten.

Screenshot des Leistungsbereichs der Entwicklertools. Der Cursor bewegt sich über die Interaktion im Bereich und ein Kurz-Tooltip mit dem Timing der Interaktion wird angezeigt.

Wenn Sie den Mauszeiger auf die Interaktion bewegen, sehen Sie, dass sie schnell war, keine Zeit für die Verarbeitungsdauer benötigt hat und nur eine minimale Zeit für die Eingabeverzögerung und Darstellungsverzögerung. Die genauen Zeitspannen hängen von der Geschwindigkeit Ihres Computers ab.

4. Lang andauernde Event-Listener

Öffnen Sie die Datei index.js und entfernen Sie die Kommentarzeichen vor der Funktion blockFor im Event-Listener.

Vollständiger Code: click_block.html

button.addEventListener('click', () => {
  blockFor
(1000);
  score
.incrementAndUpdateUI();
});

Speichern Sie die Datei. Der Server erkennt die Änderung und aktualisiert die Seite für Sie.

Versuchen Sie noch einmal, mit der Seite zu interagieren. Die Interaktionen werden jetzt deutlich langsamer ablaufen.

Leistungsanalyse

Nehmen Sie eine weitere Aufzeichnung im Steuerfeld „Leistung“ vor, um zu sehen, wie das dort aussieht.

Eine Sekunde lange Interaktion im Bereich „Leistung“

Was früher eine kurze Interaktion war, dauert jetzt eine volle Sekunde.

Wenn Sie den Mauszeiger auf die Interaktion bewegen, sehen Sie, dass die Zeit fast vollständig für die „Verarbeitungsdauer“ aufgewendet wird. Das ist die Zeit, die für die Ausführung der Event-Listener-Callbacks benötigt wird. Da der blockierende blockFor-Aufruf vollständig innerhalb des Event-Listeners erfolgt, wird die Zeit dort verbracht.

5. Test: Verarbeitungsdauer

Probieren Sie verschiedene Möglichkeiten aus, die Arbeit des Event-Listeners neu anzuordnen, um die Auswirkungen auf INP zu sehen.

Benutzeroberfläche zuerst aktualisieren

Was passiert, wenn Sie die Reihenfolge der JS-Aufrufe ändern – zuerst die Benutzeroberfläche aktualisieren und dann blockieren?

Vollständiger Code: ui_first.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  blockFor
(1000);
});

Ist Ihnen aufgefallen, dass die Benutzeroberfläche früher angezeigt wurde? Wirkt sich die Reihenfolge auf INP-Messwerte aus?

Erstellen Sie einen Trace und untersuchen Sie die Interaktion, um festzustellen, ob es Unterschiede gab.

Separate Listener

Was passiert, wenn Sie die Arbeit in einen separaten Event-Listener verschieben? Aktualisieren Sie die Benutzeroberfläche in einem Event-Listener und blockieren Sie die Seite in einem separaten Listener.

Vollständiger Code: two_click.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('click', () => {
  blockFor
(1000);
});

Wie sieht das jetzt im Bereich „Leistung“ aus?

Verschiedene Ereignistypen

Bei den meisten Interaktionen werden viele Arten von Ereignissen ausgelöst, von Zeiger- oder Tastaturereignissen bis hin zu Hover-, Fokus-/Unfokus- und synthetischen Ereignissen wie „beforechange“ und „beforeinput“.

Viele echte Seiten haben Listener für viele verschiedene Ereignisse.

Was passiert, wenn Sie die Ereignistypen für die Ereignis-Listener ändern? Ersetzen Sie beispielsweise einen der click-Event-Listener durch pointerup oder mouseup.

Vollständiger Code: diff_handlers.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('pointerup', () => {
  blockFor
(1000);
});

Keine Änderungen an der Benutzeroberfläche

Was passiert, wenn Sie den Aufruf zum Aktualisieren der Benutzeroberfläche aus dem Event-Listener entfernen?

Vollständiger Code: no_ui.html

button.addEventListener('click', () => {
  blockFor
(1000);
 
// score.incrementAndUpdateUI();
});

6. Verarbeitungsdauer von Testergebnissen

Leistungs-Trace: UI zuerst aktualisieren

Vollständiger Code: ui_first.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  blockFor
(1000);
});

Wenn Sie sich eine Aufzeichnung des Leistungsbereichs ansehen, in der auf die Schaltfläche geklickt wird, sehen Sie, dass sich die Ergebnisse nicht geändert haben. Obwohl eine UI-Aktualisierung vor dem blockierenden Code ausgelöst wurde, hat der Browser die Darstellung auf dem Bildschirm erst nach Abschluss des Event-Listeners aktualisiert. Die Interaktion hat also immer noch etwas mehr als eine Sekunde gedauert.

Eine Interaktion von einer Sekunde im Bereich „Leistung“

Leistungs-Trace: separate Listener

Vollständiger Code: two_click.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('click', () => {
  blockFor
(1000);
});

Auch hier gibt es funktional keinen Unterschied. Die Interaktion dauert weiterhin eine volle Sekunde.

Wenn Sie die Klickinteraktion stark vergrößern, sehen Sie, dass tatsächlich zwei verschiedene Funktionen als Ergebnis des click-Ereignisses aufgerufen werden.

Wie erwartet wird die erste Aufgabe – die Aktualisierung der Benutzeroberfläche – sehr schnell ausgeführt, während die zweite eine volle Sekunde dauert. Die Summe ihrer Auswirkungen führt jedoch zu derselben langsamen Interaktion für den Endnutzer.

Eine vergrößerte Ansicht der einsekündigen Interaktion in diesem Beispiel. Der erste Funktionsaufruf dauert weniger als eine Millisekunde.

Leistungs-Trace: verschiedene Ereignistypen

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

button
.addEventListener('pointerup', () => {
  blockFor
(1000);
});

Diese Ergebnisse sind sehr ähnlich. Die Interaktion dauert weiterhin eine volle Sekunde. Der einzige Unterschied besteht darin, dass der kürzere click-Listener, der nur die Benutzeroberfläche aktualisiert, jetzt nach dem blockierenden pointerup-Listener ausgeführt wird.

Eine vergrößerte Ansicht der einsekündigen Interaktion in diesem Beispiel, die zeigt, dass der Click-Event-Listener nach dem Pointerup-Listener weniger als eine Millisekunde benötigt.

Leistungs-Trace: keine Aktualisierung der Benutzeroberfläche

Vollständiger Code: no_ui.html

button.addEventListener('click', () => {
  blockFor
(1000);
 
// score.incrementAndUpdateUI();
});
  • Der Wert wird nicht aktualisiert, die Seite aber schon.
  • Animationen, CSS-Effekte, Standardaktionen für Webkomponenten (Formulareingabe), Texteingabe und Texthervorhebung werden weiterhin aktualisiert.

In diesem Fall wechselt die Schaltfläche beim Klicken in den aktiven Zustand und wieder zurück. Dazu muss der Browser die Seite neu rendern, was bedeutet, dass es immer noch einen INP gibt.

Da der Event-Listener den Hauptthread für eine Sekunde blockiert hat, wodurch die Seite nicht gerendert werden konnte, dauert die Interaktion immer noch eine volle Sekunde.

Wenn Sie ein Performance-Panel aufzeichnen, sieht die Interaktion praktisch genauso aus wie bei früheren Aufzeichnungen.

Eine Interaktion von einer Sekunde im Bereich „Leistung“

Fazit

Jeder Code, der in einem beliebigen Ereignis-Listener ausgeführt wird, verzögert die Interaktion.

  • Dazu gehören Listener, die über verschiedene Skripts registriert wurden, sowie Framework- oder Bibliothekscode, der in Listenern ausgeführt wird, z. B. eine Statusaktualisierung, die das Rendern einer Komponente auslöst.
  • Nicht nur Ihr eigener Code, sondern auch alle Drittanbieterskripts.

Das ist ein häufiges Problem.

Schließlich gilt: Nur weil Ihr Code keinen Paint-Vorgang auslöst, heißt das nicht, dass kein Paint-Vorgang auf langsame Event-Listener wartet.

7. Test: Eingabeverzögerung

Was ist mit Code, der außerhalb von Event-Listenern lange ausgeführt wird? Beispiel:

  • Wenn Sie ein spät ladendes <script> hatten, das die Seite während des Ladevorgangs zufällig blockiert hat.
  • Ein API-Aufruf, z. B. setInterval, der die Seite regelmäßig blockiert?

Entfernen Sie blockFor aus dem Event-Listener und fügen Sie es einem setInterval() hinzu:

Vollständiger Code: input_delay.html

setInterval(() => {
  blockFor
(1000);
}, 3000);


button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

Was ändert sich?

8. Ergebnisse des Tests zur Eingabeverzögerung

Vollständiger Code: input_delay.html

setInterval(() => {
  blockFor
(1000);
}, 3000);


button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
});

Wenn ein Schaltflächenklick aufgezeichnet wird, der zufällig während der Ausführung des blockierenden Tasks setInterval erfolgt, führt dies zu einer lang andauernden Interaktion, auch wenn in der Interaktion selbst keine blockierenden Vorgänge ausgeführt werden.

Diese langen Zeiträume werden oft als „Long Tasks“ bezeichnet.

Wenn Sie mit dem Mauszeiger auf die Interaktion in den DevTools zeigen, sehen Sie, dass die Interaktionszeit jetzt hauptsächlich auf die Eingabeverzögerung und nicht auf die Verarbeitungsdauer zurückzuführen ist.

Das DevTools-Leistungsanalyse-Panel zeigt eine blockierende Aufgabe von einer Sekunde, eine Interaktion, die während dieser Aufgabe erfolgt, und eine Interaktion von 642 Millisekunden, die hauptsächlich auf die Eingabelatenz zurückzuführen ist.

Beachten Sie, dass sich dies nicht immer auf die Interaktionen auswirkt. Wenn Sie während der Ausführung der Aufgabe nicht klicken, haben Sie vielleicht Glück. Solche „zufälligen“ Nieser können sehr schwer zu debuggen sein, wenn sie nur manchmal Probleme verursachen.

Eine Möglichkeit, diese Probleme zu finden, besteht darin, lange Aufgaben (oder Long Animation Frames) und die Total Blocking Time zu messen.

9. Langsame Präsentation

Bisher haben wir uns die Leistung von JavaScript über Eingabeverzögerung oder Event-Listener angesehen. Was wirkt sich noch auf das Rendern des nächsten Paint aus?

Nun, die Seite mit teuren Effekten zu aktualisieren!

Auch wenn die Seiten schnell aktualisiert werden, muss der Browser möglicherweise viel Arbeit leisten, um sie zu rendern.

Im Hauptthread:

  • UI-Frameworks, die nach Zustandsänderungen Updates rendern müssen
  • DOM-Änderungen oder das Umschalten vieler aufwendiger CSS-Abfrageselektoren können viele Style-, Layout- und Paint-Vorgänge auslösen.

Neben dem Hauptthread:

  • GPU-Effekte mit CSS nutzen
  • Sehr große Bilder mit hoher Auflösung hinzufügen
  • Komplexe Szenen mit SVG/Canvas zeichnen

Skizze der verschiedenen Elemente des Renderns im Web

RenderingNG

Hier einige Beispiele, die häufig im Web zu finden sind:

  • Eine SPA-Website, die das gesamte DOM nach dem Klicken auf einen Link neu erstellt, ohne eine Pause einzulegen, um ein erstes visuelles Feedback zu geben.
  • Eine Suchseite mit komplexen Suchfiltern und einer dynamischen Benutzeroberfläche, für die jedoch teure Listener ausgeführt werden.
  • Ein Schalter für den dunklen Modus, der das Design/Layout für die gesamte Seite auslöst

10. Test: Verzögerung bei der Präsentation

Langsames Speichergerät (requestAnimationFrame)

Wir simulieren eine lange Präsentationsverzögerung mit der requestAnimationFrame() API.

Verschieben Sie den blockFor-Aufruf in einen requestAnimationFrame-Callback, damit er nach der Rückgabe des Event-Listeners ausgeführt wird:

Vollständiger Code: presentation_delay.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

Was ändert sich?

11. Ergebnisse von Tests zur Verzögerung bei der Präsentation

Vollständiger Code: presentation_delay.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();
  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

Die Interaktion dauert weiterhin eine Sekunde. Was ist also passiert?

requestAnimationFrame fordert einen Rückruf vor dem nächsten Rendern an. Da INP die Zeit von der Interaktion bis zum nächsten Paint-Vorgang misst, blockiert das blockFor(1000) im requestAnimationFrame den nächsten Paint-Vorgang weiterhin für eine volle Sekunde.

Eine Interaktion von einer Sekunde im Bereich „Leistung“

Beachten Sie jedoch Folgendes:

  • Wenn Sie den Mauszeiger darauf bewegen, sehen Sie, dass die gesamte Interaktionszeit jetzt auf „Präsentationsverzögerung“ entfällt, da die Blockierung des Hauptthreads nach der Rückgabe des Event-Listeners erfolgt.
  • Die Aktivität des Hauptthreads wird nicht mehr durch das Klickereignis, sondern durch „Animation Frame Fired“ ausgelöst.

12. Interaktionen diagnostizieren

Auf dieser Testseite ist die Reaktionsfähigkeit mit den Werten, Timern und der Zähler-Benutzeroberfläche sehr gut sichtbar. Beim Testen der durchschnittlichen Seite ist sie jedoch subtiler.

Wenn Interaktionen lange dauern, ist nicht immer klar, woran das liegt. Mögliche Ursachen:

  • Eingabeverzögerung?
  • Wie lange dauert die Verarbeitung von Ereignissen?
  • Verzögerung bei der Präsentation?

Sie können die Entwicklertools auf jeder beliebigen Seite verwenden, um die Reaktionsfähigkeit zu messen. So können Sie sich die Gewohnheit aneignen:

  1. Surfen Sie wie gewohnt im Web.
  2. Behalten Sie das Interaktionsprotokoll in der Ansicht mit Live-Messwerten des DevTools-Bereichs „Leistung“ im Blick.
  3. Wenn eine Interaktion schlecht ausgeführt wird, versuchen Sie, sie zu wiederholen:
  • Wenn Sie den Vorgang nicht wiederholen können, können Sie das Interaktionslog verwenden, um Informationen zu erhalten.
  • Wenn Sie das Problem reproduzieren können, zeichnen Sie einen Trace im Bereich „Leistung“ auf.

Alle Verzögerungen

Fügen Sie der Seite ein wenig von allen diesen Problemen hinzu:

Vollständiger Code: all_the_things.html

setInterval(() => {
  blockFor
(1000);
}, 3000);

button
.addEventListener('click', () => {
  blockFor
(1000);
  score
.incrementAndUpdateUI();

  requestAnimationFrame
(() => {
    blockFor
(1000);
 
});
});

Verwenden Sie dann die Konsole und das Leistungsfeld, um die Probleme zu diagnostizieren.

13. Test: Asynchrone Arbeit

Da Sie nicht visuelle Effekte in Interaktionen starten können, z. B. Netzwerkanfragen stellen, Timer starten oder einfach den globalen Status aktualisieren, stellt sich die Frage, was passiert, wenn diese schließlich die Seite aktualisieren.

Solange das Next Paint nach einer Interaktion gerendert werden darf, wird die Interaktionsmessung beendet, auch wenn der Browser entscheidet, dass kein neues Rendering-Update erforderlich ist.

Um das auszuprobieren, aktualisieren Sie die Benutzeroberfläche weiterhin über den Klick-Listener, führen Sie die blockierende Arbeit aber über das Zeitlimit aus.

Vollständiger Code: timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

Und nun?

14. Ergebnisse des Tests zur asynchronen Zusammenarbeit

Vollständiger Code: timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

Eine 27 Millisekunden lange Interaktion mit einer 1 Sekunde langen Aufgabe, die jetzt später im Trace auftritt

Die Interaktion ist jetzt kurz, da der Hauptthread sofort nach der Aktualisierung der Benutzeroberfläche verfügbar ist. Die lange blockierende Aufgabe wird weiterhin ausgeführt, aber erst nach dem Rendern. Der Nutzer erhält also sofort Feedback zur Benutzeroberfläche.

Lektion: Wenn Sie es nicht entfernen können, verschieben Sie es zumindest!

Methoden

Können wir das besser machen als mit einem festen setTimeout von 100 Millisekunden? Wir möchten wahrscheinlich immer noch, dass der Code so schnell wie möglich ausgeführt wird, sonst hätten wir ihn einfach entfernt.

Ziel:

  • Die Interaktion wird incrementAndUpdateUI() ausgeführt.
  • blockFor() wird so schnell wie möglich ausgeführt, blockiert aber nicht den nächsten Malvorgang.
  • Das führt zu einem vorhersehbaren Verhalten ohne „magische Zeitüberschreitungen“.

Dazu haben Sie u. a. folgende Möglichkeiten:

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

„requestPostAnimationFrame“

Im Gegensatz zu requestAnimationFrame allein (das versucht, vor dem nächsten Rendern ausgeführt zu werden, und in der Regel immer noch zu einer langsamen Interaktion führt) ist requestAnimationFrame + setTimeout ein einfaches Polyfill für requestPostAnimationFrame, das den Callback nach dem nächsten Rendern ausführt.

Vollständiger Code: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame
(() => {
    setTimeout
(callback, 0);
 
});
}

button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  afterNextPaint
(() => {
    blockFor
(1000);
 
});
});

Aus ergonomischen Gründen können Sie es sogar in ein Promise einbetten:

Vollständiger Code: raf+task2.html

async function nextPaint() {
 
return new Promise(resolve => afterNextPaint(resolve));
}

button
.addEventListener('click', async () => {
  score
.incrementAndUpdateUI();

  await nextPaint
();
  blockFor
(1000);
});

15. Mehrere Interaktionen (und Wutklicks)

Das Verschieben langer blockierender Aufgaben kann helfen, aber diese langen Aufgaben blockieren die Seite weiterhin und wirken sich auf zukünftige Interaktionen sowie viele andere Seitenanimationen und ‑aktualisierungen aus.

Versuchen Sie es noch einmal mit der asynchronen blockierenden Version der Seite (oder mit Ihrer eigenen, wenn Sie im letzten Schritt eine eigene Variante zum Aufschieben von Aufgaben entwickelt haben):

Vollständiger Code: timeout_100.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  setTimeout
(() => {
    blockFor
(1000);
 
}, 100);
});

Was passiert, wenn Sie mehrmals schnell klicken?

Leistungsanalyse

Für jeden Klick wird eine einsekündige Aufgabe in die Warteschlange gestellt, wodurch der Hauptthread für einen längeren Zeitraum blockiert wird.

Mehrere Aufgaben im Hauptthread, die mehrere Sekunden dauern und Interaktionen mit einer Dauer von bis zu 800 ms verursachen

Wenn sich diese langen Aufgaben mit neuen Klicks überschneiden, führt das zu langsamen Interaktionen, obwohl der Event-Listener selbst fast sofort zurückgegeben wird. Wir haben dieselbe Situation wie im vorherigen Experiment mit Eingabeverzögerungen geschaffen. Diesmal wird die Eingabeverzögerung jedoch nicht durch ein setInterval, sondern durch Arbeit verursacht, die durch frühere Event-Listener ausgelöst wurde.

Strategien

Im Idealfall möchten wir lange Aufgaben komplett entfernen.

  • Entfernen Sie unnötigen Code, insbesondere Skripts.
  • Code optimieren, um lange Aufgaben zu vermeiden
  • Veraltete Aufgaben abbrechen, wenn neue Interaktionen eingehen.

16. Strategie 1: Entprellen

Eine klassische Strategie. Wenn Interaktionen in schneller Folge eingehen und die Verarbeitung oder Netzwerkeffekte kostspielig sind, sollten Sie den Start der Arbeit bewusst verzögern, damit Sie sie abbrechen und neu starten können. Dieses Muster ist nützlich für Benutzeroberflächen wie Felder für die automatische Vervollständigung.

  • Verwenden Sie setTimeout, um den Start aufwendiger Vorgänge mit einem Timer zu verzögern, z. B. um 500 bis 1.000 Millisekunden.
  • Speichern Sie dabei die Timer-ID.
  • Wenn eine neue Interaktion eingeht, brechen Sie den vorherigen Timer mit clearTimeout ab.

Vollständiger Code: debounce.html

let timer;
button
.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

 
if (timer) {
    clearTimeout
(timer);
 
}
  timer
= setTimeout(() => {
    blockFor
(1000);
 
}, 1000);
});

Leistungsanalyse

Mehrere Interaktionen, aber nur eine einzige lange Aufgabe als Ergebnis aller Interaktionen

Trotz mehrerer Klicks wird nur eine blockFor-Aufgabe ausgeführt. Sie wartet eine volle Sekunde lang, bis keine Klicks mehr erfolgen. Bei Interaktionen, die in Schüben erfolgen, z. B. bei der Eingabe von Text oder bei Zielelementen, die voraussichtlich mehrere schnelle Klicks erhalten, ist dies die ideale Standardstrategie.

17. Strategie 2: Lang andauernde Aufgaben unterbrechen

Es besteht immer noch die unglückliche Möglichkeit, dass ein weiterer Klick kurz nach Ablauf des Debounce-Zeitraums erfolgt, in der Mitte dieser langen Aufgabe landet und aufgrund der Eingabeverzögerung zu einer sehr langsamen Interaktion wird.

Idealerweise möchten wir unsere laufende Aufgabe unterbrechen, wenn eine Interaktion in der Mitte unserer Aufgabe eingeht, damit alle neuen Interaktionen sofort bearbeitet werden. Wie können wir das tun?

Es gibt einige APIs wie isInputPending, aber es ist in der Regel besser, lange Aufgaben in Blöcke aufzuteilen.

Viele setTimeout

Erster Versuch: Machen Sie etwas Einfaches.

Vollständiger Code: small_tasks.html

button.addEventListener('click', () => {
  score
.incrementAndUpdateUI();

  requestAnimationFrame
(() => {
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
    setTimeout
(() => blockFor(100), 0);
 
});
});

Dabei kann der Browser jede Aufgabe einzeln planen und Eingaben können eine höhere Priorität erhalten.

Mehrere Interaktionen, aber alle geplanten Arbeiten wurden in viele kleinere Aufgaben unterteilt.

Wir sind wieder bei fünf Sekunden Arbeit für fünf Klicks, aber jede Ein-Sekunden-Aufgabe pro Klick wurde in zehn 100-Millisekunden-Aufgaben unterteilt. Selbst wenn sich mehrere Interaktionen mit diesen Aufgaben überschneiden, hat keine Interaktion eine Eingabeverzögerung von mehr als 100 Millisekunden. Der Browser priorisiert die eingehenden Event-Listener gegenüber der setTimeout-Arbeit und Interaktionen bleiben reaktionsschnell.

Diese Strategie eignet sich besonders gut, wenn Sie separate Einstiegspunkte planen, z. B. wenn Sie eine Reihe unabhängiger Funktionen haben, die beim Laden der Anwendung aufgerufen werden müssen. Wenn Sie nur Skripts laden und alles zur Skriptauswertungszeit ausführen, wird standardmäßig alles in einer riesigen langen Aufgabe ausgeführt.

Diese Strategie funktioniert jedoch nicht so gut, um eng gekoppelten Code aufzuteilen, z. B. eine for-Schleife, die einen gemeinsamen Status verwendet.

Jetzt mit yield()

Wir können jedoch moderne async und await nutzen, um jeder JavaScript-Funktion ganz einfach „Yield-Punkte“ hinzuzufügen.

Beispiel:

Vollständiger Code: yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

Wie zuvor wird der Hauptthread nach einem Arbeitsabschnitt freigegeben und der Browser kann auf eingehende Interaktionen reagieren. Jetzt ist jedoch nur noch ein await schedulerDotYield() anstelle separater setTimeouts erforderlich, sodass die Funktion auch in der Mitte einer for-Schleife verwendet werden kann.

Jetzt mit AbortContoller()

Das hat funktioniert, aber jede Interaktion löst mehr Arbeit aus, auch wenn neue Interaktionen eingegangen sind, die die zu erledigende Arbeit möglicherweise geändert haben.

Bei der Debouncing-Strategie haben wir das vorherige Zeitlimit bei jeder neuen Interaktion abgebrochen. Können wir hier etwas Ähnliches machen? Eine Möglichkeit hierfür ist die Verwendung eines AbortController():

Vollständiger Code: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

Wenn ein Klick eingeht, wird die blockInPiecesYieldyAborty for-Schleife gestartet, in der alle erforderlichen Aufgaben ausgeführt werden. Dabei wird der Hauptthread regelmäßig freigegeben, damit der Browser auf neue Interaktionen reagieren kann.

Wenn ein zweiter Klick eingeht, wird die erste Schleife mit AbortController als abgebrochen gekennzeichnet und eine neue blockInPiecesYieldyAborty-Schleife wird gestartet. Wenn die erste Schleife das nächste Mal ausgeführt werden soll, wird festgestellt, dass signal.aborted jetzt true ist, und die Funktion wird sofort beendet, ohne weitere Aktionen auszuführen.

Die Arbeit im Hauptthread ist jetzt in viele kleine Teile unterteilt, Interaktionen sind kurz und die Arbeit dauert nur so lange wie nötig.

18. Fazit

Wenn Sie alle langen Aufgaben aufteilen, kann eine Website auf neue Interaktionen reagieren. So können Sie schnell erstes Feedback geben und Entscheidungen treffen, z. B. laufende Arbeiten abbrechen. Manchmal bedeutet das, dass Einstiegspunkte als separate Aufgaben geplant werden müssen. Manchmal bedeutet das, dass Sie an geeigneten Stellen „Yield“-Punkte hinzufügen.

Wichtig

  • INP misst alle Interaktionen.
  • Jede Interaktion wird vom Input bis zum nächsten Rendern gemessen – so sieht der Nutzer die Reaktionsfähigkeit.
  • Eingabeverzögerung, Dauer der Ereignisverarbeitung und Darstellungsverzögerung wirken sich alle auf die Reaktionszeit bei Interaktionen aus.
  • Mit den Entwicklertools können Sie INP und Aufschlüsselungen von Interaktionen ganz einfach messen.

Strategien

  • Vermeiden Sie lang laufenden Code (lange Aufgaben) auf Ihren Seiten.
  • Verschieben Sie unnötigen Code aus Event-Listenern bis nach dem nächsten Rendern.
  • Achten Sie darauf, dass die Aktualisierung des Renderings selbst für den Browser effizient ist.

Weitere Informationen