Informationen zu Interaction to Next Paint (INP)

1. Einführung

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

Ein Diagramm, das eine Interaktion im Hauptthread darstellt. Der Nutzer gibt etwas ein, während die Ausführung von Aufgaben blockiert wird. Die Eingabe wird verzögert, bis diese Aufgaben abgeschlossen sind. Anschließend werden die Listener für Zeigerup, Mouseup und Click-Event ausgeführt. Anschließend werden Rendering- und Paint-Arbeiten gestartet, bis der nächste Frame dargestellt wird.

Vorbereitung

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

Lerninhalte

  • Wie das Zusammenspiel von Nutzerinteraktionen und Ihr Umgang mit diesen Interaktionen die Reaktionszeit von Seiten beeinflussen
  • Wie Sie Verzögerungen reduzieren und vermeiden, um die Nutzererfahrung zu optimieren.

Voraussetzungen

  • Computer mit der Fähigkeit, Code aus GitHub zu klonen und npm-Befehle auszuführen.
  • Einen Texteditor.
  • Eine aktuelle Version von Chrome für alle Interaktionsmessungen.

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. Durchlaufen Sie das geklonte Verzeichnis: cd web-vitals-codelabs/understanding-inp
  3. Abhängigkeiten installieren: npm ci
  4. Webserver starten: npm run start
  5. Rufen Sie in Ihrem Browser http://localhost:5173/understanding-inp/ auf.

App-Übersicht

Oben auf der Seite befinden sich ein Score-Zähler und eine Erhöhen-Schaltfläche. Eine klassische Demo der Reaktionsfähigkeit und Reaktionsfähigkeit!

Screenshot der Demo-App für dieses Codelab

Unter der Schaltfläche befinden sich vier Messwerte:

  • INP: Der aktuelle INP-Wert, der in der Regel die schlechteste Interaktion ist.
  • Interaktion: der Wert der letzten Interaktion.
  • fps: die Frames pro Sekunde der Seite im Hauptthread
  • Timer: Eine laufende Timer-Animation zur Visualisierung der Verzögerung.

Die Einträge „fps“ und „Timer“ sind zum Messen von Interaktionen nicht erforderlich. Sie werden hinzugefügt, um die Visualisierung der Reaktionsfähigkeit etwas zu erleichtern.

Jetzt ausprobieren

Versuchen Sie, mit der Schaltfläche Erhöhen zu interagieren, und beobachten Sie, wie sich der Wert erhöht. Ändern sich die Werte INP und Interaction mit jedem Inkrement?

INP gibt an, wie lange es von einer Nutzerinteraktion dauert, bis dem Nutzer das gerenderte Update auf der Seite angezeigt wird.

3. Interaktionen mit den Chrome-Entwicklertools messen

Öffne die Entwicklertools über Weitere Tools > Entwicklertools öffnen, indem Sie mit der rechten Maustaste auf die Seite klicken und Untersuchen auswählen oder die Tastenkombination verwenden.

Wechseln Sie zum Bereich Leistung, in dem Sie Interaktionen messen können.

Screenshot des Steuerfelds „Leistung“ in den Entwicklertools neben der App

Als Nächstes erfassen Sie eine Interaktion im Bereich „Leistung“.

  1. Tippen Sie auf die Schaltfläche zum Aufzeichnen.
  2. Klicken Sie auf die Schaltfläche Erhöhen, um mit der Seite zu interagieren.
  3. Beenden Sie die Aufzeichnung.

In der Zeitachse sehen Sie den Track Interaktionen. Klicke auf das Dreieck auf der linken Seite, um es zu maximieren.

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

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

Screenshot des Steuerfelds „Leistung“ in den Entwicklertools mit dem Mauszeiger auf der Interaktion im Steuerfeld und einer Kurzinfo mit dem kurzen zeitlichen Ablauf der Interaktion

Wenn Sie den Mauszeiger auf die Interaktion bewegen, sehen Sie, dass die Interaktion schnell war, keine Zeit für die Verarbeitungsdauer aufwendet und eine Mindestdauer für Eingabe- und Präsentationsverzögerung hatte. Die genaue Dauer hängt von der Geschwindigkeit Ihres Computers ab.

4. Lang andauernde Ereignis-Listener

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

Vollständigen Code ansehen: click_block.html

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

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

Versuchen Sie noch einmal, mit der Seite zu interagieren. Die Interaktionen sind nun merklich langsamer.

Leistungs-Trace

Machen Sie eine weitere Aufnahme im Bereich „Leistung“, um zu sehen, wie das dort aussieht.

Eine ein Sekunde lange Interaktion im Bereich „Leistung“

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

Wenn Sie den Mauszeiger auf die Interaktion bewegen, sehen Sie, dass die Verarbeitungsdauer fast vollständig abgeschlossen ist. Das ist die Zeit, die für die Ausführung der Ereignis-Listener-Callbacks benötigt wird. Da sich der blockierende blockFor-Aufruf vollständig innerhalb des Event-Listeners befindet, ist die Zeit dafür erforderlich.

5. Test: Verarbeitungsdauer

Probieren Sie aus, wie Sie die Funktion des Event-Listeners neu anordnen können, um die Auswirkungen auf INP zu sehen.

Zuerst UI aktualisieren

Was passiert, wenn Sie die Reihenfolge von JS-Aufrufen vertauschen – erst die Benutzeroberfläche aktualisieren und dann blockieren?

Vollständigen Code ansehen: ui_first.html

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

Ist Ihnen aufgefallen, dass die Benutzeroberfläche zuvor angezeigt wurde? Wirkt sich die Reihenfolge auf die INP-Werte aus?

Versuchen Sie, die Interaktion nachzuzeichnen und zu untersuchen, ob es Unterschiede gibt.

Separate Zuhörer

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

Vollständigen Code ansehen: two_click.html

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

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

Wie sieht das jetzt im Bereich „Leistung“ aus?

Verschiedene Ereignistypen

Die meisten Interaktionen lösen viele Arten von Ereignissen aus, von Zeiger- oder Schlüsselereignissen über den Mauszeiger, Fokus/Unkenntlichmachung und synthetische Ereignisse 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? Möchten Sie beispielsweise einen der click-Event-Listener durch pointerup oder mouseup ersetzen?

Vollständigen Code ansehen: diff_handlers.html

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

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

Kein UI-Update

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

Vollständigen Code ansehen: no_ui.html

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

6. Ergebnisse des Tests zur Verarbeitungsdauer

Leistungs-Trace: Aktualisieren Sie zuerst die Benutzeroberfläche

Vollständigen Code ansehen: ui_first.html

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

Wenn Sie sich eine Aufzeichnung des Klicks auf die Schaltfläche im Bereich „Leistung“ ansehen, sehen Sie, dass sich die Ergebnisse nicht verändert haben. Obwohl vor dem Blockiercode ein UI-Update ausgelöst wurde, hat der Browser die auf dem Bildschirm angezeigten Inhalte erst aktualisiert, nachdem der Ereignis-Listener abgeschlossen war. Das bedeutet, dass die Interaktion noch etwas mehr als eine Sekunde gedauert hat.

Eine noch eine Sekunde lange Interaktion im Bereich „Leistung“

Leistungs-Trace: separate Listener

Vollständigen Code ansehen: two_click.html

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

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

Auch in der Funktion gibt es keinen Unterschied. Die Interaktion dauert immer noch eine ganze Sekunde.

Wenn Sie näher an die Klickinteraktion herangehen, sehen Sie, dass als Ergebnis des click-Ereignisses tatsächlich zwei verschiedene Funktionen aufgerufen werden.

Wie erwartet, wird die erste – Aktualisierung der Benutzeroberfläche – sehr schnell ausgeführt, während die zweite eine ganze Sekunde in Anspruch nimmt. Die Summe ihrer Auswirkungen führt jedoch zu derselben langsamen Interaktion für den Endanwendenden.

Vergrößerte Darstellung der einsekündigen Interaktion in diesem Beispiel, wobei der erste Funktionsaufruf in weniger als einer Millisekunde abgeschlossen ist

Leistungs-Trace: verschiedene Ereignistypen

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

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

Diese Ergebnisse sind sehr ähnlich. Die Interaktion dauert immer noch eine ganze Sekunde. Der einzige Unterschied besteht darin, dass der kürzere click-Listener, der nur UI-Update umfasst, nach dem blockierenden pointerup-Listener ausgeführt wird.

Herangezoomtes Bild der einsekündigen Interaktion in diesem Beispiel. Der Click-Event-Listener benötigt nach dem Zeigerup-Listener weniger als eine Millisekunde für den Abschluss.

Leistungs-Trace: kein UI-Update

Vollständigen Code ansehen: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • Die Punktzahl wird zwar nicht aktualisiert, die Seite aber schon.
  • Animationen, CSS-Effekte, Standardaktionen für Webkomponenten (Formulareingabe), Texteingaben, Texthervorhebungen werden weiterhin aktualisiert.

In diesem Fall wechselt die Schaltfläche in den aktiven Status und kehrt zurück, wenn sie angeklickt wird. Dies erfordert eine Paint-Anzeige durch den Browser, sodass immer noch ein INP vorhanden ist.

Da der Ereignis-Listener den Hauptthread für eine Sekunde blockiert hat, sodass die Seite nicht dargestellt werden konnte, dauert die Interaktion trotzdem noch eine volle Sekunde.

Eine Aufzeichnung im Bereich „Leistung“ zeigt die Interaktion, die praktisch identisch mit den vorherigen ist.

Eine noch eine Sekunde lange Interaktion im Bereich „Leistung“

Tipp

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

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

Dieses Problem tritt häufig auf.

Und schließlich: Nur weil Ihr Code keinen Paint auslöst, bedeutet das nicht, dass ein Paint nicht darauf wartet, dass langsame Ereignis-Listener abgeschlossen werden.

7. Test: Eingabeverzögerung

Was ist mit Code mit langer Ausführungszeit außerhalb von Ereignis-Listenern? Beispiel:

  • Wenn eine <script> zu spät geladen wurde, die die Seite beim Laden zufällig blockiert hat,
  • Ein API-Aufruf wie setInterval, der die Seite regelmäßig blockiert?

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

Vollständigen Code ansehen: input_delay.html

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


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

Was ändert sich?

8. Ergebnisse des Tests zur Eingabeverzögerung

Vollständigen Code ansehen: input_delay.html

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


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

Wird ein Schaltflächenklick aufgezeichnet, der während der Ausführung des blockierenden setInterval-Tasks erfolgt, führt dies zu einer lang andauernden Interaktion, auch wenn die Interaktion selbst nicht blockiert wird.

Diese lang andauernden Zeiträume werden oft als lange Aufgaben bezeichnet.

Wenn Sie den Mauszeiger in den Entwicklertools auf die Interaktion bewegen, sehen Sie, dass die Interaktionszeit jetzt in erster Linie der Eingabeverzögerung und nicht der Verarbeitungsdauer zugeordnet wird.

Der Leistungsbereich der Entwicklertools zeigt eine einzügige Blockierungsaufgabe, eine Interaktion, die teilweise durch diese Aufgabe verläuft, und eine Interaktion von 642 Millisekunden, die hauptsächlich der Eingabeverzögerung zugeschrieben wird.

Beachten Sie, dass sich dies nicht immer auf die Interaktionen auswirkt. Wenn Sie nicht klicken, während die Aufgabe ausgeführt wird, haben Sie vielleicht Glück. Solche „Zufälligen“ niesen kann ein Albtraum sein, wenn sie nur dann zu Problemen führen.

Eine Möglichkeit zum Aufspüren dieser Probleme besteht darin, lange Aufgaben (oder Long Animation Frames) und die Total Blocking Time (Gesamtblockierungszeit) zu messen.

9. Langsame Präsentation

Bisher haben wir uns die Leistung von JavaScript über Eingabeverzögerungen oder Ereignis-Listener angesehen. Aber was wirkt sich noch auf das Rendering von Next Paint aus?

Nun, ich aktualisiere die Seite mit teuren Effekten!

Auch wenn die Aktualisierung der Seite schnell erfolgt, muss der Browser möglicherweise dennoch hart arbeiten, um die Seiten zu rendern.

Im Hauptthread:

  • UI-Frameworks, die Aktualisierungen nach Statusänderungen rendern müssen
  • DOM-Änderungen oder das Wechseln vieler teurer CSS-Abfrageselektoren können viele Stil-, Layout- und Paint-Auslösungen auslösen.

Aus dem Hauptthread:

  • CSS für GPU-Effekte verwenden
  • Hinzufügen sehr großer Bilder mit hoher Auflösung
  • Komplexe Szenen mit SVG/Canvas zeichnen

Skizze der verschiedenen Elemente des Renderings im Web

RenderingNG

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

  • Eine SPA-Website, die nach dem Klicken auf einen Link das gesamte DOM neu erstellt, ohne eine Pause zu machen, um ein erstes visuelles Feedback zu geben.
  • Eine Suchseite, die komplexe Suchfilter mit einer dynamischen Benutzeroberfläche bietet, dafür aber teure Listener ausführt.
  • Ein/Aus-Schaltfläche für den dunklen Modus, der Stil/Layout für die gesamte Seite auslöst

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

Langsames Speichergerät (requestAnimationFrame)

Simulieren Sie nun eine lange Verzögerung bei der Präsentation mit der requestAnimationFrame() API.

Verschiebt den blockFor-Aufruf in einen requestAnimationFrame-Callback, sodass er ausgeführt wird, nachdem der Event-Listener Folgendes zurückgegeben hat:

Vollständigen Code ansehen: presentation_delay.html

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

Was ändert sich?

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

Vollständigen Code ansehen: presentation_delay.html

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

Die Interaktion dauert eine Sekunde, was ist passiert?

requestAnimationFrame fordert vor dem nächsten Paint einen Callback an. Da INP die Zeit von der Interaktion bis zum nächsten Paint misst, blockiert blockFor(1000) im requestAnimationFrame die nächste Paint weiterhin eine ganze Sekunde lang.

Eine noch eine Sekunde lange Interaktion im Bereich „Leistung“

Beachten Sie jedoch zwei Dinge:

  • Wenn Sie den Mauszeiger auf die Anzeige bewegen, sehen Sie, dass die gesamte Interaktionszeit jetzt in „Präsentationsverzögerung“ aufgewendet wird. da die Blockierung des Hauptthreads erfolgt, nachdem der Event-Listener zurückgegeben wurde.
  • Der Stamm der Hauptthread-Aktivität ist nicht mehr das Click-Event, sondern „Animation Frame ausgelöst“.

12. Interaktionen diagnostizieren

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

Wenn Interaktionen lang dauern, ist nicht immer klar, wo der Täter liegt. Ist es:

  • Eingabeverzögerung?
  • Dauer der Ereignisverarbeitung?
  • Verzögerung bei der Präsentation?

Mit den Entwicklertools können Sie auf jeder beliebigen Seite die Reaktionsfähigkeit messen. Probieren Sie diesen Ablauf aus, um es zur Gewohnheit zu machen:

  1. Sie können wie gewohnt im Web surfen.
  2. Optional: Lassen Sie die Entwicklertools-Konsole geöffnet, während die Web Vitals-Erweiterung Interaktionen protokolliert.
  3. Wenn Sie eine Interaktion mit schlechter Leistung feststellen, wiederholen Sie sie:
  • Wenn Sie es nicht wiederholen können, verwenden Sie die Konsolenlogs, um Statistiken abzurufen.
  • Wenn Sie sie wiederholen können, nehmen Sie sie im Bereich „Leistung“ auf.

Alle Verzögerungen

Versuchen Sie, der Seite einige der folgenden Probleme hinzuzufügen:

Vollständigen Code ansehen: all_the_things.html

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

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

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

Anschließend können Sie die Probleme über die Konsole und das Leistungssteuerfeld diagnostizieren.

13. Test: asynchrone Arbeit

Sie können nicht visuelle Effekte innerhalb von Interaktionen starten, z. B. das Senden von Netzwerkanfragen, das Starten von Timern oder das Aktualisieren des globalen Status. Was passiert, wenn diese die Seite letztendlich aktualisieren?

Solange der nächste Paint nach einer Interaktion gerendert werden darf, wird die Interaktionsmessung gestoppt, selbst wenn der Browser feststellt, dass kein neues Rendering-Update erforderlich ist.

Um dies auszuprobieren, aktualisieren Sie die Benutzeroberfläche weiterhin über den Klick-Listener, führen Sie die Blockierung jedoch bis zum Zeitlimit aus.

Vollständigen Code ansehen: timeout_100.html

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

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

Und nun?

14. Asynchrone Ergebnisse des Arbeitstests

Vollständigen Code ansehen: timeout_100.html

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

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

Eine 27-Millisekunden-Interaktion mit einer einsekündigen Aufgabe, die jetzt später im Trace auftritt

Die Interaktion ist jetzt kurz, da der Hauptthread sofort verfügbar ist, nachdem die Benutzeroberfläche aktualisiert wurde. Die lange Blockierungsaufgabe wird immer noch ausgeführt, sie wird nur einige Zeit nach dem Paint ausgeführt, sodass der Nutzer sofortiges UI-Feedback erhält.

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

Methoden

Ist das besser als ein fester setTimeout-Wert von 100 Millisekunden? Wahrscheinlich möchten wir trotzdem, dass der Code so schnell wie möglich ausgeführt wird. Andernfalls hätten wir ihn einfach entfernen können.

Ziel:

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

Dazu gehören:

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

„requestPostAnimationFrame“

Im Gegensatz zu requestAnimationFrame alleine (die versucht, vor dem nächsten Paint auszuführen, und in der Regel trotzdem eine langsame Interaktion bewirkt), sorgt requestAnimationFrame + setTimeout für einen einfachen Polyfill für requestPostAnimationFrame und führt den Callback nach dem nächsten Paint aus.

Vollständigen Code ansehen: raf+task.html

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

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

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

Für Ergonomie kannst du ihm sogar ein Versprechen geben:

Vollständigen Code ansehen: 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 Rage-Klicks)

Das Verschieben langer Blockierungen kann helfen, aber diese langen Aufgaben blockieren weiterhin die Seite, was sich auf zukünftige Interaktionen sowie viele andere Seitenanimationen und -aktualisierungen auswirkt.

Versuchen Sie es noch einmal mit der asynchronen Blockierungsversion der Seite (oder Ihre eigene, wenn Sie im letzten Schritt eine eigene Variante zum Zurückstellen von Arbeit erstellt haben):

Vollständigen Code ansehen: timeout_100.html

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

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

Was passiert, wenn Sie schnell mehrmals klicken?

Leistungs-Trace

Bei jedem Klick befindet sich eine eine Sekunde lange Aufgabe in der Warteschlange, wodurch sichergestellt wird, dass der Hauptthread für einen Großteil der Zeit blockiert ist.

Mehrere zweitlange Aufgaben im Hauptthread, die Interaktionen von nur 800 ms verursachen

Wenn sich diese langen Aufgaben mit neuen Klicks überschneiden, führt dies zu langsamen Interaktionen, auch wenn der Ereignis-Listener selbst fast sofort eine Antwort zurückgibt. Wir haben dieselbe Situation wie im früheren Test mit Eingabeverzögerungen geschaffen. Nur dieses Mal stammt die Eingabeverzögerung nicht von einem setInterval, sondern von Arbeit, die von früheren Event-Listenern ausgelöst wurde.

Strategien

Idealerweise möchten wir lange Aufgaben vollständig entfernen.

  • Entfernen Sie unnötigen Code, insbesondere Skripts.
  • Optimieren Sie Code, um die Ausführung langer Aufgaben zu vermeiden.
  • Veraltete Arbeit abbrechen, wenn neue Interaktionen eintreffen.

16. Strategie 1: Entprellen

Eine klassische Strategie. Wenn Interaktionen schnell hintereinander eingehen und die Verarbeitungs- oder Netzwerkeffekte teuer sind, verzögern Sie absichtlich den Beginn, damit Sie ihn abbrechen und neu starten können. Dieses Muster ist für Benutzeroberflächen wie Felder für die automatische Vervollständigung hilfreich.

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

Vollständigen Code ansehen: debounce.html

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

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

Leistungs-Trace

Mehrere Interaktionen, aber nur eine einzige lange Aufgabe aufgrund aller

Trotz mehrerer Klicks wird nur eine blockFor-Aufgabe ausgeführt. Warten Sie damit, bis eine ganze Sekunde lang keine Klicks erfolgt sind. Für Interaktionen, die in kurzen Abständen erfolgen, z. B. die Eingabe einer Texteingabe oder Zielelemente, die voraussichtlich mehrere Schnellklicks erhalten, ist diese Strategie standardmäßig ideal.

17. Strategie 2: Lang andauernde Arbeit unterbrechen

Es ist immer noch unglücklich, dass direkt nach Ablauf der Entprellphase ein weiterer Klick eingeht, der mitten in dieser langen Aufgabe landet und aufgrund der Eingabeverzögerung zu einer sehr langsamen Interaktion wird.

Wenn eine Interaktion mitten in einer Aufgabe steht, sollten wir im Idealfall unser viel Arbeitsaufwand unterbrechen, damit alle neuen Interaktionen sofort bearbeitet werden. Wie können wir das tun?

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

Viele setTimeouts

Erster Versuch: Etwas Einfaches tun.

Vollständigen Code ansehen: 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);
  });
});

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

Mehrere Interaktionen, aber die gesamte geplante Arbeit wurde in viele kleinere Aufgaben aufgeteilt

Wir haben wieder ganze fünf Sekunden Arbeit für fünf Klicks, aber jede einsekündigen Aufgabe pro Klick wurde in zehn 100-Millisekunden-Aufgaben aufgeteilt. Daher hat keine Interaktion eine Eingabeverzögerung von mehr als 100 Millisekunden, selbst wenn sich mehrere Interaktionen mit diesen Aufgaben überschneiden. Der Browser priorisiert eingehende Event-Listener gegenüber der setTimeout-Arbeit und Interaktionen bleiben responsiv.

Diese Strategie eignet sich besonders gut, wenn Sie separate Einstiegspunkte planen, z. B. wenn Sie eine Reihe unabhängiger Funktionen zum Laden der Anwendung aufrufen müssen. Wenn lediglich die Skripts geladen und zum Zeitpunkt der Skriptauswertung ausgeführt werden, wird möglicherweise standardmäßig alles in einer riesigen langen Aufgabe ausgeführt.

Diese Strategie eignet sich jedoch nicht so gut, um eng gekoppelten Code wie eine for-Schleife mit gemeinsamem Status zu trennen.

Jetzt bei yield()

Wir können jedoch moderne async und await nutzen, um einfach „Ertragspunkte“ hinzuzufügen an jede JavaScript-Funktion an.

Beispiel:

Vollständigen Code ansehen: revenuey.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 Teil der Arbeit erzeugt und der Browser kann auf alle eingehenden Interaktionen reagieren. Jetzt ist jedoch nur noch ein await schedulerDotYield() statt separater setTimeouts erforderlich. Dadurch ist er ergonomisch genug, um ihn auch mitten in einer for-Schleife verwenden zu können.

Jetzt bei AbortContoller()

Das hat funktioniert, aber jede Interaktion plant mehr Arbeit, selbst wenn neue Interaktionen eingetreten sind und möglicherweise die zu erledigende Arbeit verändert haben könnten.

Mit der Entprellstrategie haben wir das vorherige Timeout bei jeder neuen Interaktion aufgehoben. Können wir hier etwas Ähnliches machen? Eine Möglichkeit dafür ist die Verwendung eines AbortController():

Vollständigen Code ansehen: abbrecheny.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 for-Schleife von blockInPiecesYieldyAborty mit den auszuführenden Aktionen gestartet. Dabei wird regelmäßig der Hauptthread zurückgegeben, damit der Browser weiterhin auf neue Interaktionen reagiert.

Wenn ein zweiter Klick eingeht, wird die erste Schleife mit der AbortController als abgebrochen gekennzeichnet und eine neue blockInPiecesYieldyAborty-Schleife wird gestartet. Wenn die erste Schleife wieder geplant wird, wird festgestellt, dass signal.aborted jetzt true ist und sofort ohne weitere Arbeit fortgesetzt wird.

Der Hauptthread besteht heute aus vielen winzigen Teilen, Interaktionen sind kurz und Arbeit dauert nur so lange, wie es nötig ist.

18. Fazit

Wenn Sie alle langen Aufgaben aufteilen, kann eine Website auf neue Interaktionen reagieren. Auf diese Weise können Sie schnell erstes Feedback geben und Entscheidungen treffen, z. B. können Sie laufende Arbeiten abbrechen. Manchmal bedeutet dies, Einstiegspunkte als separate Aufgaben zu planen. Manchmal bedeutet das, „Ertrag“ hinzuzufügen, Punkte, wenn es praktisch ist.

Wichtig

  • INP misst alle Interaktionen.
  • Jede Interaktion wird von der Eingabe bis zum nächsten Paint gemessen – also der Art und Weise, wie der Nutzer die Reaktionsfähigkeit sieht.
  • Alle davon beeinflussen die Reaktionszeit der Eingabe, der Verarbeitungsdauer von Ereignissen und der Präsentationsverzögerung.
  • Mit den Entwicklertools kannst du INP- und Interaktionsaufschlüsselungen ganz einfach messen.

Strategien

  • Vermeiden Sie Code mit langer Ausführungszeit (lange Aufgaben) auf Ihren Seiten.
  • Verschieben Sie unnötigen Code bis zum nächsten Paint aus den Ereignis-Listenern.
  • Achte darauf, dass das Rendering-Update selbst für den Browser effizient ist.

Weitere Informationen