Animacje – setInterval() vs requestAnimationFrame()
Tym razem zajmiemy się animacjami w JS. Porównamy sobie rozwiązania wykorzystujące setInterval() oraz requestAnimationFrame(). Postaram się przedstawić zalety tego drugiego rozwiązania i wyjaśnić na jakie zasadzie ono działa.
Na początek przyjrzyjmy się prostemu kodowi, który jakiś czas temu publikowałem we wpisie slideUp w Vanilla JS
/** * Funkcja, która zmniejsza wysokość elementu HTML wprost proporcjonalnie do zadanego czasu. * * Pierwszy parametr to element, którego wielkość będzie zmniejszana. * Drugi parametr to czas w milisekundach w jakim element osiągnie wysokość zero */ function slideUp(item, time) { //proste sprawdzenie czy to element HTML //należałoby to zrobić dokładniej, ale niepotrzebnie zaciemni to rozwiązanie if(typeof item === 'object' && typeof item.clientHeight !== 'undefined') { var height = item.clientHeight; // można też użyć .offsetHeight //określamy jak dokładna ma być animacja var intervalTime = 5; //określamy co ile ma być zmieniana wysokość var step = (height * intervalTime) / time; var id = setInterval(function(){ height = height - step; //sprawdzam czy animacja się nie zakończyła if(height > 0) { //nadajemy nową wysokość item.style.height = height+'px'; } else { //ukrywamy element item.style.display = 'none'; //usuwamy pętlę clearInterval(id); } console.log('interval', height); }, intervalTime); } else { console.log('Błąd! Nie mogę wykonać operacji na tym elemencie!'); } }
W powyższym przykładzie animacja będzie wykonywana co 5 milisekund czyli 200fps (ang. frames per second). Przeciętny monitor posiada odświeżanie na poziomie 60MHz (odświeża obraz 60 razy na sekundę) co oznacza, że co najmniej 60% klatek będzie nie zauważalne dla użytkownika. Należałoby zmniejszyć odstęp między animacjami do około 60fps czyli wykonanie ponawiamy co około 17ms (1000ms / 60 = 16.6666).
Zalety requestAnimationFrame()
Jak wyżej zaprezentowałem możemy dostosować (w dużym uproszczeniu) setInterval() do odpowiedniej ilości klatek na sekundę. To nie spowoduje, że animacja będzie mniej płynna, a będzie to na pewno z korzyścią dla zużycia zasobów. Pamiętajmy jednak, że w setInterval() możemy jedynie optymalizować pod względem ilości klatek na sekundę ustawiając czas, co ile ma być wywoływana funkcja.
Natomiast requestAnimationFrame() nie tylko dostosowuje ilość wyświetlanych klatek na sekundę do 60fps (wg. rekomendacji W3C powinno to być zależne od odświeżania monitora) to jeszcze dodatkowo przeglądarka może zoptymalizować tą animację (np. gdy element jest nie widoczny to nie wykonuje ponownego odświeżenia).
Ujmując prościej zawsze gdzie mamy do czynienia z animacją (ponownym renderowaniu elementu na scenie) to zawsze powinniśmy użyć requestAnimationFrame(). Obecnie praktycznie wszystkie nowoczesne przeglądarki wspierają to rozwiązanie – możemy to sprawdzić w caniuse.com.
Implementacja requestAnimationFrame()
Spróbuję teraz zmodyfikować poprzednie rozwiązanie wykorzystując requestAnimationFrame():
/** * Funkcja, która zmniejsza wysokość elementu HTML wprost proporcjonalnie do zadanego czasu. * * Pierwszy parametr to element, którego wielkość będzie zmniejszana. * Drugi parametr to czas w milisekundach w jakim element osiągnie wysokość zero */ function slideUp(item, time) { // proste sprawdzenie czy to element HTML // należałoby to zrobić dokładniej, ale niepotrzebnie zaciemni to rozwiązanie if(typeof item === 'object' && typeof item.clientHeight !== 'undefined') { var height = item.clientHeight; // można też użyć .offsetHeight // określamy jak dokładna ma być animacja // przyjmujemy, że jest to 60fps var intervalTime = 17; //określamy co ile ma być zmieniana wysokość var step = (height * intervalTime) / time; // opakowaliśmy wszystko to co było w funkcji anonimowej // w funkcję callback, ktora jest przekazyna do requestAnimationFrame function callback(){ height = height - step; // sprawdzam czy animacja się nie zakończyła if(height > 0) { // nadajemy nową wysokość item.style.height = height+'px'; // ustawiam kolejną klatkę requestAnimationFrame(callback); } else { // ukrywamy element item.style.display = 'none'; } }; // wywołujemy funkcję po raz pierwszy callback(); } else { console.log('Błąd! Nie mogę wykonać operacji na tym elemencie!'); } }
Wymuszenie fps w requestAnimationFrame()
Przy naszej implementacji pojawia się jeden problem. Uznaliśmy, że ilość klatek na sekundę to 60. Co się stanie, gdy przeglądarka postanowi renderować element z inną wartości? Choćby dlatego, że użytkownik ma lepszy monitor, a jak za pewne pamiętasz rekomendacja W3C określa, że renderowanie elementu powinno być zgodne z odświeżaniem monitora.
Aby ograniczyć fps do np. 25 klatek na sekundę wystarczy wprowadzić drobną modyfikację:
/** * Funkcja, która zmniejsza wysokość elementu HTML wprost proporcjonalnie do zadanego czasu. * * Pierwszy parametr to element, którego wielkość będzie zmniejszana. * Drugi parametr to czas w milisekundach w jakim element osiągnie wysokość zero */ function slideUp(item, time) { // proste sprawdzenie czy to element HTML // należałoby to zrobić dokładniej, ale niepotrzebnie zaciemni to rozwiązanie if(typeof item === 'object' && typeof item.clientHeight !== 'undefined') { var height = item.clientHeight; // można też użyć .offsetHeight // określamy jak dokładna ma być animacja // przyjmujemy, że jest to 25fps var intervalTime = 40; //określamy co ile ma być zmieniana wysokość var step = (height * intervalTime) / time; // opakowaliśmy wszystko to co było w funkcji anonimowej // w funkcję callback, ktora jest przekazyna do requestAnimationFrame function callback(){ // ustawiamy timeout z czasem zgodnym z fps setTimeout(function(){ height = height - step; // sprawdzam czy animacja się nie zakończyła if(height > 0) { // nadajemy nową wysokość item.style.height = height+'px'; // ustawiam kolejną klatkę requestAnimationFrame(callback); } else { // ukrywamy element item.style.display = 'none'; } }, intervalTime); }; // wywołujemy funkcję po raz pierwszy callback(); } else { console.log('Błąd! Nie mogę wykonać operacji na tym elemencie!'); } }