Zeitmessung langer Animationsframes
Lange Animationsframes (Long Animation Frames, LoAFs) können die Nutzung einer Website beeinträchtigen. Sie können Aktualisierungen der Benutzeroberfläche (UI) verzögern. Dadurch wirken Bedienelemente möglicherweise nicht reaktionsfähig, und Animationen sowie das Scrollen können ruckeln. Die Long Animation Frames API ermöglicht es Entwicklern, Informationen über lange Animationsframes abzurufen und deren Ursachen besser zu verstehen. Dieser Artikel zeigt, wie Sie die Long Animation Frames API verwenden.
Was ist ein langer Animationsframe?
Ein langer Animationsframe – kurz LoAF – ist eine Rendering-Aktualisierung, die sich um mehr als 50 ms verzögert.
Eine gute Reaktionsfähigkeit bedeutet, dass eine Seite schnell auf Interaktionen reagiert. Dazu müssen die für Benutzer erforderlichen Aktualisierungen zeitnah dargestellt werden. Außerdem darf nichts diese Aktualisierungen blockieren. Die Google-Metrik Interaction to Next Paint (INP) empfiehlt beispielsweise, dass eine Website innerhalb von 200 ms auf Seiteninteraktionen wie Klicks oder Tastendrücke reagieren sollte.
Für flüssige Animationen müssen Aktualisierungen schnell erfolgen: Damit eine Animation mit 60 Bildern pro Sekunde flüssig läuft, sollte jeder Animationsframe innerhalb von etwa 16 ms (1000/60) gerendert werden.
Lange Animationsframes beobachten
Um Informationen über LoAFs zu erhalten und deren Verursacher zu ermitteln, können Sie mit einem regulären PerformanceObserver Einträge der Performance-Timeline beobachten, deren entryType "long-animation-frame" ist:
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
observer.observe({ type: "long-animation-frame", buffered: true });
Frühere lange Animationsframes können auch mit einer Methode wie Performance.getEntriesByType() abgefragt werden:
const loafs = performance.getEntriesByType("long-animation-frame");
Beachten Sie jedoch, dass der Puffer für Einträge des Typs "long-animation-frame" höchstens 200 Einträge fasst. Danach werden neue Einträge verworfen. Daher wird die Verwendung von PerformanceObserver empfohlen.
"long-animation-frame"-Einträge untersuchen
Einträge der Performance-Timeline vom Typ "long-animation-frame" werden durch PerformanceLongAnimationFrameTiming-Objekte dargestellt. Die Eigenschaft scripts eines solchen Objekts enthält ein Array von PerformanceScriptTiming-Objekten. Jedes davon enthält Informationen über ein Skript, das zum langen Animationsframe beigetragen hat.
Das folgende Beispiel zeigt einen vollständigen Performance-Eintrag vom Typ "long-animation-frame" mit einem einzelnen Skript:
({
blockingDuration: 0,
duration: 60,
entryType: "long-animation-frame",
firstUIEventTimestamp: 11801.099999999627,
name: "long-animation-frame",
paintTime: 11862.400000000373,
presentationTime: 11863.199999999255,
renderStart: 11858.800000000745,
scripts: [
{
duration: 45,
entryType: "script",
executionStart: 11803.199999999255,
forcedStyleAndLayoutDuration: 0,
invoker: "DOMWindow.onclick",
invokerType: "event-listener",
name: "script",
pauseDuration: 0,
sourceURL: "https://web.dev/js/index-ffde4443.js",
sourceFunctionName: "myClickHandler",
sourceCharPosition: 17796,
startTime: 11803.199999999255,
window: {
// …Window object…
},
windowAttribution: "self",
},
],
startTime: 11802.400000000373,
styleAndLayoutStart: 11858.800000000745,
});
Zusätzlich zu den Standarddaten eines PerformanceEntry-Eintrags enthält er die folgenden wichtigen Angaben:
blockingDuration-
Ein
DOMHighResTimeStamp, der die Gesamtzeit in Millisekunden angibt, während der der Hauptthread daran gehindert war, auf Aufgaben mit hoher Priorität wie Benutzereingaben zu reagieren. Zur Berechnung werden alle langen Tasks innerhalb des LoAF mit einerdurationvon mehr als50msherangezogen. Von jedem wird50msabgezogen, die Rendering-Zeit wird zur Dauer des längsten Tasks addiert und die Ergebnisse werden aufsummiert. firstUIEventTimestamp-
Ein
DOMHighResTimeStamp, der den Zeitpunkt des ersten UI-Ereignisses angibt, das während des aktuellen Animationsframes verarbeitet wird, etwa eines Maus- oder Tastaturereignisses. Dieser Zeitstempel kann vor dem Beginn des Animationsframes liegen, wenn zwischen dem Auftreten und der Verarbeitung des Ereignisses eine Verzögerung lag. paintTime-
Gibt den
timestampzurück, zu dem die Rendering-Phase endete und der Animationsframe begann. presentationTime-
Gibt den
timestampzurück, zu dem die UI-Aktualisierung tatsächlich auf dem Bildschirm dargestellt wurde. renderStart-
Ein
DOMHighResTimeStamp, der den Beginn des Rendering-Zyklus angibt. Dieser umfasst Callbacks vonWindow.requestAnimationFrame(), die Berechnung von Stilen und Layout sowie Callbacks vonResizeObserverundIntersectionObserver. styleAndLayoutStart-
Ein
DOMHighResTimeStamp, der den Beginn der Stil- und Layoutberechnungen für den aktuellen Animationsframe angibt. - Eigenschaften von
PerformanceScriptTiming: -
Eigenschaften, die Informationen über die Skripte liefern, die zum LoAF beigetragen haben:
script.executionStart-
Ein
DOMHighResTimeStamp, der angibt, wann die Kompilierung des Skripts abgeschlossen war und seine Ausführung begann. script.forcedStyleAndLayoutDuration-
Ein
DOMHighResTimeStamp, der die Gesamtzeit in Millisekunden angibt, die das Skript für erzwungene Layout- und Stilberechnungen aufgewendet hat. Unter Layout Thrashing vermeiden erfahren Sie, wodurch dies verursacht wird. script.invokerundscript.invokerType-
Zeichenfolgen, die angeben, wie das Skript aufgerufen wurde (beispielsweise
"IMG#id.onload"oder"Window.requestAnimationFrame") und welchen Typ sein Einstiegspunkt hat (beispielsweise"event-listener"oder"resolve-promise"). script.pauseDuration-
Ein
DOMHighResTimeStamp, der die Gesamtzeit in Millisekunden angibt, die das Skript mit „pausierenden“ synchronen Vorgängen verbracht hat, beispielsweise mit Aufrufen vonWindow.alert()oder synchronenXMLHttpRequest-Anfragen. script.sourceCharPosition,script.sourceFunctionNameundscript.sourceURL-
Werte, die die Zeichenposition im Skript, den Funktionsnamen beziehungsweise die Skript-URL angeben. Beachten Sie, dass der gemeldete Funktionsname zum „Einstiegspunkt“ des Skripts gehört, also zur obersten Ebene des Aufrufstapels, und nicht zu einer bestimmten langsamen Unterfunktion.
Wenn beispielsweise ein Event-Handler eine Funktion auf oberster Ebene aufruft, die wiederum eine langsame Unterfunktion aufruft, geben die
source*-Felder den Namen und die Position der Funktion auf oberster Ebene an, nicht die der langsamen Unterfunktion. Der Grund dafür ist die Performance: Die Ermittlung eines vollständigen Stack-Traces ist aufwendig. script.windowAttributionundscript.window-
Ein Aufzählungswert, der die Beziehung des Containers, in dem das Skript ausgeführt wurde – entweder das Dokument auf oberster Ebene oder ein
<iframe>– zum Dokument auf oberster Ebene beschreibt, sowie eine Referenz auf dessenWindow-Objekt.
Hinweis: Informationen zur Skriptzuordnung werden nur für Skripte bereitgestellt, die im Hauptthread einer Seite ausgeführt werden. Dazu gehören auch
<iframe>-Elemente mit gleichem Ursprung. Für<iframe>-Elemente mit anderem Ursprung, Web Workers, Service Workers und Code von Erweiterungen enthalten lange Animationsframes dagegen keine Skriptzuordnung – selbst wenn dieser Code die Dauer eines Frames beeinflusst.
Zeitstempel zur Berechnung weiterer Zeitwerte verwenden
Anhand der Zeitstempel der Klasse PerformanceLongAnimationFrameTiming lassen sich weitere nützliche Zeitwerte für einen langen Animationsframe berechnen:
| Zeitwert | Berechnung |
|---|---|
| Startzeit | startTime |
| Endzeit | startTime + duration (oder paintTime/presentationTime) |
| Dauer der Verarbeitung | renderStart ? renderStart - startTime : duration |
| Rendering-Dauer | renderStart ? (startTime + duration) - renderStart : 0 |
| Rendering: Dauer vor dem Layout | styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0 |
| Rendering: Stil- und Layoutdauer | styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0 |
Beispiele
>Unterstützung der Long Animation Frames API prüfen
Mit PerformanceObserver.supportedEntryTypes können Sie prüfen, ob die Long Animation Frames API unterstützt wird:
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
// Monitor LoAFs
}
LoAFs oberhalb eines bestimmten Schwellenwerts melden
Der Schwellenwert für LoAFs ist auf 50 ms festgelegt. Wenn Sie mit der Performance-Optimierung beginnen, kann dies zu sehr vielen Meldungen führen. Zunächst können Sie LoAFs daher erst ab einem höheren Schwellenwert melden und diesen schrittweise senken, während Sie die Website verbessern und die problematischsten LoAFs beseitigen. Mit dem folgenden Code können LoAFs oberhalb eines bestimmten Schwellenwerts für weitere Analysen erfasst werden, beispielsweise um sie an einen Analyse-Endpunkt zu senden:
const REPORTING_THRESHOLD_MS = 150;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > REPORTING_THRESHOLD_MS) {
// Example here logs to console; real code could send to analytics endpoint
console.log(entry.paintTime);
console.log(entry.presentationTime);
}
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
Einträge zu langen Animationsframes können recht umfangreich sein. Überlegen Sie deshalb genau, welche Daten aus jedem Eintrag Sie an den Analysedienst senden müssen. Möglicherweise reichen die zusammengefassten Zeitwerte der Einträge und die Skript-URLs bereits aus.
Die längsten Animationsframes beobachten
Um die zu erfassende Datenmenge zu verringern, können Sie nur Daten zu den längsten Animationsframes sammeln, beispielsweise zu den fünf oder zehn längsten. Das lässt sich wie folgt umsetzen:
MAX_LOAFS_TO_CONSIDER = 10;
let longestBlockingLoAFs = [];
const observer = new PerformanceObserver((list) => {
longestBlockingLoAFs = longestBlockingLoAFs
.concat(list.getEntries())
.sort((a, b) => b.blockingDuration - a.blockingDuration)
.slice(0, MAX_LOAFS_TO_CONSIDER);
});
observer.observe({ type: "long-animation-frame", buffered: true });
// Report data on visibilitychange event
document.addEventListener("visibilitychange", () => {
// Example here logs to console; real code could send to analytics endpoint
console.log(longestBlockingLoAFs);
});
Lange Animationsframes mit Interaktionen melden
Eine weitere nützliche Methode besteht darin, die größten LoAF-Einträge zu senden, bei denen während des Frames eine Interaktion stattfand. Dies lässt sich daran erkennen, dass ein Wert für firstUIEventTimestamp vorhanden ist.
Der folgende Code protokolliert alle LoAF-Einträge mit einer Dauer von mehr als 150 ms, bei denen während des Frames eine Interaktion stattfand. Je nach Bedarf können Sie einen höheren oder niedrigeren Wert wählen.
const REPORTING_THRESHOLD_MS = 150;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (
entry.duration > REPORTING_THRESHOLD_MS &&
entry.firstUIEventTimestamp > 0
) {
// Example here logs to console; real code could send to analytics endpoint
console.log(entry);
}
}
});
observer.observe({ type: "long-animation-frame", buffered: true });
Häufige Skriptmuster in langen Animationsframes erkennen
Eine alternative Strategie besteht darin, zu untersuchen, welche Skripte am häufigsten in LoAF-Einträgen vorkommen. Daten können auf Ebene des Skripts und/oder der Zeichenposition gemeldet werden, um die problematischsten Skripte zu identifizieren. Das ist hilfreich, wenn Themes oder Plugins, die Performance-Probleme verursachen, auf mehreren Websites verwendet werden.
Die Ausführungszeiten häufig vorkommender Skripte (oder von Drittanbieter-Ursprüngen) in LoAFs können aufsummiert und gemeldet werden. So lassen sich häufige Ursachen für LoAFs auf einer Website oder über mehrere Websites hinweg ermitteln.
Um beispielsweise Skripte nach URL zu gruppieren und die Gesamtdauer anzuzeigen:
const observer = new PerformanceObserver((list) => {
const allScripts = list.getEntries().flatMap((entry) => entry.scripts);
const scriptSource = [
...new Set(allScripts.map((script) => script.sourceURL)),
];
const scriptsBySource = scriptSource.map((sourceURL) => [
sourceURL,
allScripts.filter((script) => script.sourceURL === sourceURL),
]);
const processedScripts = scriptsBySource.map(([sourceURL, scripts]) => ({
sourceURL,
count: scripts.length,
totalDuration: scripts.reduce(
(subtotal, script) => subtotal + script.duration,
0,
),
}));
processedScripts.sort((a, b) => b.totalDuration - a.totalDuration);
// Example here logs to console; real code could send to analytics endpoint
console.table(processedScripts);
});
observer.observe({ type: "long-animation-frame", buffered: true });
Vergleich mit der Long Tasks API
Der Long Animation Frames API ging die Long Tasks API voraus (siehe PerformanceLongTaskTiming). Beide APIs haben einen ähnlichen Zweck und werden ähnlich verwendet: Sie stellen Informationen über lange Tasks bereit, die den Hauptthread mindestens 50 ms lang blockieren.
Es ist sinnvoll, die Anzahl langer Tasks auf Ihrer Website zu verringern, da diese die Reaktionsfähigkeit beeinträchtigen können. Klickt ein Benutzer beispielsweise auf eine Schaltfläche, während der Hauptthread einen langen Task verarbeitet, verzögert sich die Reaktion der Benutzeroberfläche auf den Klick, bis der Task abgeschlossen ist. Eine gängige Empfehlung lautet daher, lange Tasks in mehrere kürzere Tasks aufzuteilen, damit wichtige Interaktionen dazwischen verarbeitet werden können.
Die Long Tasks API hat jedoch Einschränkungen:
- Ein Animationsframe kann aus mehreren Tasks bestehen, die jeweils unter dem Schwellenwert von 50 ms liegen, den Hauptthread zusammen aber dennoch blockieren. Die Long Animation Frames API löst dieses Problem, indem sie den Animationsframe als Ganzes betrachtet.
- Der Eintragstyp
PerformanceLongTaskTimingliefert weniger Informationen alsPerformanceLongAnimationFrameTiming. Er kann beispielsweise angeben, in welchem Container ein langer Task ausgeführt wurde, nicht aber, welches Skript oder welche Funktion ihn verursacht hat. - Die Long Tasks API liefert ein unvollständiges Bild, da sie einige wichtige Tasks möglicherweise nicht erfasst. Manche Aktualisierungen, etwa das Rendering, finden in separaten Tasks statt. Um den „Gesamtaufwand“ einer Interaktion genau zu messen, sollten sie idealerweise zusammen mit der vorangegangenen Ausführung erfasst werden, die die Aktualisierung ausgelöst hat.
Siehe auch
- Lange Tasks optimieren auf web.dev (2024)
- Wo die Long Tasks API an ihre Grenzen stößt, Erläuterung zur Long Animation Frames API (2024)