Delan Azabani

Web workers in a single script file

 583 words 3 min  home

Web workers are an immensely useful tool, enabling concurrency and perhaps parallelism with JavaScript. Before their introduction, any non-trivial computation would completely lock up the user interface of the current document, or even the entire browser.

One could work around this by breaking up a large job into very small parts and chain them with timers, but this is highly inefficient and choosing an ideal amount of work for each part across all clients is almost impossible.

However, the interface for creating workers is tricky; the Worker constructor takes an external script's filename to execute. Suppose you are developing a JavaScript library that relies upon workers heavily. Are you forced to have two separate script files? No!

Interestingly, it is possible to have a single JavaScript file act as both a parent and a worker, but the implementation is not completely intuitive.

First of all, the single-file library must be aware of whether it is the parent or a worker. This can be achieved by detecting the presence of the DOM document:

var is_worker = !this.document;

Of course, if you need to execute the above in a context where this isn't the global object, then you can reliably obtain the global with something like

var global = (function(){ return this; })();

Surely, spawning a worker from the parent is now as simple as this, right?

var worker = new Worker('mylibrary.js');

Not quite. Worker paths are not resolved relative to the parent script file's path, but instead, relative to the path of the parent page. If the script isn't in the same directory as the page, the above will fail. Also, if the user has renamed the library file, workers will break.

The path of the script relative to the page must be used instead. This can be obtained by appending a dummy element with document.write and getting the previous element's src. The previous element, of course, is the script tag of the parent script, as conveniently for this situation, scripts block the building of the DOM tree while running.

var script_path = is_worker ? null : (function() {
	var id = +new Date + Math.random();
	document.write('<script id="dummy' + id + '"><\/script>');
	return document.getElementById('dummy' + id).
		previousSibling.src;
})();

The test for is_worker is present because DOM manipulation can only be done when the script isn't running as a worker. Thankfully, only the parent script needs to know the path to start workers, unless you want to start subworkers, but the method to do that already resolves paths relative to the worker's location.

A <script> tag is used as the dummy element because it's guaranteed to be a valid child element, be it <head> or <body>, unlike context-sensitive elements such as <div>. It's also not an element that might have side-effects on page display depending on CSS.

Now, workers can be spawned in the script with

var worker = new Worker(script_path);

The library in a single file might look something like this:

(function(global) {
	var is_worker = !this.document;
	var script_path = is_worker ? null : (function() {
		var id = +new Date + Math.random();
		document.write('<script id="dummy' + id + '"><\/script>');
		return document.getElementById('dummy' + id).
			previousSibling.src;
	})();
	function msg_from_parent(e) {
		// event handler for parent -> worker messages
	}
	function msg_from_worker(e) {
		// event handler for worker -> parent messages
	}
	function new_worker() {
		var w = new Worker(script_path);
		w.addEventListener('message', msg_from_worker, false);
		return w;
	}
	if (is_worker)
		global.addEventListener('message', msg_from_parent, false);
	// the rest of the library goes here
	// to spawn a worker, use new_worker()
})(this);