Using Web Workers to Improve the Performance of Metro HTML5-JavaScript* Apps

Download Article

Download Using Web Workers to Improve the Performance of Metro HTML5-JavaScript* Apps [PDF 591KB]

 

Objective

This article provides an introduction on how to use web workers inside HTML5-JavaScript* Metro apps. We will discuss what web workers are, why we need them, and how to use them. A sample app will be used as a case study to discuss these topics. We will mainly address two areas where web workers can be of tremendous help: heavy computation and background server interaction or downloading content in the background. We will use the sample app to showcase the expected performance benefit of using web workers.

 

Introduction and Sample App

HTML5/JavaScript apps are single threaded at the core. We need to interleave enough time between computations/processing, to let the HTML DOM (Document Object Model) update the UI elements and handle different events. Without these non-blocking interleaves for appropriate durations, the apps' UI and user interaction becomes either blocked or too slow. Users usually perceive the app as being stuck or crashed.

One highly recommended way to mitigate some of these problems is to adopt Metro asynchronous programming patterns. More details on asynchronous programming in JavaScript can be found here:

http://msdn.microsoft.com/en-US/library/windows/apps/hh700330

Though asynchronous programming can help resolve some of these responsiveness problems of being single threaded, the performance could still be lacking. Also, the app might have additional use cases where it might require continued computation, monitoring, or background processing. Being single threaded severely undercuts performance in these use cases.

Web workers were introduced to address these problems. They allow apps to instantiate additional threads as needed. Web workers do not have access to the DOM, they are strictly used for non-DOM related work. They can be very useful in use cases where the apps might need to do some heavy computation, that could potentially be blocking otherwise, and also in use cases where the app needs to interact with server or download data continuously from the server in the background.

In this article we will use a sample app, "Feed Analyzer," to demonstrate these concepts. The objective of this app is to download several RSS feeds from the Internet, and analyze the feed for several keywords, search for the number of occurrences, and categorize posts from all feeds according to keywords. We will also simulate heavy computation by blocking for a few milliseconds. The app will implement both with and without web worker use cases. Users will have a choice to select a number of web workers, up to 4. The user can choose 0 web workers for the non-web-worker use case.

The app's final UI is shown below:

Users can compare performance using a time-elapsed metric, shown in the upper left of the app's UI. Clicking any of the keywords will filter all the posts and show them in "Feed Posts" view, or users can click "Show All" to display all posts. Clicking "Start Feed Analysis" will (re)start new feed analysis using the specified number of web workers (0 being the non-web-worker use case). The app will also show log messages from each web worker or the main thread for non-web-worker use cases. Users can click any feed post to view the full feed article in a flyout (example shown below).

 

Web Workers Overview

Web workers (or just "workers") let HTML apps instantiate additional thread instances for doing work in parallel. Though they enable a kind of multi-thread programming, they do not use any shared data which makes the programming model simpler. All communication between workers and the main thread is through message passing (http://www.w3.org/TR/webmessaging/). They are relatively heavyweight and take significant time to instantiate. Depending on app requirements we might instantiate them during app initialization itself and continue (re)using them, instead of instantiating and closing them on demand.

The web workers standard (http://www.w3.org/TR/workers/) specifies two kinds of web workers-dedicated and shared. Dedicated workers have a single connection and can only communicate with the parent (single page), while shared workers can have multiple connections to/from any script of the same origin. Metro apps only support dedicated web workers, which we will focus on in this article.

Web workers do not have access to the full DOM. They can access a few APIs like XHR, location, and navigator along with the standard JavaScript library. For more information on differences between web page and web worker, please refer to this blog post:

http://blogs.msdn.com/b/ie/archive/2011/07/01/web-workers-in-ie10-background-javascript-makes-web-apps-faster.aspx

Web workers communicate with the main thread (the parent) through the postMessage method, and message event (onmessage event handler). For detailed web worker methods, properties, and events, refer to this link:

http://msdn.microsoft.com/en-us/library/windows/apps/hh767434.aspx

Web workers can also import other scripts using the importScripts function call. This allows for breaking down app logic into multiple scripts, and then importing common functions or logic into the workers as needed. Following is a web worker code snippet from our sample app.

importScripts("//Microsoft.WinJS.1.0.RC/js/base.js", "/js/feedcommon.js");
 

Using Web Workers in Metro Apps

Web workers in Metro Apps have access to the native Metro framework API. They can access the Windows* WinRT API just like the main thread in the app, and the same restrictions and security rules apply.

Creating a new web worker is straightforward. Visual Studio* provides a default worker template that we can use to quickly create a new "dedicated worker," as shown below.

This template will create a new JavaScript file with an onmessage event handler stub for the worker, and add the file to our app project folder. We need to write down the filename of this script ("feedworker.js" in our example) because we must specify it in the worker constructor function call.

We can implement all of worker's code inside this worker source file or import code from other scripts. In our sample app, we implement common functionality inside a single JavaScript file "feedcommon.js." We can import this file into the worker using the importScripts function, and also import into our non-web-worker code path via the regular page import method (script tag).

Below is the full source code for the web worker (feedworker.js) in the sample app.

importScripts("//Microsoft.WinJS.1.0.RC/js/base.js", "/js/feedcommon.js");

onmessage = function (ev) {

    feedAnalyzeAsync(ev.data.url, ev.data.tags, ev.data.source)
.then(function (ev) { postMessage("end"); },
                    function (ev) { postMessage(ev); postMessage("end"); },
                    function (ev) { postMessage(ev); });

}

We import both WinJS base API, as well as our app's common functionality (feedcommon.js). Please note the onmessage event handler simply calls the common function, and then passes the results back to the main thread via the postMessage function. The default scope of this script is worker global script ("self"), hence we can directly invoke methods like postMessage and onmessage declarations without qualifying.

The sample app's main functionality-retrieving multiple feeds (background downloads) and searching/analyzing them (heavy blocking computation)-is implemented as a single function "feedAnalyzeAsync" in the common script (feedcommon.js). Since the common script uses both WinRT API and WinJS API, the web workers in our sample app will be exercising both these APIs as well (this demonstrates accessing WinRT and WinJS from web workers). It also allows us to compare web worker performance as they use the same underlying code path.

Instantiating new web workers from the app's main thread is also straightforward. We specify the filename of the web worker script inside the worker constructor as shown below ("new Worker").

for (var wwi = 0; wwi < parseInt(id("wwbtn").max) ; wwi++) {
            wworkers[wwi] = new Worker('/js/feedworker.js');
            wworkers[wwi].onmessage = function (ev) {
                if (ev.data === 'end')
                    enablefabtn();
                else
                    facallback(ev.data);
            }
        }

Please note, we are instantiating several workers at once and configuring the message handler (onmessage) for each worker. Web workers are relatively heavy weight, and they could take significant time to instantiate. In our sample app, we instantiate all workers when the app starts up, during the initialization phase.

Once instantiated, the worker can start receiving messages via the onmessage event handler and communicate with main thread via the postMessage method call. Likewise, our main app thread can communicate with workers via the postMessage method call and receive messages via the onmessage event handler on worker objects.

The data passed between the main thread and worker using postMessage can be of any type supported by the message passing standard (http://www.w3.org/TR/webmessaging). JSON format is widely supported, which is used in our sample app.

 

Performance Benefits of Web Workers

Web workers can be useful in a variety of different use cases. They can be used to handle a long monitoring task in the background, or do compute heavy processing, or handle all non-UI logic and leave the DOM UI logic to the main thread, or even help simulate non-user actions inside games. All of these use cases benefit from web workers either by improving responsiveness or performance.

Our sample app "FeedAnalyzer" covers two main use cases: background downloads and heavy computation/processing. To compare performance with and without web workers, we use the same underlying code path for with-web-worker and without-web-worker cases. We use the asynchronous programming pattern (WinJS promises) for both cases.

The following code is from a common function for background downloads (partial listing):

function feedAnalyzeAsync(url, tags, source) {
    return new WinJS.Promise(function (completed, failed, progress) {

        try {
            var uri = new Windows.Foundation.Uri(url);
            var synd = new Windows.Web.Syndication.SyndicationClient();
            synd.bypassCacheOnRetrieve = true;
            progress({ 'id': source, 'type': 'log', 'data': "Downloading Feed..." });
            synd.retrieveFeedAsync(uri).done(function (f) {

                var ftitle = f.title ? f.title.text : "(no title)"
                progress({ 'id': source, 'type': 'log', 'data': 'Download Complete! Analyzing feed "' + ftitle + '"' });

We wrap our common function inside a WinJS promise as it could block for a long duration. As seen above, we use WinRT syndication APIs to download feeds asynchronously in the background. Once the feed is fully downloaded, we analyze all the posts inside the feed for keyword occurrences, and simulate some compute heavy processing by blocking for a significant time. Below is the code snippet for this use case (partial listing).

// some heavy computation - search for all keywords in each post
var tagfound;
tagfound = 0;
tags.forEach(function (tag) {
var m = content.match(new RegExp(tag, 'gi'));
if (m) { result['tags'][tag] = m.length; logmsg += ' (' + m.length + " " + tag + ')'; tagfound = 1; }
 });
 
 if (tagfound === 0) logmsg += ' ( none )';
                    
// simulate even more heavy computation - block, then delay processing using promise timeout
var time = new Date();
while ((new Date() - time) < 100);
WinJS.Promise.timeout(200 * (i+1)).then(function (source, logmsg, result, test) {
                        
           return function () {
       	 progress({ 'id': source, 'type': 'log', 'data': logmsg + ' in "' + result['title'] + '"' });

When the user clicks the "Start Feed Analysis" button, the app will start downloading several feeds, and analyzing all posts in each feed in the background. In the non-web-worker case, it uses an asynchronous pattern so the UI can be updated after each post analysis. The app will report time elapsed for completing both of these use cases.

Below is a screenshot of non-web-worker case.

Please note, it took 34743 msec on our test system. It could be a different result on your test system.

For the web-worker case, the user-configured number of workers are started and feeds are distributed to these workers in round-robin. Each worker will then kick off the feed download and analyze all posts inside the feed, similar to non-worker case. Time elapsed is reported when all workers complete their tasks. Below is snapshot of a 4 web-worker scenario.

As seen above, the web worker scenario completed in 27300 msec, a significant improvement in performance. Please note these performance numbers are on our test system, and your results will vary. In general, web workers can show significant performance improvement only when the underlying system has more than one CPU thread.

 

Summary

HTML5/JavaScript apps are single threaded and can be unresponsive or degrade performance if the app has code that blocks for a long duration. Web workers help solve these problems and enable new usage models. We provide an introduction to web workers, why we need them, and how to use them. We also show how to use web workers in Metro HTML5/JavaScript apps, and showcase the performance difference compared to a non-web-worker scenario. We demonstrated two main uses where web workers can make a significant performance difference: compute heavy processing and downloading/analyzing in the background.

*Other names and brands may be claimed as the property of others.

**This sample source code is released under the Intel Sample Source Code License Agreement

For more complete information about compiler optimizations, see our Optimization Notice.

Comments