Open Sourcing Hulu’s Data-Binding Library for Low Powered Living Room Devices — Introducing Quickdraw.js
By Zachary Cava, Senior Software Developer
The majority of our viewers enjoy sitting back on the couch to watch their favorite TV shows and movies. In fact, 78% of our viewers watch Hulu through their living room devices, such as TVs, game consoles, and streaming devices. Each device has a variety of computational resources and the tech team at Hulu has to make sure Hulu is able to run across all devices — from high to low powered devices.
Gaming consoles like Xbox One and Playstation provide a similar level of computing power as a mobile phone or desktop computer but devices like smart TVs and streaming devices (i.e. Blu-ray players, set top boxes, and streaming sticks) are limited in computational power. For our team to consistently bring Hulu to life across as many devices as possible, we have had to develop new tools that are rooted in compatibility and performance, without sacrificing the simplicity and paradigms that are familiar to developers.
One of these tools we developed to manage low powered environments is a data-binding library called Quickdraw.js. This library allows even lowest powered devices to use an MVVM architectural pattern by removing the assumption that computational and memory resources are plentiful (which is not the case for living room devices). Quickdraw abstracts and normalizes view interactions across browsers and enables our developers to ship updates more quickly.
We’ve open sourced Quickdraw to share our knowledge with the web developer community and also get feedback from our peers. In this blog post, we will take a closer look at the challenges and complexities we faced when working with low powered devices and discuss how the process ultimately led to the creation of Quickdraw.
After investigating performance problems in MVVM proof of concepts we created, it was clear that low-powered devices were breaking a big assumption of existing frameworks — that you are running your code in a modern web browser on a personal computer.
We needed an updated framework that would:
- Run in an environment where resources like computing and memory are scarce
- Handle device-based browsers that are typically many years behind the desktop market
- Use common web developer paradigms, making it easier to onboard new Hulugans
- Manage all these requirements and run quickly to ensure the best viewer experience
From a technical perspective, this framework had to provide us with more than MVVM and operate with just ECMAScript 3. It also had to run as fast as possible with a JS engine in interpreter mode, and be flexible with the number of concurrent DOM trees it could interact with.
Designing and Experimenting with an Existing Framework
Initially, our thought was to rework an existing framework to fit our needs. The team preferred the syntax and features of Knockout, so we used it as a base and dove deep into the internals, reviewing how it handled view management, memory management, update computations, and DOM rendering.
Rewriting Knockout for our needs would have changed it too much structurally to warrant attempting the necessary fixes, so instead we adopted the API from Knockout that worked for our needs and set out to build a new library.
Our core library philosophy was going to be performant data binding, so we called it Quickdraw.
Building our Data Binding Library
The first version of Quickdraw was quite basic: our teams could create basic observables and bind a model to a DOM tree. Changing the observables would rebind the connected nodes and the accompanying subtree. This version had great initial binding performance but sacrificed update speed for coarse tracking and synchronous evaluation.
With just this simple implementation, we had considerable performance improvements in the application (with PS3 going from 500ms to just under 100ms for keyboard rendering time). Larger pages however, performed on-par or worse than Knockout.
Profiling in browser, we found that it was not uncommon for an observable to be changed multiple times in an update loop. The synchronous evaluations were forcing each change to be rendered, wasting valuable computing time. To address this issue, we decoupled view updates from observable changes and processed them in batches instead of one by one. This change vastly improved the performance profile of updates — a search result list that had initially taken 350ms to render, now took 50ms to render. Further improvements were made to re-enable fine-grained dependency tracking, and these enhancements sped up the update process even more.
With performance of our library on the right track, we wanted to review and streamline our memory usage. The observables were implemented with two-way tracking, with tracking references stored directly on the observer and observee.
This approach had two benefits:
- There was no central data store, meaning an ever-growing object did not need to be allocated in memory
- Losing a reference to both meant that the whole set was automatically garbage collected — this allowed us to side step explicit unbinding at first
As we profiled on device however, we found memory being leaked instead of being cleaned up. Our approach to garbage collection was great in theory, but it was not as simple in practice. In particular, we found that older browser engines had trouble freeing objects (especially DOM objects) that were captured in function closures that were no longer referenced. To handle these unusual constraints, we changed all usages of closure captured variables to explicit properties and parameters and added a full unbind cleanup mechanism.
Refining and Testing
At this point, the binding in our library performed well on devices and didn’t leak memory. When we ran it through our original proof of concept benchmark, it passed with flying colors. We then began porting our new application to Quickdraw.
Early results looked promising, however as we worked and incorporated more complex binding handles and our custom platform hooks, we found that while we were binding through the DOM tree hierarchy quickly, we were not animating smoothly. An additional review of the framework showed that binding handlers created and modified DOM elements during their changes — this caused reflows, repaints and garbage collection cycles. To resolve this, we looked to batch the update evaluations for the DOM, just as we had batched the update evaluations for the model. React had implemented a concept called virtual DOM to handle a similar situation, and we adapted this concept for Quickdraw.
To ensure the introduction of the virtual DOM concept did not violate our performance-first approach to Quickdraw, we decided to take a minimal approach. Instead of building a completely mirrored hierarchy, the virtual DOM is represented as additional descriptors on top of the real DOM. As the main purpose of the virtual DOM was to delay the application of binding actions on the DOM tree, its state is lazily constructed, providing pass-through data access to unchanged values and read-from-state access for changed values. This is true for element specific information as well as hierarchical information. When tree nodes are rearranged by bindings, the virtual DOM tracks the changes as a simple rearrangement of pointers to other virtual structures. When it’s time to render, all tracked changes are applied to the DOM and the virtual state is cleared, meaning the effective cost of the virtual DOM is directly tied to the complexity of a model update, not to the complexity of the overall DOM tree.
By leveraging the virtual DOM for Quickdraw, any modifications made can be immediately observed, but changes that cause a layout or paint will not be made until an explicit render is requested. Additionally, the virtual DOM coupled with a basic templating system separated the binding and rendering processes, allowing us to decouple the rendering updates from the binding updates. This made the process even more efficient — a full update of the system could take 50–100ms to process, with rendering the DOM changes taking less than 10ms.
What’s Next for Quickdraw
At Hulu, we leverage Quickdraw to quickly prototype, iterate, and release large scale UI changes, such as the recent live guide, across our entire living room device ecosystem. We share Quickdraw with you because it is a crucial piece of our application infrastructure and it’s an interesting case study on performant web applications in resource-constrained environments. We also believe it will be a valuable resource to any web developer working in resource constrained environments. In sharing this code with the community, we hope to gather feedback that we can directly integrate into upcoming investments that will further modernize and evolve Quickdraw.
If you’re interested in working on projects like these and powering play at Hulu, see our current job openings here.