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.
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
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.
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.
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.
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.
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!