Lange Tasks finden, bevor es die Nutzer tun
Jede Arbeit, die den Main Thread länger als 50 ms blockiert, verzögert den nächsten Klick. So macht man sie sichtbar.
Ein Long Task ist alles, was den Main Thread des Browsers länger als 50 ms beschäftigt. Solange er läuft, kann die Seite nicht reagieren: Ein Klick wartet, ein Tastendruck wartet, und der Nutzer spürt das als Verzögerung. Die meisten langsamen Seiten sind nicht wegen des Netzwerks langsam, sondern wegen einiger weniger langer Tasks im falschen Moment.
Sichtbar machen
Der Browser zeichnet lange Tasks ohnehin auf. Ein PerformanceObserver meldet jeden einzelnen:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`Long Task: ${Math.round(entry.duration)} ms`, entry);
}
});
observer.observe({ type: 'longtask', buffered: true });
buffered: true liefert auch die Tasks, die schon vor dem Anlegen des Observers passiert sind. Man kann ihn also spät registrieren, ohne die teure Arbeit beim Start zu verpassen.
Den Verursacher finden
Ein Long-Task-Eintrag sagt, dass etwas langsam war, aber kaum, was. In Chromium-basierten Browsern hilft die neuere Long Animation Frames API mehr, weil sie die Skripte auflistet, die in jedem langsamen Frame gelaufen sind:
new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
for (const script of frame.scripts) {
console.log(script.invoker, script.sourceURL, Math.round(script.duration));
}
}
}).observe({ type: 'long-animation-frame', buffered: true });
Am aufschlussreichsten ist meist invoker: Das Feld nennt den Event-Listener, Timer oder Promise-Callback, der die Arbeit ausgelöst hat.
Beheben
Selten muss die Arbeit selbst schneller werden. Meist reicht es, sie aufzuteilen und dem Browser zwischendurch Gelegenheit zu geben, Eingaben zu verarbeiten:
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function processAll(items) {
let last = performance.now();
for (const item of items) {
process(item);
if (performance.now() - last > 40) {
await yieldToMain();
last = performance.now();
}
}
}
Die Schleife erledigt so viel, wie in etwa 40 ms passt, und macht dann Platz. scheduler.yield() hat den Vorteil, dass die restliche Arbeit vor anderen wartenden Tasks fortgesetzt wird, sodass der Job trotzdem schnell fertig wird.