Schlankere, gemeinere, schnellere Animationen mit requestAnimationFrame

Es ist eine gute Wette, dass Sie in Ihrer Zeit als Entwickler einige Animationsarbeiten durchgeführt haben, egal ob es sich um kleinere UI-Effekte oder große iteraktive Canvas-Teile handelt. Die Chancen sind Sie auch über gekommen sind requestAnimationFrame, oder rAF (wir sagen , es raff diese Teile herum), und hoffentlich haben Sie eine Chance hatte , es in Ihren Projekten zu verwenden. Falls Sie es nicht wissen, requestAnimationFrameist dies die native Methode des Browsers, um Ihre Animationen zu verarbeiten. Da rAF speziell für Animation und Rendering entwickelt wurde, kann der Browser dies zum am besten geeigneten Zeitpunkt planen. Wenn wir unsere Karten richtig spielen, erhalten wir butterweiche 60 Bilder pro Sekunde.

In diesem Artikel möchten wir einige zusätzliche Möglichkeiten skizzieren, um sicherzustellen, dass Sie den maximalen Nutzen aus Ihrem Animationscode ziehen. Selbst wenn Sie es verwenden, requestAnimationFramegibt es andere Möglichkeiten, wie Sie zu Engpässen in Ihren Animationen führen können. Bei 60 Bildern pro Sekunde hat jedes Bild, das Sie zeichnen, 16,67 ms, um alles zu erledigen. Das ist nicht viel, also zählt jede Optimierung!

TL; DR Entkoppeln Sie Ihre Ereignisse von Animationen. Vermeiden Sie Animationen, die zu Reflow-Repaint-Schleifen führen. Aktualisieren Sie Ihre rAF-Aufrufe, um einen hochauflösenden Zeitstempel als ersten Parameter zu erwarten. Rufen Sie rAF nur an, wenn Sie visuelle Updates durchführen müssen.

Scroun-Ereignisse entprellen

Beim Entprellen wird Ihre Animation von allen Eingaben entkoppelt, die sie betreffen. Nehmen Sie zum Beispiel einen Effekt, der beim Scrollen der Seite ausgelöst wird. Der Effekt überprüft möglicherweise, ob einige DOM-Elemente für den Benutzer sichtbar sind, und wendet dann gegebenenfalls einige CSS-Klassen auf diese Elemente an.

Oder Sie codieren einen Parallaxen-Bildlaufeffekt, bei dem Hintergrundbilder beim Scrollen ihre Position relativ zur Bildlaufposition der Seite ändern. Ich werde mich für die erstere der beiden gebräuchlichen Verwendungen entscheiden, und der allgemeine Kern unseres Codes könnte sein:


function onScroll() {
    update();
}

function update() {

    // assume domElements has been declared
	// by this point :)
	for(var i = 0; i < domElements.length; i++) {

		// read offset of DOM elements
		// to determine visibility - a reflow

		// then apply some CSS classes
		// to the visible items	- a repaint

	}
}

window.addEventListener('scroll', onScroll, false);

Das Hauptproblem hierbei ist, dass wir einen Reflow auslösen und neu malen, wenn ein Bildlaufereignis auftritt: Wir bitten den Browser, die tatsächlichen Positionen von DOM-Elementen neu zu berechnen , eine teure Reflow-Operation, und wenden dann einige CSS-Klassen an, die den Browser verursachen neu streichen. Am Ende ping-ponging zwischen Reflow und Neulackieren, was die Leistung Ihrer App beeinträchtigen wird. Wir wählen hier Bildlaufereignisse aus, aber das Gleiche gilt für die Größenänderung von Ereignissen. Tatsächlich kann jedes Ereignis, das Sie auf diese Weise nutzen, Leistungsprobleme verursachen. Lesen Sie den Fastersite-Blogbeitrag von Tony Gentilcore, um eine Aufschlüsselung der Eigenschaften zu erhalten, die einen Reflow in WebKit verursachen .

Jetzt müssen wir das Bildlaufereignis von der updateFunktion entkoppeln , und genau hier können Sie requestAnimationFramehelfen. Wir müssen die Dinge ändern, damit wir unsere Bildlaufereignisse abhören können, aber wir werden nur den neuesten Wert speichern:

var latestKnownScrollY = 0;

function onScroll() {
	latestKnownScrollY = window.scrollY;
}

Jetzt sind wir an einem besseren Ort: onScrollWird ausgeführt, wenn der Browser dies ausführt, aber wir speichern nur die Bildlaufposition des Fensters. Dieser Code kann einmal, zwanzig oder hundert Mal ausgeführt werden, bevor wir versuchen, den Wert in unserer Animation zu verwenden, und es spielt keine Rolle. Der Punkt ist, dass wir den Wert verfolgen, ihn aber nicht verwenden, um potenziell unnötige Draw-Aufrufe auszulösen. Wenn Ihr Draw Call teuer ist, profitieren Sie wirklich davon, diese zusätzlichen Anrufe zu vermeiden.

Der andere Teil dieser Änderung besteht darin requestAnimationFrame, die visuellen Aktualisierungen zum für den Browser günstigsten Zeitpunkt durchzuführen:

var latestKnownScrollY = 0;

function onScroll() {
	latestKnownScrollY = window.scrollY;
}

Jetzt ziehen wir nur den neuesten Wert ab, lastKnownScrollYwenn wir ihn brauchen, und verwerfen alles andere. Wenn Sie alle Ereigniswerte seit der letzten Ziehung erfassen müssen, können Sie ein Array verwenden und alle erfassten Werte onScrolldarauf übertragen. Wenn die Zeit für das Zeichnen gekommen ist, können Sie die Werte mitteln oder das tun, was am besten geeignet ist. In diesem Fall halten wir es einfach und verfolgen nur den zuletzt erfassten Wert.

Was können wir sonst noch tun? Zum einen laufen wir ständig requestAnimationFrameund das ist nicht notwendig, wenn wir nicht nur gescrollt haben, da sich nichts geändert hat. Um dies zu beheben, haben wir den onScroll initiiert requestAnimationFrame:

var latestKnownScrollY = 0,
	ticking = false;

function onScroll() {
	latestKnownScrollY = window.scrollY;
	requestTick();
}

function requestTick() {
	if(!ticking) {
		requestAnimationFrame(update);
	}
	ticking = true;
}

Wenn wir jetzt scrollen, werden wir versuchen, anzurufen requestAnimationFrame, aber wenn einer bereits angefordert wird, initiieren wir keinen anderen . Dies ist eine wichtige Optimierung, da der Browser alle wiederholten rAF-Anforderungen stapelt und wir zu einer Situation zurückkehren würden, in der mehr Anrufe getätigt werden, updateals wir benötigen.

Dank dieses Setups müssen wir nicht mehr requestAnimationFrameoben im Update aufrufen, da wir wissen, dass es nur angefordert wird, wenn ein oder mehrere Bildlaufereignisse stattgefunden haben. Wir brauchen auch den Kick-off-Aufruf unten nicht mehr, also aktualisieren wir entsprechend:

function update() {
	// reset the tick so we can
	// capture the next onScroll
	ticking = false;

	var currentScrollY = latestKnownScrollY;

	// read offset of DOM elements
	// and compare to the currentScrollY value
	// then apply some CSS classes
	// to the visible items
}

// kick off - no longer needed! Woo.
// update();

Hoffentlich können Sie die Vorteile des Entprellens der Animationen in Ihrer App anhand von Bildlauf- oder Größenänderungsereignissen erkennen, die diese beeinflussen. Wenn Sie immer noch Zweifel haben, hat John Resig vor einiger Zeit einen großartigen Artikel darüber geschrieben, wie Twitter von Scroll-Ereignissen beeinflusst wurde. Wäre rAF damals schon da gewesen, wäre die obige Technik wahrscheinlich seine Empfehlung gewesen.

Mausereignisse entprellen

Wir haben einen allgemeinen Anwendungsfall für die Verwendung von rAF zum Entkoppeln von Animationen von Bildlauf- und Größenänderungsereignissen durchlaufen. Lassen Sie uns nun über einen anderen sprechen: Verwenden Sie ihn, um mit Interaktionen umzugehen. In diesem Fall bleibt etwas an der aktuellen Mausposition hängen, aber nur, wenn die Maustaste gedrückt wird. Wenn es veröffentlicht ist, stoppen wir die Animation.

Lassen Sie uns direkt in den Code springen, dann werden wir ihn auseinander nehmen:

var mouseIsDown = false,
	lastMousePosition = { x: 0, y: 0 };

function onMouseDown() {
	mouseIsDown = true;
	requestAnimationFrame(update);
}

function onMouseUp() {
	mouseIsDown = false;
}

function onMouseMove(evt) {
	lastMousePosition.x = evt.clientX;
	lastMousePosition.y = evt.clientY;
}

function update() {
	if(mouseIsDown) {
		requestAnimationFrame(update);
	}

	// now draw object at lastMousePosition
}

document.addEventListener('mousedown', onMouseDown, false);
document.addEventListener('mouseup',   onMouseUp,   false);
document.addEventListener('mousemove', onMouseMove, false);

In diesem Fall setzen wir einen Booleschen Wert ( mouseIsDown), je nachdem, ob die Maustaste gerade gedrückt wird oder nicht. Wir können auch auf die mousedownVeranstaltung zurückgreifen , um den ersten requestAnimationFrameAnruf einzuleiten , was praktisch ist. Während wir die Maus bewegen, machen wir einen ähnlichen Trick wie im vorherigen Beispiel, in dem wir einfach die letzte bekannte Position der Maus speichern, die wir später in der updateFunktion verwenden. Das Letzte, was Sie bemerken müssen, ist, dass Sie updateden nächsten Animationsrahmen anfordern, bis wir angerufen haben, onMouseUpund mouseIsDownauf zurückgesetzt werden false.

Auch hier besteht unsere Taktik darin, die Mausereignisse so oft ablaufen zu lassen, wie es der Browser für erforderlich hält, und wir haben die Draw-Aufrufe unabhängig von diesen Ereignissen. Nicht unähnlich zu dem, was wir mit Scroll-Ereignissen machen.

Wenn die Dinge etwas komplexer sind und Sie etwas animieren, das sich nach dem onMouseUpAufruf weiter bewegt , müssen Sie die Anrufe requestAnimationFrameanders verwalten. Eine geeignete Lösung besteht darin, die Position des Animationsobjekts zu verfolgen. Wenn die Änderung in zwei aufeinander folgenden Bildern unter einen bestimmten Schwellenwert fällt, beenden Sie den Anruf requestAnimationFrame. Die Änderungen an unserem Code würden ungefähr so aussehen:

var mouseIsDown = false,
	lastMousePosition = { x: 0, y: 0 },
	rAFIndex = 0;

function onMouseDown() {
	mouseIsDown = true;

	// cancel the existing rAF
	cancelAnimationFrame(rAFIndex);

	rAFIndex = requestAnimationFrame(update);
}

// other event handlers as above

function update() {

	var objectHasMovedEnough = calculateObjectMovement();

	if(objectHasMovedEnough) {
		rAFIndex = requestAnimationFrame(update);
	}

	// now draw object at lastMousePosition
}

function calculateObjectMovement() {

	var hasMovedFarEnough = true;

	// here we would perhaps use velocities
	// and so on to capture the object
	// movement and set hasMovedFarEnough
	return hasMovedFarEnough;
}

Die wichtigste Änderung in der oben kommt von der Tatsache , dass , wenn Sie die Maustaste loslassen die rAF Anrufe weiterhin würde , bis das Objekt in eine Ruhe gekommen ist , aber Sie können beginnen Klicken und Ziehen wieder bedeutet , dass Sie einen zweiten rAF Anruf geplant bekommen würde sowie die Original . Nicht gut. Um dem entgegenzuwirken, müssen wir jeden geplanten requestAnimationFrameAnruf (in onMouseDown) abbrechen, bevor wir einen neuen Anruf tätigen .

requestAnimationFrame und hochauflösende Zeitstempel

Während wir einige Zeit damit verbringen, darüber zu sprechen, ist requestAnimationFramees erwähnenswert, wie Rückrufe behandelt werden. Der an Ihre Rückruffunktion übergebene Parameter ist ein hochauflösender Zeitstempel, der auf einen Bruchteil einer Millisekunde genau ist. Zwei Dinge dazu:

  1. Es ist fantastisch für Ihre Animationen, wenn sie zeitbasiert sind, da sie jetzt wirklich genau sein können
  2. Sie müssen jeden von Ihnen geschriebenen Code aktualisieren, der erwartet, dass ein Objekt oder Element der erste Parameter ist

Den vollständigen Überblick finden Sie unter: requestAnimationFrame API: jetzt mit einer Genauigkeit von weniger als einer Millisekunde

Ein Beispiel

OK, lassen Sie uns diesen Artikel mit einem Beispiel abschließen, damit Sie alles in Aktion sehen können. Es ist leicht erfunden, und wir werden auch einen Bonus-Performance-Killer einsetzen, den wir im Laufe der Zeit reparieren können. Viel zu viel Spaß!

Wir haben ein Dokument mit 800 DOM-Elementen, das wir verschieben, wenn Sie mit der Maus scrollen. Da wir uns mit moderner Webentwicklung auskennen, werden wir CSS-Übergänge verwenden requestAnimationFrame. Wenn wir die Seite nach unten scrollen, bestimmen wir, welche unserer 800 DOM-Elemente sich jetzt über der Mitte des sichtbaren Bereichs des Bildschirms befinden, und verschieben sie durch Hinzufügen einer leftKlasse auf die linke Seite .

Es ist zu bedenken, dass wir eine so große Anzahl von Elementen ausgewählt haben, da wir so Leistungsprobleme leichter erkennen können. Und es gibt einige.

So sieht unser JavaScript aus:

var movers = document.querySelectorAll('.mover');

/**
 * Set everthing up and position all the DOM elements
 * - normally done with the CSS but, hey, there's heaps
 * of them so we're doing it here!
 */
(function init() {

    for(var m = 0; m < movers.length; m++) {
        movers[m].style.top = (m * 10) + 'px';
    }

})();

/**
 * Our animation loop - called by rAF
 */
function update() {

	// grab the latest scroll position
    var scrollY             = window.scrollY,
        mover               = null,
        moverTop            = [],
        halfWindowHeight    = window.innerHeight * 0.5,
        offset              = 0;

	// now loop through each mover div
	// and change its class as we go
    for(var m = 0; m < movers.length; m++) {

        mover       = movers[m];
        moverTop[m] = mover.offsetTop;

        if(scrollY > moverTop[m] - halfWindowHeight) {
            mover.className = 'mover left';
        } else {
            mover.className = 'mover';
        }

    }

	// keep going
    requestAnimationFrame(update);
}

// schedule up the start
window.addEventListener('load', update, false);
Unsere Demoseite vor Performance- und rAF-Optimierungen

Wenn Sie sich die voroptimierte Seite ansehen, werden Sie feststellen, dass es beim Scrollen wirklich schwierig ist, Schritt zu halten, und es gibt eine Reihe von Gründen, warum. Erstens sind wir Brute Force Calling requestAnimationFrame, während wir wirklich nur Änderungen berechnen sollten, wenn wir ein Scroll-Ereignis erhalten. Zweitens rufen wir auf, offsetTopwas einen Reflow verursacht, aber dann wenden wir die classNameÄnderung sofort an und das wird ein Repaint verursachen. Und drittens verwenden wir für unseren Bonus-Performance-Killer classNameeher als classList.

Der Grund für die Verwendung classNameist weniger performant als der classList, der classNamesich immer auf das DOM-Element auswirkt, auch wenn sich der Wert von classNamenicht geändert hat. Durch einfaches Einstellen des Werts lösen wir ein Repaint aus, was sehr teuer sein kann. Durch die Verwendung classListkann der Browser jedoch bei Aktualisierungen viel intelligenter vorgehen und das Element in Ruhe lassen, falls die Liste bereits die Klasse enthält, die Sie hinzufügen ( leftin unserem Fall).

Wenn Sie weitere Informationen zur Verwendung classListund zum neuen und äußerst nützlichen Frame-Breakdown-Modus in den Dev Tools von Chrome wünschen , sollten Sie sich dieses Video von Paul Irish ansehen:

Schauen wir uns also an, wie eine bessere Version davon aussehen würde:

var movers          = document.querySelectorAll('.mover'),
    lastScrollY     = 0,
    ticking         = false;

/**
 * Set everthing up and position all the DOM elements
 * - normally done with the CSS but, hey, there's heaps
 * of them so we're doing it here!
 */
(function init() {

    for(var m = 0; m < movers.length; m++) {
        movers[m].style.top = (m * 10) + 'px';
    }

})();

/**
 * Callback for our scroll event - just
 * keeps track of the last scroll value
 */
function onScroll() {
    lastScrollY = window.scrollY;
    requestTick();
}

/**
 * Calls rAF if it's not already
 * been done already
 */
function requestTick() {
    if(!ticking) {
        requestAnimationFrame(update);
        ticking = true;
    }
}

/**
 * Our animation callback
 */
function update() {
    var mover               = null,
        moverTop            = [],
        halfWindowHeight    = window.innerHeight * 0.5,
        offset              = 0;

	// first loop is going to do all
	// the reflows (since we use offsetTop)
    for(var m = 0; m < movers.length; m++) {

        mover       = movers[m];
        moverTop[m] = mover.offsetTop;
    }

	// second loop is going to go through
	// the movers and add the left class
	// to the elements' classlist
    for(var m = 0; m < movers.length; m++) {
        mover       = movers[m];
        if(lastScrollY > moverTop[m] - halfWindowHeight) {
            mover.classList.add('left');
        } else {
            mover.classList.remove('left');
        }
    }
	// allow further rAFs to be called
    ticking = false;
}
// only listen for scroll events
window.addEventListener('scroll', onScroll, false);
Unsere Demoseite nach Performance- und rAF-Optimierungen

Wenn Sie sich optimierte Version der Demo ansehen, werden Sie viel flüssigere Animationen sehen, wenn Sie auf der Seite nach oben und unten scrollen. Wir haben aufgehört, requestAnimationFramewahllos anzurufen. Wir tun dies jetzt nur beim Scrollen (und stellen sicher, dass nur ein Anruf geplant ist). Wir haben auch die offsetTopEigenschaftssuche in eine Schleife verschoben und die Klassenänderungen in eine zweite Schleife eingefügt, was bedeutet, dass wir das Problem des Reflow-Repaint vermeiden. Wir haben unsere Ereignisse vom Draw Call abgekoppelt, damit sie so oft auftreten können, wie sie möchten, und wir werden nicht unnötig zeichnen. Schließlich wechselten wir haben aus className für classList, was eine massive Leistungsersparnis.

Natürlich gibt es noch andere Dinge, die wir tun können, um dies weiter voranzutreiben, insbesondere nicht alle 800 DOM-Elemente bei jedem Durchgang zu durchlaufen , aber selbst die Änderungen, die wir vorgenommen haben, haben uns große Leistungsverbesserungen gebracht.

Fazit

Es ist wichtig, dass Sie requestAnimationFrameIhre Animationen nicht nur verwenden , sondern auch richtig verwenden. Wie Sie hoffentlich sehen können, ist es recht einfach, versehentlich Engpässe zu verursachen. Wenn Sie jedoch wissen, wie der Browser Ihren Code tatsächlich ausführt, können Sie Probleme schnell beheben. Nehmen Sie sich etwas Zeit mit den Dev Tools von Chrome, insbesondere im Frame-Modus, und sehen Sie, wo Ihre Animationen verbessert werden können.