API requestAnimationFrame - agora com precisão de menos de um milissegundo

Ilmari Heikkinen

Se você estiver usando a requestAnimationFrame, gostou de ver as pinturas sincronizadas com a taxa de atualização da tela, resultando em animações de maior fidelidade possível. Além disso, você economiza o ruído do ventilador da CPU e a energia da bateria quando eles alternam para outra guia.

No entanto, há uma mudança prestes a acontecer em parte da API. O carimbo de data/hora que é transmitido para sua função de callback muda de um carimbo de data/hora típico do tipo Date.now() para uma medição de alta resolução de milissegundos de ponto flutuante desde que a página foi aberta. Se você usar esse valor, precisará atualizar seu código, com base na explicação abaixo.

Só para esclarecer, isto é do que estou falando:

// assuming requestAnimationFrame method has been normalized for all vendor prefixes..
requestAnimationFrame(function(timestamp){
    // the value of timestamp is changing
});

Se você usa o paliativo requestAnimFrame comum fornecido aqui, você não está usando o valor do carimbo de data/hora. Você está fora do gancho. :)

Por que

Por quê? O rAF ajuda você a atingir o máximo ideal de 60 QPS, e 60 QPS equivale a 16,7 ms por quadro. Mas medir com milissegundos inteiros significa que temos uma precisão de 1/16 para tudo o que queremos observar e segmentar.

Comparação do gráfico de 16 ms x 16 ms inteiros.

Como você pode ver acima, a barra azul representa a quantidade máxima de tempo que você tem que fazer todo o seu trabalho antes de pintar um novo quadro (a 60 fps). Provavelmente, você está fazendo mais de 16 coisas, mas, com milissegundos de número inteiro, só é possível programar e medir nesses incrementos enormes. Não é o suficiente.

O Temporizador de alta resolução resolve isso fornecendo um valor muito mais preciso:

Date.now()         //  1337376068250
performance.now()  //  20303.427000007

No momento, o timer de alta resolução está disponível no Chrome como window.performance.webkitNow(), e esse valor geralmente é igual ao novo valor do argumento transmitido ao callback rAF. Depois que a especificação avançar nos padrões, o método descartará o prefixo e estará disponível pelo performance.now().

Você também notará que os dois valores acima têm muitas ordens de magnitude diferentes. performance.now() é uma medida de milissegundos de ponto flutuante desde que determinada página começou a ser carregada (para ser específico, o performance.navigationStart).

Em uso

O principal problema do corte são as bibliotecas de animação que usam esse padrão de design:

function MyAnimation(duration) {
    this.startTime = Date.now();
    this.duration = duration;
    requestAnimFrame(this.tick.bind(this));
}
MyAnimation.prototype.tick = function(time) {
    var now = Date.now();
    if (time > now) {
        this.dispatchEvent("ended");
        return;
    }
    ...
    requestAnimFrame(this.tick.bind(this));
}

Uma edição para corrigir isso é muito fácil... aumente o startTime e o now para usar window.performance.now().

this.startTime = window.performance.now ?
                    (performance.now() + performance.timing.navigationStart) :
                    Date.now();

Essa é uma implementação bastante simples, que não usa um método now() prefixado e também pressupõe suporte a Date.now(), que não está no IE8.

Detecção de recursos

Se você não está usando o padrão acima e só quer identificar que tipo de valor de callback está recebendo, use esta técnica:

requestAnimationFrame(function(timestamp){

    if (timestamp < 1e12){
        // .. high resolution timer
    } else {
        // integer milliseconds since unix epoch
    }

    // ...

Verificar if (timestamp < 1e12) é um teste rápido de pato para ver o tamanho de um número com que estamos lidando. Tecnicamente, poderia ser um falso positivo, mas somente se uma página da Web ficar aberta continuamente por 30 anos. No entanto, não é possível testar se ele é um número de ponto flutuante (em vez de um número inteiro). Peça timers de alta resolução suficientes para receber valores inteiros em algum momento.

Planejamos implementar essa mudança no Chrome 21. Por isso, se você já estiver usando esse parâmetro de callback, atualize seu código.