Cross domain and cross browser web workers

What are web workers?

A web worker is a script that runs in the background in an isolated way. They run in a separate thread and can perform tasks without interfering with the user interface. Since the scripts on a page are executed in a single thread of execution, a long running script will make the page unresponsive. Web workers allow to hide this from the user and let the browser continue with normal operation while the script is running in the background.

Limitations of web workers

Web workers are great because they can perform computationally expensive tasks without interrupting the user interface. But they also bring quite a few limitations:

  1. Web workers do not have access to the DOM
  2. They do not have access to the document object
  3. These workers do not have access to the window object
  4. Web workers do not have direct access to the parent page.
  5. They will not work if the web page is being served a file:// URL
  6. You are limited by the same origin policy i.e. the worker script must be served from the same domain (including the protocol) as the script that is creating the worker

The first four limitation mean that you cannot move all your Javascript logic to webworkers. The fifth one means that even when developing, you will need to serve your page through a web server (which can be on the localhost).

The purpose of this article is to see how to work around the last limitation (same origin policy). But first let’s briefly see how to use a worker.

How to use web workers

Creating a worker is first of all pretty straight forward:

//Creating the worker
var worker = new Worker(workerUrl);

//Registering a callback to process messages from the worker
worker.onmessage = function(event) { ... });

//Sending a message to the worker
worker.postMessage("Hey there!");

//Terminating the worker
worker.terminate();
worker = undefined;

In the worker things then work in a similar way:

//Registering a callback to process messages from the parent
self.onmessage = function (event) { ... });

//Sending a message to the parent
self.postMessage("Hey there!");

Now the problem I had was that I needed to run a worker provided by a partner and therefore served from a different domain. So new Worker(...); will fail with an error similar to this:
Uncaught SecurityError: Failed to construct 'Worker': Script at 'xxx' cannot be accessed from origin 'xxx'

Cross domain workers

So the browser will not allow you to create a worker with a URL pointing to a different domain. But it will allow you to create a blob URL which can be used to initialize your worker.

Blob URLs

A blob is in general something which doesn’t necessarily in JavaScript “format” but it can be. You can then have the browser internally generate a URL. This URL uses a pseudo protocol called “blob”. So you get a URL in this form: blob:origin/UID. The origin is the origin of the page where you create the blob URL and the UID is a generated unique ID e.g. blob:https://mydomain/8126d58c-edbc-ee14-94a6-108b8f215304.

A blob can be created this way:

var blob = new Blob(["some JavaScript code;"], { "type": 'application/javascript' });

The following browser versions seem to support the blob constructor: IE 10, Edge 12, Firefox 13, Chrome 20, Safari 6, Opera 12.1, iOS Safari 6.1, Android browser/Chrome 53. So if you want to support an older version you will need to revert to the BlobBuilder interface has been deprecated in favor of the newly introduced Blob constructor in the newer browsers:

var blobBuilder = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)();
blobBuilder.append("some JavaScript code;");
var blob = blobBuilder.getBlob('application/javascript');

In order to support old and new browsers, you will want to try using the Blob constructor and revert to the BlobBuilder in case you get an exception:

var blob;
try {
	blob = new Blob(["some JavaScript code;"], { "type": 'application/javascript' });
} catch (e) {
	var blobBuilder = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)();
	blobBuilder.append("some JavaScript code;");
	blob = blobBuilder.getBlob('application/javascript');
}

You can then generate a URL object from the blob:

var url = window.URL || window.webkitURL;
var blobUrl = url.createObjectURL(blob);

Finally, you can create your web worker using this URL object:

var worker = new Worker(blobUrl);

Now, the piece of JavaScript you want to have in your blob would be this one liner which will load the worker file:

importScripts('https://mydomain.com/worker.js');

So a method to load, create and return the worker both in case we are in a same-domain scenario or in a cross-domain scenario would look like this:

function createWorker (workerUrl) {
	var worker = null;
	try {
		worker = new Worker(workerUrl);
	} catch (e) {
		try {
			var blob;
			try {
				blob = new Blob(["importScripts('" + workerUrl + "');"], { "type": 'application/javascript' });
			} catch (e1) {
				var blobBuilder = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)();
				blobBuilder.append("importScripts('" + workerUrl + "');");
				blob = blobBuilder.getBlob('application/javascript');
			}
			var url = window.URL || window.webkitURL;
			var blobUrl = url.createObjectURL(blob);
			worker = new Worker(blobUrl);
		} catch (e2) {
			//if it still fails, there is nothing much we can do
		}
	}
	return worker;
}

Cross-browser support

Unfortunately, we still have another problem to handle: in some browser, the failed creation of a web worker will not result in an exception but with an unusable worker in the cross-domain scenario. In this case, an error is raised as an event on the worker. So you would need to consider this also as a feedback that the creation of the worker failed and that the fallback with the blob URL should be used.

In order to do this, you should probably first extract the fallback into its own function:

function createWorkerFallback (workerUrl) {
	var worker = null;
	try {
		var blob;
		try {
			blob = new Blob(["importScripts('" + workerUrl + "');"], { "type": 'application/javascript' });
		} catch (e) {
			var blobBuilder = new (window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder)();
			blobBuilder.append("importScripts('" + workerUrl + "');");
			blob = blobBuilder.getBlob('application/javascript');
		}
		var url = window.URL || window.webkitURL;
		var blobUrl = url.createObjectURL(blob);
		worker = new Worker(blobUrl);
	} catch (e1) {
		//if it still fails, there is nothing much we can do
	}
	return worker;
}

Now we can implement the logic to handle the different cases:

var worker = null;
try {
	worker = new Worker(workerUrl);
	worker.onerror = function (event) {
		event.preventDefault();
		worker = createWorkerFallback(workerUrl);
	};
} catch (e) {
	worker = createWorkerFallback(workerUrl);
}

Of course, you could save yourself this try/catch/onerror logic and just directly use the fallback which should also work in all browsers.

Another option I’ve been using is still trying the get the worker to get initialized with this logic but only in case of same domain scenarios.

In order to do this, you’d need to first implement a check whether we are in a same-domain or a cross-domain scenario e.g.:

function testSameOrigin (url) {
	var loc = window.location;
	var a = document.createElement('a');
	a.href = url;
	return a.hostname === loc.hostname && a.port === loc.port && a.protocol === loc.protocol;
}

It just creates an anchor tag (which will not be bound to the dom), set the URL and then checking the different part of the URL relevant for identifying the origin (protocol, hostname and port).

With this function, you can then update the logic in this way:

var worker = null;
try {
	if (testSameOrigin(workerUrl)) {
		worker = new Worker(workerUrl);
		worker.onerror = function (event) {
			event.preventDefault();
			worker = createWorkerFallback(workerUrl);
		};
	} else {
		worker = createWorkerFallback(workerUrl);
	}
} catch (e) {
	worker = createWorkerFallback(workerUrl);
}

This may all sounds overly complex just to end up using a web worker but unfortunately because of cross-domain restrictions and implementation inconsistencies between browser, you very often need to have such things in your code.

 

 

Leave a Reply

Your email address will not be published. Required fields are marked *