Общо показвания

юли 27, 2015

Request Animation Frame for high performance applications

Disclaimer: This is a recent document I wrote for the technical blog run by a SaaS company operating in the USA. I got the permission to post here as well and this is what I am doing. 
Disclaimer 2: The code is based on the implementation of the same approach as found in the open source closure library.



As per recent publication on optimizing the drawing operations on a canvas element one of the recommendations is to use a single RAF instance and put your callbacks that should be working right before the next drawing to be done by the browser.

In a less demanding application a simplistic solution based on a queue should be sufficient to bundle the callbacks to run when the browser is ready to draw, this however does not solves the main bottleneck of the RAF approach in the first place: the callbacks might be causing a lot of layout requests and trashing without knowing about each other's internals and thus make the frame (the pre-drawing execution) much slower than it needs to be.

Another possible problem with the default approach (i.e. calling

function myCallback {
// potentially close over the variables in the current scope
requestAnimationFrame(myCallback);
}

inside the body of the callback) is the creation of closures on each execution. When multiple such occurrences are live in the application this might end up creating too much memory allocations every N-th frame and thus promoting the garbage collector run. This is a potential problem because several generations of callbacks have already been passed and the result is that generative garbage collection might not be effective enough and your application might end up spending > 40ms for middle sized apps in GC phase while inside a RAF callback. As expected this leads to a delay in the call for the next frame and user perceives this as frame being dropped.

To mitigate those pitfalls we can devise an approach that has the following characteristics:

  • make it easy to schedule work for the next frame
  • make it easy to only do work once per animation frame, even if more than one event fires that triggers the same work
  • make it easy to separate and do work in two phases to avoid repeated style recalculations caused by interleaved reads and writes
  • avoid closures creation per schedule operation

To achieve this we need to be able to:
  • create the callback only once and call it without using closures
  • create the callback in such a way as to allow ordered execution in two contexts: read from DOM and write to DOM for each callback

Because the callback is now separated in two phases we also need an object to represent 'read' state and to be passed to the 'write' state. For example we want to read the offsetHeight of an element and we want to perform some calculations with it. After that we might need to update the style / positioning of an element based on the performed calculations.

As already known the fastest way to perform a task is to not perform it at all. Same principle applies to animating with RAF: if you can avoid a job - avoid it. An extension to this principle is to use 'pre-calculation' - that is to use the time between the triggering of a new animated interaction and actually start the animation to pre calculate anything you might need as values while the animation is running. An example for this is pre calculating dragging thresholds that might trigger an action: instead of calculating the potential next threshold while performing the dragging you can pre-calculate all thresholds in the visible area and use them to compare values while animating the object the user is dragging.  This will also avoid more memory allocations while animation is being run. For example if you create an array of threshold values it is better than to create a new value after each threshold. Basically think of this code path as critical and make the code as static as possible: pre-allocate all memory you might need (i.e. new array with the exact length you expect to use in any calculation involved), pre-generate the actual animation code.

Now lets take a look at one possible implementation of such approach: https://gist.github.com/pstjvn/f0197e09381eb346160b

What we did is define a single function that will be available in the global scope and can be used to create callbacks for event handling that can be used as regular handlers for events and still work in sync with the browser's drawing operations.

Lets see an example:

var el = document.body;

var task = window.createTask({
 measure: function(ts, state) {
   // Record if the document is scrolled and how much.
   state.scrollTop = this.scrollTop;
 },
 mutate: function(ts, state) {
   if (state.scrollTop == 0) {
     // remove header shadow
     el.classList.remove('scrolled');
   } else {
     el.classList.add('scrolled');
     // add header shadow to indicate that there is scrolled content
   }
 }
}, el);

// Will ignore parameters passes.
el.addEventListener('scroll', task);


While contrived, this example demonstrates the power of this approach: a single function is created once for each RAF tasks and is reused as a regular event handler. Internally the work is synchronized with the browser drawing scheduler and layout trashing is prevented assuming you avoid mixing the measure and mutate operations and correctly separate them in the corresponding functions. Existing implementation do check your calls for correct use of only measuring in measure phase and only mutations in mutation phase, but ultimately it is developer's responsibility to use the tool as per its design.

What can be improved in this example? One might add new property to the state that keeps the last state and only assign classes when there is actually change. In this case this is neglectable as we know that modern browsers do avoid re-layouting when 'classList' is used and no change is detected, but might be a potential gain in other use cases.

An optional improvement in the implementation is to allow the creation of the task to also accept an instance that has a certain shape and thus avoid garbage while restructuring the state in the animations. For example:

function MyState() {
 this.scrollTop = 0;
 this.wasZeroPreviousTime = false;
}

Now one can create state instance when creating the task and have completely static task representation with state.


Conclusion


When developing large and highly interactive web application with JavaScript one might often be tempted to take the short road and write code in an intuitive way in order to accomplish programming tasks faster. JavaScript is notorious for allowing the developers enough flexibility to do just that. However flexibility often comes at a cost and while the code is valid and runs fast on your desktop computer, one need to consider the implications in mobile devices.

Finally, if you already have an application and you see performance penalties instead of blindly rewriting your code always measure and deduct where the potential for improvement is and only then start refactoring.

The approach presented in this article is a tool and not a complete solution for all your intensive animations, but is a good one and should be considered when applicable.