Using Webworkers to make React faster

Tl;Dr; ReactJS is faster when Virtual DOM reconciliations are done on a Web Worker thread. Check out the difference at the demo page

A typical ReactJS application consists of two parts - the React library responsible for most of the complex Virtual DOM calculations, and React-Dom that interacts with the browser's DOM to display contents on the screen. Both these are added to the page using script tags and run in the main UI thread.
In a blog post a few weeks ago, I had written about an experiment where I tried to run the React Virtual DOM calculations in a Web worker instead of main UI thread of the browser. I had also run performance measurements to understand the impact of parameters like node count or parallel workers on frame rates.

Recap of previous results

The frame rate numbers in themselves were not conclusive from the previous implementation. It was observed that the real benefit of Web Workers only surfaced when there were sufficiently large number of nodes to change. In fact, the performance of Web Workers was worse than the normal React implementation when the node count as small as in a typical for most applications.

Updates and new results

The reason that the Web-workers case is slow was due to the time spent passing and processing messages between Web Workers and the main UI thread. I was trying to solve this problem by trying to find an optimal batch size so that the message processing time is much less than actual DOM manipulation. While tweaking the batch size did not yield great benefits, I got a couple of good suggestions from folks on the internet.  
  1. The first suggestion was to use transferable objects instead of using JSON data to pass messages. The DOM manipulation instructions I was passing between the worker and the UI thread did not have a fixed structure. Thus, I would have to implement a custom binary protocol to make this work.
  2. The second suggestion was to simply use JSON.stringify when passing messages. I guess this is similar to transferable objects, just that in this case, it is a big blob of 8-bit characters. There is also a comment about this by one of the IndexedDB authors.
By 'stringifying' all messages between the worker and the main thread, React implemented on a Web worker faster than the normal react version. The perf benefit of the Web Worker approach starts to increase as the number of nodes increases. 

I wrote an automation script to calculate the frame rates using browser-perf, and here is the chart. The tests were run on Desktop Chrome on a Macbook pro, and a Nexus Android device.
As the number of nodes get to more than 100, the difference is not very visible. To make the difference explicit, here is the same chart with the frame rates in a logarithmic scale when running on desktop chrome.

As you can see from the charts, the React Worker version is at least as fast as, if not faster than the normal version. The difference starts to get more pronounced as the number of nodes increases.
A good experiment should be reproducible, and you can use these instructions to run the tests and collect the information, or simple use Chrome's FPS meter to see the difference in the worker and normal pages.

A real world app

While it worked well on an articifial app like DBMonster, it is also important to test this idea on typical real world apps. I wrote a todo app that also serves as an example to show the changes needed in a react app to make it work with Web workers. The changes are not many and we basically need to separate React and React-DOM into the worker and main threads respectively.

Browser Events

A web worker does not have access to the browser DOM and hence cannot listen to click or scroll events. Presently, React has an event system with a top level event listener that listens to all events, converts them into synthetic events and sends it over to listeners that we define in the Virtual DOM (in JSX files).
For our webworker case, I re-use this event listener and subscribe to all events. Thus, all events are handled in the main thread, converted to synthetic events and then passed over to the worker. This also means that all the calculations to create synthetic events happens in the main thread. A potential improvement would be passing the raw events over to the worker and calculating synthetic events and bubbling on the worker.
The other issue is about semantics like preventDefault() or stopPropogation(), as also described in the pokedox article. Responding to event in a browser is synchronous while passing messages and getting a result back from a web worker is asynchronous. Thus, a way is needed to determine if we need to prevent default even before the event handler running on a worker can tell us.
At the moment, I simply prevent all default actions, but there are two options here to ensure correct behavior. As vjeux suggests, we could use a pure function that can be serialized and sent to the main UI thread from the worker. Another option would be to prevent the current event and raise another event in case preventDefault is not called.
I am still exploring the options and as other frameworks start offloading work to web workers, I am sure we could come up with a pattern.

Next Steps

The tests conclusively tell me that Web Workers are always better. May be we are in an era where Web Workers are finally used by mainstream Javascript framework to offload all expensive computations.
My implementation may have some gaps and I would like to try it out on more real world apps. If you have an app suggestion and would like to try it out, I would love to work with you. You can either ping me, or head over to the github repo to send in pull requests !

Writing a custom debugger for ReactNative

ReactNative enables us to build mobile apps that have the elegance of a native use interface while taking advantage of a fast, web like development process. The creative use of Chrome devtools to debug the JavaScript code is definitely a big plus in the workflow of a developer. While I love Chrome for debugging, I still prefer to set breakpoints or watch variable right from within my editor. This way, I still benefit from editor features like syntax highlighting and autocomplete, support for my backend system and simply having lesser windows cluttering my desktop.
Over the past few weeks, I was experimenting with ways to add debugging to my editor, and this post is an explanation of how to add custom debuggers to ReactNative. Our team is planning to add debugging to a bunch of other features that we plan to release as an extension for VSCode.

ReactNative Debugger today


Before writing a custom debugger, it is useful to appreciate how the existing setup works. I found an article that has an excellent explanation, though it is for an older version. The biggest change from the article is the use of a web worker in order to provide an isolated sandbox for the running scripts.
I created an "old-style" UML sequence diagram, hoping to capture most of the concepts without going too deep into the details.



The full SVG file may be easier to read.  Most of the messages have a direct correspondence to methods in the source code.

Path to a Custom Debugger

When trying to implement a custom debugger, I considered the following approaches
  1. Attaching a Javascript debugger directly to the Javascript VM packaged with the app on the device. This is probably the most accurate debugger since you are debugging the code running in its real environment. I believe that the NativeScript debugger uses this approach, but it was a little hard to implement.
  2. Create a parallel JSDebuggerWebSocketClient class to send messages to a process that I write, instead of sending it to the packager. While my process would have all the necessary debug hooks, I would still need to get source files and source maps from the packager.
  3. Simply attach a debugger to the running Chrome process. This seemed like the simplest case, but I was not a fan of having Chrome open and using it to just execute Javascript.
I finally settled on an variation of the third approach where instead of opening Chrome, I open a headless Node process and attach a debugger to that. Instead of launching Chrome, my node process would simply need to open a web socket connection to the packager, and the debug process would now be redirected to the new Node process. Most editors already have excellent support for debugging Node.

Refining the debugger

Since the packager now proxies to the Node process instead of Chrome, some improvements are needed in the Node process
  • In case of the Chrome debugger, ReactNative modules are loaded using the webworker construct - "importScripts". A Node process does not have a simple way to load scripts from a web server. Thus, we had to implement a way to download the code, and "require" it using runInNewContext. The sandboxed context also allows code isolation that the Webworker provides.
  • Sourcemaps also have to be downloaded and changed so that they point to source files on the local system. 
  • For websockets capability in the node process, we could use the websocket npm module that provides an excellent, w3c compliant interface which could be used as a drop in replacement.
  • Instead of requiring the user to shake the phone to enter into the debug mode, we could run adb shell am broadcast -a "com.rnapp.RELOAD_APP_ACTION" --ez jsproxy=true to enable proxy mode on the app. 
However, we still suffer from one problem. ReactNative hardcodes the fact that Chrome needs to be launched when debugging starts. If Chrome connects to the packager's websocket fast enough, our Node process will not work. 
Here is a pull request that looks at an environment variable and then launches a custom process, instead of defaulting to Chrome. This is similar to the way custom editors can be launched from ReactNative. I hope that the pull request is merged soon, so that custom debuggers can be added. 

The final product

Putting all of this together, a demo video of the capabilities is up on youtube. We plan to release it as a part of VSCode+ReactNative extension. In addition to debugging, you would also have support for Javascript and JSX syntax highlighting, autocomple, and ways to call ReactNative commands from within VSCode.
You can also signup for a preview. If you have additional feature requests or ideas that you think we should implement, please ping me and our team would love to talk to you.

Using Chrome Traces to Automate Rendering Performance - aka how browser-perf works

Cross post from calendar.perfplanet.com

Ten years ago, increasing the performance of a website usually meant tweaking the server side code to spit out responses faster. Web Performance engineering has come a long way since then. We have discovered patterns and practices that make the (perceived) performance of websites faster for users just by changing the way the front end code is structured, or tweaking the order of elements on a HTML page. Majority of the experiments and knowledge has been around delivering content to the user as fast as possible.
Today, web sites have grown to become complex applications that offer the same fidelity as applications installed on computers. Thus, consumers have also started to compare the user experience of native apps to the web applications. Providing a rich and fluid experience as users navigate web applications has started to play a major role in the success of the web.
Most modern browsers have excellent tools that help measure the runtime performance of websites. The Chrome Devtools features a powerful Timeline panel that gives us the tracing information needed to diagnose performance problems while interacting with a website. Metrics like frame rates, paint and layout times present an aggregated state of the website’s runtime performance from these logs.
In this article, we will explore ways to collect and analyze some of those metrics using scripts. Automating this process can also help us integrate these metrics into the dashboards that we use to monitor the general health of our web properties. The process usually consists of two phases – getting the tracing information, and analyzing it to get the metrics we are interested in.

Collecting Tracing information

The first step to get the metrics would be to collect the performance trace from chrome while interacting with the website.

Manually recording a trace

The simplest way to record a trace would be to hit the “start recording” button in the timeline panel, performing interactions like scrolling the page or clicking buttons, and then finally hitting “stop recording”. Chrome process all this information and shows graphs with framerates. All this information can also be saved as a JSON file by right-clicking (or Alt-clicking) on the graph and selecting the option to save the timeline.

Using Selenium

While the above method is the most common way to collect tracing from Chrome, doing these repeatedly over multiple deployments or for different scenarios can be cumbersome. Selenium is a popular tool that allows us to write scripts that perform interactions like navigating webpages or clicking buttons and we could leverage such scripts to capture trace information.
To start performance logging, we just need to ensure that certain parameters are added to the existing set of capabilities.
Selenium scripts have binding for multiple programming languages and if you already have selenium tests, you could add the capabilities in the following examples to also get performance logs. Since we are only testing Chrome, running just Chromedriver instead of setting up Selenium and then executing the following node script gets the trace logs.

var wd = require('wd');
var b = wd.promiseRemote('http://localhost:9515');

b.init({
    browserName: "chrome",
    chromeOptions: {
        perfLoggingPrefs: {
            "traceCategories": "toplevel,disabled-by-default-devtools.timeline.frame,blink.console,disabled-by-default-devtools.timeline,benchmark"
        },
        args: ["--enable-gpu-benchmarking", "--enable-thread-composting"]
    },
    loggingPrefs: {
        performance: "ALL"
    }
}).then(function() {
    return b.get('http://calendar.perfplanet.com'); // Fetch the URL that you want
}).then(function() {
    // We only want to measure interaction, so getting a log once here
    // Flushes any pervious tracing logs we have
    return b.log('performance');
}).then(function() {
    // Perform custom actions like clicking buttons, etc
    return b.execute('chrome.gpuBenchmarking.smoothScrollBy(2000, function(){})');
}).then(function() {
    // Wait for the above action to complete. Ideally this should not be an arbitraty timeout but be a flag
    return b.sleep(5000);
}).then(function() {
    // Get all the trace logs since last time log('performance') was called
    return b.log('performance');
}).then(function(data) {
    // This data is the trace.json
    return require('fs').writeFileSync('trace.json', JSON.stringify(data.map(function(s) {
        return JSON.parse(s.message); // This is needed since Selenium spits out logs as strings
    })));
}).fin(function() {
    return b.quit();
}).done();

The script above tells selenium to start chrome with performance logs, and enables specific trace event categories. The “devtools.timeline” category is the one used by Chrome devtools for displaying its graphs about Paints, Layouts, etc. The flags “enable-gpu-benchmarking” expose a window.chrome.gpuBenchmarking object that has a method to smoothly scroll the page. Note that the scroll can be replaced by other selenium commands like clicking buttons, typing text, etc.

Using WebPageTest

WebPageTest also roughly relies on the performance log to display metrics like SpeedIndex. As shown in the image, the option to capture the timeline and trace json files has to be enabled.
Image of WebPage Test Interface
Custom actions can also be added to perform actions in a WebPageTest run scenario. Once WebPageTest run finishes, you can click download the trace files by clicking the appropriate links. This method could not only be used in WebPageTest, but also with tools like SpeedCurve that support custom metrics.

Analyzing the Trace Information

The trace file that we get using any of the above methods can be loaded in Chrome to view the page’s runtime performance. The trace event format defines that each record should contain a cat (category), a pid (processId), a name and other parameters that are specific to an event type. These records can be analyzed to arrive at individual metrics. Note that the data obtained using the Selenium Script above has some additional metadata for every record that may need to be scrubbed before it can be loaded in Chrome.

Framerates

To calculate the FrameRates, we could look at events by the name DrawFrame. This indicates that a frame was drawn on the screen and by calcuating the number of frames drawn, divided by the time for the test, we could arrive at the average time per frame. Since we also have benchmarking category enabled, we could look “BenchmarkInstrumentation:*” events that have timestamps associated with them. Chrome’s telemetry benchmarking system uses this data to calculate the average frame rates.

Paints, Styles, Layouts and other events

The events corresponding to ParseAuthorStyleSheet, UpdateLayoutTree and RecalculateStyles are usually attributed to the time spent in Styles. Similarly, the log also contains Paint and Layout events that could be useful. Javascript time can also be calculated using events like FunctionCall or EvaluateScript. We can also add the GC events to this list.

Network Metrics

The tracelogs also have information related to firstPaint etc. However, the ResourceTiming API and the NavigationTiming APIs make this information available on the web page anyway. It may be more accurate to collect these metrics directly from real user deployments using tools like Boomerang, or from WebPageTest.

Considerations

There are a few things to consider while running the tests using the methods mentioned above.

Getting Consistent metrics

Chrome is very smart at trying to draw the page as fast as possible and as a result, you could see variations in the trace logs that are gathered when running tests for a scenario twice. When recording the traces manually, human interaction may introduce differences in the way a mouse is moved or a button is clicked. Even when automating, running separate selenium scripts occurs over the network and executing these scripts is also recorded in the logs.
To get consistent results, all this noise needs to be reduced. It would be prudent to not combine scenarios when recording trace runs. Additionally, running all automation scripts as a single unit would also reduce any noise. Finally, comparing tests runs across multiple deploys is even better since both these runs would have the same test script overhead.

Trace file size

The trace file can take up a lot of space and could also fill up Chrome’s memory. Requesting for the logs from ChromeDriver could in a single, buffered request could also result in Node throwing out of memory exceptions. The recommendation here would be to use libraries like JSONStream to parse the records as a stream. Since we are only interested in aggregating the individual records, streaming can help consume scenarios that are longer and hence take more memory.

Browser Support

The performance logs are available on the recent versions of the Chrome browser, both on the desktop and on Android. I had worked on a commit to make similar trace logs available for mobile Safari. Hybrid applications using Apache Cordova use WebViews which are based on browsers. Hence, this method could also be applied to Apache Cordova apps for Android and iOS.
Note that these performance logs started as being Webkit specific, and hence are not readily available for Firefox or IE yet.

Conclusion

All the techniques described above are packaged together in the browser-perf package. Browser-perf only adds the extra logic to smooth out the edge cases like the scrolling timeout, or ability to pick up specific metrics, or processing the trace file as a stream. I invite you to check out the source code of browser-perf to understand these optimizations.
These metrics can be piped into performance dashboards and watching out for these metrics should give us an idea of general trends of how the web site has been working across multiple commits. This could be one small step to ensure that websites continue to deliver a smooth user experience.