Performance affects everything. Increased performance — apparent or real — improves user experience. In turn, improved user experience boosts the bottom line.
Several major studies have proven that increases in latency drastically decrease revenue. Bing reported that a 2,000ms increase in latency results in a whopping 2% decrease in revenue-per-user. Similarly, Google found that a 500ms delay causes a 20% drop in traffic.
Thus, at the heart of my journey toward building a performant UI engine, I was simultaneously building a user experience engine. This article serves to contextualize the current web performance landscape, and to dive deeply into the performance optimizations underlying Velocity.js—an animation engine that dramatically improves UI performance and workflow across all browsers and devices.
Before diving into Velocity, let’s answer the pressing question. How is it possible that the browser has secretly had tremendous performance potential for years, but that it’s remained largely untapped by front-end developers?
The short answer: There’s a fundamental lack of web performance awareness among developers. Let’s explore.
Web Performance Landscape
From the perspective of UI design, there’s no shortage of articles extolling the virtues of building mobile-first, responsive sites. By now, developers get it. Conversely, from the perspective of UI performance, most developers will admit that they don’t know what they’re doing. While advocates from Google, Mozilla, and Microsoft have written countless articles on performance best practices, most developers simply aren’t reading them.
Compounding this lack of awareness is the dynamic that, with UI design, artistic skill can be confidently iterated upon throughout years of experience. However, while the principles of performance (data structures, latency, and rendering pipelines) are subject to the same process of iteration, the specifics of their front-end implementations can change regularly. To put it bluntly, performance-minded developers are often held captive to browser quirks and device capabilities. Such a landscape necessitates that developers be astutely aware of the underlying architectural layers of the web (the rendering stack, garbage collection, and networking), so that they can broadly abstract their approach to performance problem solving.
But with the workload developers already have on their plates, the current ethos suggests that it’s unreasonable for the average dev to master this domain. As a reaction to this, the web’s predominant performance advocate, Google’s Ilya Grigorik, recently wrote a point-by-point analysis of the myths surrounding browser and networking performance: High Performance Browser Networking. (Additional web performance resources can be found at the bottom of this article.)
The current web performance landscape is analogous to staying abreast of IE8 quirks—after a while, you throw in the towel and simply raise the cutoff point for your site’s legacy browser support.
The situation is nearly identical on mobile: Developers tell themselves, “Well, devices are getting faster. So throughout the coming months, my site will naturally become more performant as users continue upgrading their devices.”
Unfortunately, the truth is the polar opposite: First, the smartphones that the developing world is adopting fall short of the performance of the iPhones in our pockets—do you really want to forsake building products for the next two billion people coming online? If your gut reaction is, “It’s not my problem,” rest assured that your evil web developer twin is sitting a thousand miles away cackling at the thought of getting to market before you do by putting effort into developing a solution that’ll be blazing fast even on low-powered devices.
The upcoming Firefox OS initiative is poised to bring capable smartphones to hundreds of millions of people. The future is already here. We are not talking in hypotheticals. Ericsson reports that the global smartphone subscriber count will rise from 1.9 billion to 5.9 billion in the next five years — fueled almost exclusively by the developing world.
The second danger of the set-it-and-forget-it mindset to web performance is that developers systematically make the mistake of testing their mobile pages on devices undergoing ideal performance loads. But, try opening up a couple more apps and web pages. Now, re-test your site. Yikes, you’ve just artificially recreated having a relatively “ancient” Android 2.3 device. Plus you’ve stumbled into the heart of our second problem: Browser-based apps are sensitive to device load—CPU, GPU, and memory usage. Add in the variability of device hardware, and you start approaching the reality of mobile performance: You should always develop the fastest site you can, not just a site that works fine on your iPhone.
Performance is complex, and performance matters. That much is clear. But, what can we actually do about it? That’s what I set out to answer over a three month deep-dive into open source development.
Web Animation Landscape
Whereas jQuery—which doubles as the web’s predominant animation tool—began development in 2006, Velocity was built in 2014. As such, it incorporates the latest performance best practices from the ground-up.
$.animate() in order to ease the transition from
Here are the drawbacks of those libraries:
- jQuery’s native
$.animate() is slow and relatively light on UI animation design features—even when paired with jQuery UI.
- GSAP is a full-fledged animation platform with tremendous power. Its features are nearly limitless; it animates anything from the DOM to WebGL. (Velocity, in contrast, is solely focused on being a lightweight tool for drastically improving UI animation performance and workflow.) Whereas GSAP requires a licensing fee for various types of businesses, Velocity is freely open-sourced via the ultra-permissive MIT license.
Velocity drastically outperforms jQuery at all levels of stress, and Transit beginning at medium levels of stress. GSAP performs similarly to Velocity. For head-to-head UI performance comparisons, refer to Velocity’s documentation.
We’re ready to dive into the juicy performance details. How do you make an animation engine fast? Is it micro-optimizations? Nope.
The real bottlenecks in DOM performance are primarily timer creation and DOM manipulation.
Let’s start by analyzing timer creation. Timers are created when
requestAnimationFrame() are used. There are two performance issues with timer creation: 1) too many timers firing at once reduces frame rates due to the browser’s overhead of maintaining them, and 2) improperly marking the time at which your animation begins results in dropped frames.
Velocity’s solution to the first problem is maintaining a single global tick loop that cycles through all active Velocity animations at once. Individual timers are not created for each Velocity animation. In short, Velocity prioritizes scheduling over interruption.
The second problem, marking the time at which an animation begins so that the loop can keep track of how much time has elapsed, is resolved by setting the start time directly inside the first animation tick itself.
The consequence of Velocity setting the start time inside the first animation loop tick and not when the animation is actually triggered is that animations have the potential to start ~16-85ms past their triggered start point. This delay, however, is practically imperceptible and is ultimately irrelevant unless you are, for example, creating a game, which often necessitates time-accurate collision detection.
In summary, with UI animation, smoothness should always be prioritized over time accuracy.
Minimizing DOM Manipulation
Timer optimization is merely a stepping stone to the real performance optimization underlying Velocity – the minimization of DOM manipulation. The core performance principal behind Velocity is, while there’s an upper limit to the frame rate you can deliver (the brain can only perceive so many frames per second), there’s no upper limit to how cleverly you can avoid DOM manipulation.
The DOM, of course, is the hierarchical representation underlying the HTML on a webpage. Naturally, DOM manipulation consists of setting and getting. When you modify the value of a CSS property on an element, you are setting (updating) the DOM. Conversely, when you query the DOM for the current value of an element’s CSS property, you are getting (querying). These DOM actions incur performance overhead. After setting the DOM, the browser has to calculate the effects of your changes. After all, when you change the width of one element, it can trigger a chain reaction resulting in width changes for its parent, sibling, and child elements.
This phenomenon of frame rate reduction resulting from alternating DOM sets and gets is known as “layout thrashing.”
Velocity goes to great lengths to minimize layout thrashing, and DOM manipulation altogether.
First, as the sole developer of Velocity, I take the time to place comments throughout Velocity’s source highlighting every offending line of code that manipulates the DOM. Simply, I sprinkle
/* GET */ and
/* SET */ wherever appropriate. Adhering to this practice allows me to quickly eyeball my code to ensure that a new feature or bug fix does not introduce layout thrashing. I follow the code path and see if a
/* GET */ is followed by a
/* SET */. If so, I either rework the path to batch SETs and GETs together (to minimize the total occurrence of thrashing), or I avoid implementing the feature altogether.
Second, Velocity operates on cached values whenever possible so that the DOM does not have to be re-queried at the start of every animation. For example, Velocity detects when multiple animations are chained together, and reuses the prior Velocity call’s animation end values as start values for the ensuing call. This is a delicate process because operating on stale values must be avoided at all costs, otherwise animations can break down. Velocity addresses this uncertainty by flagging every Velocity-initiated animation call, then subsequently avoiding value caching when it detects that a prior call in the chain wasn’t initiated by Velocity (e.g. jQuery’s
$.fade() functions were injected between Velocity calls).
The third, and final, major DOM minimization technique that Velocity employs is its “vacuum” approach to unit conversion ratios. Unit conversion logic is the code that determines what one percent unit is in terms of pixels. This is necessary when you’re animating an element’s width, for example, to “+25%”—the animation engine must determine what that value is in pixels so that incremental math can be performed using two values of the same unit type. Why pixels in particular? Because the browser returns CSS property values in pixels when queried—regardless of what unit type was used to set the property.
The unit conversion process entails temporarily setting the width and height of a target element to 1% then calculating what an ensuing DOM query returns for that element’s dimensions. The returned values, which will be in pixels, provide the ratio between between 1% and pixels.
Velocity makes three key optimizations during this unit conversion process: First, it caches unit conversion ratios across elements that pass a test determining if they share the same ratios (i.e. they have the same parent element and the same CSS position value). This is crucial when a set of elements is being animated at once.
Second, Velocity skips unit conversion altogether when it’s not needed. For example, when a property’s start value is 0, zero is zero in every unit type—no conversion is necessary.
Third, Velocity uniquely opts for layout thrashing over DOM tree modification. The former technique produces an undesirable amount of alternating DOM gets and sets by momentarily putting the animating element in a virtual CSS property “vacuum” where it’s stripped of CSS properties that can affect dimension calculations (e.g. box-sizing, overflow). Property stripping, followed by temporary value setting and getting, further followed by resetting the element to its initial values, entails several rounds of layout thrashing.
However, a discovery I had during Velocity’s development is that layout thrashing is more performant than the technique that was being employed up until now: uprooting the DOM tree by cloning the animating element, inserting the clone next to the original, performing unit conversion on the clone, then deleting the clone altogether. This method is desirable because it avoids the non-trivial process of creating a CSS vacuum, but it results in a restructuring of the DOM tree (by affecting its hierarchy through the insertion and removal of elements), which ultimately causes greater browser overhead than alternating CSS property values does. (I used jsPerf.com to confirm these results across browsers and devices; as its maintainers will tell you, this is where jsPerf becomes tremendously powerful—when you need to compare true bottleneck situations across many environments.)
Putting It All Together
So, what can you do with all the resulting power underlying Velocity.js?
Both of those demos operate entirely in the DOM. No WebGL. No canvas.
In terms of everyday web design, a recent notable example of Velocity’s UI performance can be found at Everlane.com. Browse around; the experience is incredibly smooth and responsive.
Check out the resources below to learn more.
Web Performance Resources