Hello, fellow web perf enthusiast! Contribute to the 2024 edition happening this December. Click.

Web Performance Calendar

The speed geek's favorite time of year
2016 Edition
ABOUT THE AUTHOR

Parashuram

Parashuram (@nparashuram) is a Senior Program Manager at Microsoft Open Technologies Inc and a front end developer.

From its modest origins as a collection of hyperlinked documents, the JavaScript-Powered-Web has evolved into a platform that delivers rich applications on a variety of devices. JavaScript libraries started out to deal with browser quirks but now have now evolved to add form and structure to our code. By providing constructs for patterns like change detection and componentization, frameworks enable us to concentrate on business logic.

Framework Bloat?

This growth in capabilities of frameworks has also translated into JavaScript becoming a significant part of the total work done by the browser. Timelines from profiling similar user interactions on a vanilla JavaScript vs an Angular based TodoMVC application show how scripting (indicated by yellow in the pie charts) dominates the time taken by rendering and painting for every frame.

image00

image02

While frameworks are useful in making code easy to manage, we need to ensure that our applications remain responsive despite all the extra abstractions. In practice, this means that in addition to rendering and painting, the JavaScript execution time should also be contained inside 16 milliseconds to achieve 60 fps animations and smooth response rates.

Even as frameworks squeeze every bit of optimization, there is a limit to the amount of redundant work that can be eliminated. Once this limit is hit, one approach is to turn to alternatives like multi-threading and parallelization. These techniques are common in environments like the server, desktop, or native mobile apps. Typically, complex computations and tasks run on separate threads, leaving the UI thread free to respond to user interactions.

Web Workers

In a browser, Web Workers are probably the closest concept to multi-threading. Many browsers even utilize the multiple cores in CPUs to run code in parallel when using Web Workers. Web Workers have been around since 2009 and are supported by all major browsers.

Web Workers communicate with the main UI thread on the browser using messages and have a simple event based programming API. Though they cannot manipulate the DOM, they can can access many APIs including make network requests or accessing storage capabilities of the browser.

Web Workers may be one of the most underutilized features of the HTML5 family, with examples classically demonstrating uncommon use cases like manipulating images, or running cryptographic operations. How about trying to use them for making frameworks even faster?

Frameworks in a Web Worker

In most browsers, crossing the boundary from JavaScript virtual machine over to the rendering engine (like Trident or WebKit) is usually expensive – hence the emphasis on reducing DOM operations by employing clever change detection strategies. Right from the digest cycles of Angular 1, to virtual DOM reconciliation of React, a lot of calculations are about reducing the number of DOM operations. These computations do not touch the DOM and are prime candidates to be moved over to run in a Web Worker environment.

React is an interesting framework and consists of two parts – (1) “react” core which powers the reconciliation and (2) “react-dom” which is responsible for rendering the HTML elements. Usually, both parts are imported and run in the main UI thread. However, “react” core does not necessarily interact with DOM. I experimented with a “custom renderer” by moving all of react core into a Web Worker environment. All DOM manipulation operations were simply sent over to its counterpart react-dom that runs in the main UI thread. Conceptually, this is similar to React Native for creating native iOS and Android applications.

Angular 2 also supports the idea of custom renderers and has a similar experiment where a custom Web Worker based renderer can be used.

Performance considerations

To investigate the performance impact of moving frameworks into Web Workers, I ran tests using browser-perf on both flavors. Interestingly, the Web Worker flavor was faster and continued to get better as the number of DOM node changes per second increased.

image01

Though the improvement from the above graph only seems to show a difference of 5-10 fps, this variance could be the difference between a smooth and a janky application.

The key for better performance is the assumption that the cost of communication between the worker and the main UI thread is negligible compared to the time required to run the reconciliation algorithm. I experimented with various communication protocols, including sending plain JavaScript objects, compressing them using protocol buffers and superpack, etc. The message format that finally emerged to be the fastest was using JSON.stringify to serialize and send data!

Another optimization technique used was to batch messages to avoid the extra serialization cost. The batch size was dynamically determined with the UI thread providing feedback to the Web Worker to slow down or speed up, based on the load it could handle. This worked very well when the application was tested on desktop vs mobile browsers. In case of React, I could also prioritize the messages so that DOM operations corresponding to visual feedback to user interactions (like button presses) were sent before updating other information on the page.

Limitations

The one scenario where the communication cost grew to be much higher than the JavaScript execution cost was animations. If JavaScript drove animations, ensuring that the UI thread received new values for the position of elements at a constant rate was hard to guarantee. In React Native, this problem is resolved by making animations declarative in a worker and running the actual calculations on the UI thread. For browsers, this would mean that animations should be performed in CSS using keyframes.

Another consideration was dealing with browser events like clicks and keypresses. In a browser, cancelling events is synchronous. Communicating with Web Workers is asynchronous and it gets tricky when an event handler running in a Web Worker needs to cancel an event synchronously. For now, I am using the newer EventListener options syntax to declaratively deal with such issues. The cancellation of an event needs to be pre-specified and cannot be determined in the event handler. This model of using passive event listeners is also gaining traction in regular use cases since it ensures that page scrolling is smooth with no JavaScript to interrupt it.

Conclusion

Running frameworks like React and Angular are still cutting-edge experiments and may not yet be ready for production. However, research in this area and the performance benchmarks clearly indicate that there is a value in breaking down monolithic calculations performed by current frameworks into smaller pieces that can be independently scheduled. Ideas like React Fiber are a great step in ensuring that our frameworks can do all the work they need, without slowing down the applications.

Meanwhile, we can continue to convert our services to use web workers – parts of code that are responsible for making backend calls, converting the data into a format that the UI can consume, sync local storage with backend data, etc.

Web Workers are well supported today, so they may as well be used!

Thanks to Brent Vatne and Dustan Kasten for reviewing the article.