Informationen zu diesem Codelab
1. Einführung
Eine interaktive Demo und ein Codelab zum Messwert Interaction to Next Paint (INP).
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
.
- Klonen Sie das Repository in Ihrem Terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- Wechseln Sie in das geklonte Verzeichnis:
cd web-vitals-codelabs/understanding-inp
- Abhängigkeiten installieren:
npm ci
- Starten Sie den Webserver:
npm run start
- 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.
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.
Erfassen Sie als Nächstes eine Interaktion im Bereich „Leistung“.
- Tippen Sie auf die Schaltfläche zum Aufzeichnen.
- Interagieren Sie mit der Seite (drücken Sie die Schaltfläche Increment).
- Beenden Sie die Aufnahme.
Auf der resultierenden Zeitachse finden Sie den Track Interaktionen. Klicken Sie links auf das Dreieck, um den Bereich zu maximieren.
Es werden zwei Interaktionen angezeigt. Zoomen Sie auf das zweite Bild heran, indem Sie scrollen oder die W-Taste gedrückt halten.
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.
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.
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.
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.
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.
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.
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
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.
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:
- Surfen Sie wie gewohnt im Web.
- Behalten Sie das Interaktionsprotokoll in der Ansicht mit Live-Messwerten des DevTools-Bereichs „Leistung“ im Blick.
- 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);
});
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.
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
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.
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 setTimeout
s 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.
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.