Lange Animationsrahmen-Timing
Lange Animationsrahmen (LoAFs) können die Benutzererfahrung einer Website beeinträchtigen. Sie können langsame Benutzeroberflächenaktualisierungen verursachen, was zu scheinbar unempfindlichen Kontrollen und ruckelnden (oder nicht glatten) Animationseffekten und Scrollings führt, was wiederum zu Nutzerfrustration führt. Das Long Animation Frames API ermöglicht es Entwicklern, Informationen über lange Animationsrahmen zu erhalten und ihre Ursachen besser zu verstehen. Dieser Artikel zeigt, wie man das Long Animation Frames API verwendet.
Was ist ein langer Animationsrahmen?
Ein langer Animationsrahmen — oder LoAF — ist ein Rendering-Update, dessen Verzögerung über 50ms hinausgeht.
Gute Reaktionsfähigkeit bedeutet, dass eine Seite schnell auf Interaktionen reagiert. Dies beinhaltet, dass notwendige Aktualisierungen durch den Benutzer rechtzeitig gezeichnet werden und alles vermieden wird, was diese Updates blockieren könnte. Zum Beispiel empfiehlt die Google-Metrik Interaction to Next Paint (INP), dass eine Website innerhalb von 200ms auf Seiteninteraktionen (wie Klicks oder Tastendrücke) reagieren sollte.
Für flüssige Animationen müssen Updates schnell sein — damit eine Animation mit einer flüssigen Rate von 60 Bildern pro Sekunde laufen kann, sollte jeder Animationsrahmen innerhalb von etwa 16ms (1000/60) gerendert werden.
Beobachtung von langen Animationsrahmen
Um Informationen über LoAFs zu erhalten und die Übeltäter zu identifizieren, können Sie Performance-Timeline-Einträge mit einem entryType von "long-animation-frame" mithilfe eines Standard-PerformanceObserver beobachten:
const observer = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
observer.observe({ type: "long-animation-frame", buffered: true });
Frühere lange Animationsrahmen können ebenfalls abgefragt werden, indem eine Methode wie Performance.getEntriesByType() verwendet wird:
const loafs = performance.getEntriesByType("long-animation-frame");
Beachten Sie jedoch, dass die maximale Puffergröße für "long-animation-frame"-Eintragstypen 200 beträgt, nach denen neue Einträge verworfen werden, daher wird die Verwendung der PerformanceObserver-Methode empfohlen.
Untersuchung von "long-animation-frame"-Einträgen
Performance-Timeline-Einträge, die mit einem Typ von "long-animation-frame" zurückgegeben werden, werden durch PerformanceLongAnimationFrameTiming-Objekte dargestellt. Dieses Objekt verfügt über eine scripts-Eigenschaft, die ein Array von PerformanceScriptTiming-Objekten enthält, von denen jedes Informationen über ein Skript enthält, das zum langen Animationsrahmen beigetragen hat.
Das folgende Beispiel zeigt einen vollständigen "long-animation-frame"-Performance-Eintrag, der ein einzelnes Skript enthält:
({
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,
});
Neben den standardmäßig von einem PerformanceEntry zurückgegebenen Daten enthält dies die folgenden bemerkenswerten Elemente:
blockingDuration-
Ein
DOMHighResTimeStamp, der die gesamte Dauer in Millisekunden angibt, während der der Main-Thread daran gehindert war, auf Aufgaben mit hoher Priorität, wie Benutzereingaben, zu reagieren. Dies wird berechnet, indem alle langen Aufgaben innerhalb des LoAFs erfasst werden, die einedurationvon mehr als50mshaben,50msvon jeder subtrahiert und die Renderzeit zur längsten Aufgabenzeit addiert wird, und die Ergebnisse summiert werden. firstUIEventTimestamp-
Ein
DOMHighResTimeStamp, der den Zeitpunkt des ersten UI-Ereignisses angibt — wie ein Maus- oder Tastaturereignis —, das während des aktuellen Animationsrahmens verarbeitet wird. Beachten Sie, dass dieser Zeitstempel vor dem Start dieses Animationsrahmens liegen kann, wenn es eine Verzögerung zwischen dem Ereignis und seiner Verarbeitung gab. paintTime-
Gibt den
timestampzurück, als die Renderphase endete und der Animationsrahmen begann. presentationTime-
Gibt den
timestampzurück, als das UI-Update tatsächlich auf dem Bildschirm gezeichnet wurde. renderStart-
Ein
DOMHighResTimeStamp, der die Startzeit des Rendering-Zyklus einschließlich derWindow.requestAnimationFrame()-Callbacks, der Stil- und Layoutberechnung, derResizeObserver-Callbacks und derIntersectionObserver-Callbacks angibt. styleAndLayoutStart-
Ein
DOMHighResTimeStamp, der den Beginn des Zeitraums angibt, der für Stil- und Layoutberechnungen für den aktuellen Animationsrahmen aufgewendet wird. PerformanceScriptTiming-Eigenschaften:-
Eigenschaften, die Informationen über das/die Skript(e) liefern, das/die zum LoAF beigetragen hat/haben:
script.executionStart-
Ein
DOMHighResTimeStamp, der die Zeit angibt, zu der die Skriptkompilierung abgeschlossen und die Ausführung gestartet wurde. script.forcedStyleAndLayoutDuration-
Ein
DOMHighResTimeStamp, der die gesamte Zeit angibt, die vom Skript für erzwungene Layout-/Stilverarbeitung aufgewendet wurde. Siehe Vermeiden Sie Layout-Optimierung, um zu verstehen, was dies verursacht. script.invokerundscript.invokerType-
Zeichenkettenwerte, die angeben, wie das Skript aufgerufen wurde (zum Beispiel
"IMG#id.onload"oder"Window.requestAnimationFrame") und den Skripteintragstyp (zum Beispiel"event-listener"oder"resolve-promise"). script.pauseDuration-
Ein
DOMHighResTimeStamp, der die gesamte Zeit, in Millisekunden, angibt, die das Skript für das "Pausieren" synchroner Operationen aufgewendet hat (zum Beispiel,Window.alert()-Aufrufe oder synchroneXMLHttpRequest-Abfragen). script.sourceCharPosition,script.sourceFunctionName, undscript.sourceURL-
Werte, die die Zeichenposition des Skripts, den Funktionsnamen und die Skript-URL darstellen. Es ist wichtig zu beachten, dass der gemeldete Funktionsname der "Einstiegspunkt" des Skripts sein wird (d.h. das oberste Niveau des Stapels) und nicht eine spezifische langsame Unterfunktion.
Zum Beispiel, wenn ein Ereignishandler eine Top-Level-Funktion aufruft, die wiederum eine langsame Unterfunktion aufruft, melden die
source*-Felder den Namen und Standort der Top-Level-Funktion, nicht der langsamen Unterfunktion. Dies geschieht aus Leistungsgründen — eine vollständige Stack-Trace ist kostspielig. script.windowAttributioneinscript.window-
Ein aufgezählter Wert, der die Beziehung des Containers (d.h. entweder das Top-Level-Dokument oder ein
<iframe>), in dem dieses Skript ausgeführt wurde, zum Top-Level-Dokument beschreibt, und ein Verweis auf dessenWindow-Objekt.
Hinweis: Skriptattribution wird nur für Skripte bereitgestellt, die im Main-Thread einer Seite laufen, auch für gleichoriginäre
<iframe>s. Allerdings werden fremdoriginäre<iframe>s, Web-Arbeiter, Service-Arbeiter, und Erweiterungs-Code in langen Animationsrahmen keine Skriptattribution haben, selbst wenn sie die Dauer eines beeinflussen.
Berechnung von Zeitstempeln
Die in der PerformanceLongAnimationFrameTiming-Klasse bereitgestellten Zeitstempel ermöglichen die Berechnung weiterer nützlicher Zeitangaben für den langen Animationsrahmen:
| Zeitangabe | Berechnung |
|---|---|
| Startzeit | startTime |
| Endzeit | startTime + duration (oder paintTime/presentationTime) |
| Arbeitsdauer | renderStart ? renderStart - startTime : duration |
| Renderdauer | renderStart ? (startTime + duration) - renderStart : 0 |
| Render: Vor-Layout-Dauer | styleAndLayoutStart ? styleAndLayoutStart - renderStart : 0 |
| Render: Stil- und Layoutdauer | styleAndLayoutStart ? (startTime + duration) - styleAndLayoutStart : 0 |
Beispiele
>Erkennung von Long Animation Frames API-Funktion
Sie können testen, ob das Long Animation Frames API unterstützt wird, indem Sie PerformanceObserver.supportedEntryTypes verwenden:
if (PerformanceObserver.supportedEntryTypes.includes("long-animation-frame")) {
// Monitor LoAFs
}
Melden von LoAFs über einem bestimmten Schwellenwert
Während die LoAF-Schwellenwerte auf 50ms festgelegt sind, kann dies zu einem großen Volumen von Berichten führen, wenn Sie mit der Arbeit zur Leistungsoptimierung beginnen. Anfangs möchten Sie möglicherweise LoAFs bei einem höheren Schwellenwert melden und den Schwellenwert schrittweise verringern, wenn Sie die Seite verbessern und die schlimmsten LoAFs entfernen. Der folgende Code könnte verwendet werden, um LoAFs über einem bestimmten Schwellenwert für eine weitere Analyse zu erfassen (zum Beispiel, indem sie an einen Analyse-Endpunkt gesendet werden):
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 });
Lange Animationsrahmeneinträge können recht groß sein; überlegen Sie deshalb sorgfältig, welche Daten von jedem Eintrag an die Analyse gesendet werden sollten. Beispielsweise könnten die zusammenfassenden Zeiten der Einträge und die Skript-URLs ausreichen.
Beobachtung der längsten Animationsrahmen
Vielleicht möchten Sie nur Daten zu den längsten Animationsrahmen sammeln (z. B. die obersten 5 oder 10), um das zu erfassende Datenvolumen zu reduzieren. Dies könnte wie folgt gehandhabt werden:
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);
});
Melden langer Animationsrahmen mit Interaktionen
Eine weitere nützliche Technik besteht darin, die größten LoAF-Einträge zu senden, bei denen während des Rahmens eine Interaktion stattfand, die durch das Vorhandensein eines firstUIEventTimestamp-Werts festgestellt werden kann.
Der folgende Code protokolliert alle LoAF-Einträge über 150ms, bei denen während des Rahmens eine Interaktion stattfand. Sie könnten je nach Bedarf 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 });
Identifizierung gemeinsamer Skriptmuster in langen Animationsrahmen
Eine alternative Strategie ist es, zu betrachten, welche Skripte am häufigsten in LoAF-Einträgen vorkommen. Daten könnten auf der Ebene eines Skripts und/oder einer Zeichenposition gemeldet werden, um die problematischsten Skripte zu identifizieren. Dies ist in Fällen nützlich, in denen Themes oder Plugins, die Leistungsprobleme verursachen, über mehrere Websites hinweg verwendet werden.
Die Ausführungszeiten gemeinsamer Skripte (oder Drittanbieterquellen) in LoAFs könnten summiert und zurückgemeldet werden, um gemeinsame Beiträger zu LoAFs über eine Website oder eine Sammlung von Websites hinweg zu identifizieren.
Zum Beispiel, um 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 dem Long Tasks API
Das Long Animation Frames API wurde vom Long Tasks API (siehe PerformanceLongTaskTiming) vorangestellt. Beide APIs haben einen ähnlichen Zweck und eine ähnliche Verwendung — sie stellen Informationen über lange Aufgaben bereit, die den Main-Thread für 50ms oder mehr blockieren.
Die Reduzierung der Anzahl langer Aufgaben, die auf Ihrer Website auftreten, ist sinnvoll, da lange Aufgaben Reaktionsprobleme verursachen können. Wenn ein Benutzer beispielsweise auf eine Schaltfläche klickt, während der Main-Thread mit einer langen Aufgabe beschäftigt ist, wird die UI-Reaktion auf den Klick erst erfolgen, wenn die lange Aufgabe abgeschlossen ist. Die allgemeine Weisheit besteht darin, lange Aufgaben in mehrere kleinere Aufgaben zu unterteilen, damit wichtige Interaktionen dazwischen gehandhabt werden können.
Das Long Tasks API hat jedoch seine Einschränkungen:
- Ein Animationsrahmen könnte aus mehreren Aufgaben bestehen, die unter die 50ms-Schwelle fallen, aber dennoch zusammen den Main-Thread blockieren. Das Long Animation Frames API löst dies, indem es den Animationsrahmen als Ganzes betrachtet.
- Der [
PerformanceLongTaskTiming]-Eintragstyp bietet begrenztere Informationen als der [PerformanceLongAnimationFrameTiming]-Typ — er kann Ihnen den Container anzeigen, in dem eine lange Aufgabe aufgetreten ist, aber nicht das Skript oder die Funktion, die dies verursacht hat, zum Beispiel. - Das Long Tasks API bietet eine unvollständige Ansicht, da es einige wichtige Aufgaben ausschließen kann. Einige Updates (z.B. Rendering) erfolgen in separaten Aufgaben, die idealerweise zusammen mit der vorangegangenen Ausführung einbezogen werden sollten, die dieses Update verursacht hat, um die "Gesamtarbeit" für diese Interaktion genau zu messen.
Siehe auch
- Optimieren langer Aufgaben auf web.dev (2024)
- Wo lange Aufgaben zu kurz kommen, Long Animation Frames API Erklärer (2024)