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!');
    }    
}

 

 

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

*

*

4 × 2 =