Introducing Javascript Dedicated Workers and Shared Workers

Introducing Javascript Dedicated Workers and Shared Workers

Workers is a new technique in javascripts to bring thread and background tasks to execute highly expensive operations without user interruption, so let’s discuss workers in this topic.

 

 

 

Bringing thread techniques is a huge improvement in javascript. Javascript new Apis like workers provide us to do background operations, parallel requests and more.This increases user experience by not interrupting users while interacting with web pages.

Javascript worker in simple terms is an object created using a constructor for example Worker(worker url), this constructor takes url of javascript file that contains the worker code to execute in the background. Let’s first take a look and the types of workers in javascript.

Workers Types

  • Dedicated Workers: Created using the Worker() constructor.
  • Shared Workers: Created using the SharedWorker() constructor.
  • Service Workers: This is a special kind of workers that enable offline working with web applications we will discuss this in a future lesson.

 

How Workers Work

Workers work by sending and receiving messages from the main thread to the worker thread, it’s like socket connections when you emit an event and listen for events so we have methods like postMessage() to send message, and onmessage(event) event handler to listen for messages, the event argument here contains the data posted using postMessage().

 

Workers Execution Context

Workers execute in another global context different from the window context thereby not all method and functions of the window object is available under workers. The worker context is represented by DedicatedWorkerGlobalScope in case of dedicated workers and SharedWorkerGlobalScope in case of shared workers.

 

Functions & Classes Available in Workers

Note that by logic that workers execute code in background you can run any code you want but there are exceptions to that for example DOM manipulation is forbidden, also you can not use some default functions of the window object is also forbidden. In the other hand you can send Http requests using XMLHTTP or fetch() api, Web sockets.

These are some of functions you can use in Workers:

  • Standard functions such as String, Array, JSON, Object, etc.
  • Console Api
  • Fetch
  • FileReader
  • FormData, ImageData
  • IndexedDB
  • Promise
  • XMLHTTPRequest

For the full list of functions refer to the mozilla docs.

 

Dedicated Workers

The first type of workers is the dedicated workers, this is simple workers. This worker is only accessible by the script that is called it. So in this case we have one main thread and the worker thread. To create a dedicated worker:

const apiWorker = new Worker('worker.js');

The Worker constructor accepts url of javascript file i.e the worker file.

Let’s see an example using this worker, as we will make make http call to JsonPlaceholder apis to fetch sample posts.

Create a new folder with these files inside:

  • index.html
  • main.js
  • worker.js

index.html

<!DOCTYPE html>
<html>
<head>
	<title>Dedicated workers</title>
</head>
<body>
	<button id="fetch_posts">Fetch posts</button>

	<section id="posts_wrapper">
		
	</section>

	<script src="./main.js"></script>
</body>
</html>

main.js

if(window.Worker) {

	const apiWorker = new Worker('worker.js');

	const fetchBtn = document.querySelector("#fetch_posts");

	const postsWrapper = document.querySelector("#posts_wrapper");


	fetchBtn.onclick = () => {

		apiWorker.postMessage({start_fetch: 1});
	}

	apiWorker.onmessage = (event) => {

		if(event.data.posts) {
			postsWrapper.innerHTML = renderPosts(event.data.posts);
		} else if(event.data.error_message) {
			postsWrapper.textContent = event.data.error_message;
		}
	}
}

const renderPosts = (posts) => {
	let html = "<h2>Latest Posts</h2>";

	posts.map(post => {
		html += `
			<article>
 				<h3>${post.title}</h3>
 				<p>${post.body}</p>
			</article>
			<hr/>
		`;
	});

	return html;
}

worker.js

self.onmessage = (event) => {

	if(event.data.start_fetch) {

		// using javascript fetch api
		fetch('https://jsonplaceholder.typicode.com/posts').then(response => 
			response.json()
		).then(data => {
			self.postMessage({posts: data.slice(0, 20)});
		}).catch(err => {

			self.postMessage({error_message: err});
		});
	}
}

In the above example i am fetching dummy posts data from jsonplaceholder website, so in the index.html file i added a button and an html <section> element, when we click the button we fetch the posts by posting message to the worker.

The main.js represent our main thread file so we included this file in the bottom of index.html and we added the required code to initialize the dedicated worker object.

At the top of main.js we check for Worker support by using window.Worker, if it’s true then the browser we work with supports dedicated workers, you can look at the caniuse website to check if your browser version supports web worker.

Next we initialize a worker object using the Worker() constructor and pass it worker.js which is our worker thread file. Now we need to post a message to worker when we click the button for this i added an event listener when the button is clicked as shown:

fetchBtn.onclick = () => {
    apiWorker.postMessage({start_fetch: 1});
}

As you see the postMessage() method can accept any kind of data, i prefer to work with objects, so i send a simple object which has a key start_fetch, once this data object is received in the worker we make http call to fetch to posts.

Now let’s go to worker.js, we need to listen of the message sent by our main thread (main.js) so inside worker.js i acheived this by calling onmessage() event handler. Note that i use self.onmessage(), self is a special keyword here like window object, but in worker context window object gives undefined so we use the “self” keyword. You can also call onmessage() directory.

We can get event data from onmessage() by accessing the data property like event.data, this event data is of type MessageEvent. Then i checked for start_fetch key. If start_fetch is present this means the user clicked on the button, we call the fetch() promise based api to make http call to https://jsonplaceholder.typicode.com/posts. On successful response we post message back to the main thread using self.postMessage() passing in the data. On error we post message with the error data.

Return back to main.js we need to receive the response coming from our worker, again we call onmessage() event handler on apiWorker object like shown:

apiWorker.onmessage = (event) => {

		if(event.data.posts) {
			postsWrapper.innerHTML = renderPosts(event.data.posts);
		} else if(event.data.error_message) {
			postsWrapper.textContent = event.data.error_message;
		}
	}

Rendering posts is a matter of iterating over the post data and display them in article element so i added a simple function renderPosts() to render posts in DOM.

From this example as you see the workers threads interact with each other by sending and receiving messages and vice versa.

If you run this in chrome after clicking the fetch posts button, and displaying the results, to check the workers in document go to chrome developer tools > Sources tab you will notice there are two threads (main and worker.js).

javascript dedicated workers

Using External Scripts Inside Worker files

Worker files don’t have access to the scripts available in the document so for this purpose if we need to access external scripts, plugins or libraries inside workers we can load them using importScripts() function.

importScripts('plugin.js');
importScripts('plugin1.js', 'plugin2.js');
importScripts('//example.com/foo.js');

 

Terminating a Worker

We can terminate a worker explicitly by calling terminate() method like so:

apiworker.terminate();

 

Shared Workers

This is the complex type of workers as it’s not fully supported right now in all major browsers. The shared worker have the same features and same methods like dedicated worker except that it accessible by multiple scripts, even if they are accessed by different windows, frames.

If you accessed shared workers from several browsing contexts i.e multiple windows all those browsing contexts must have the same origin (same protocol, same host, same port).

To initialize shared workers we can do this like so:

apiWorker = new SharedWorker('worker.js');

To send and receive messages between workers and the main thread, a connection to specific port is needed, we can do so by calling a accessing a port object on the worker instance, this port is opened automatically when sending and receiving messages but we can open the port connection explicitly by calling:

apiWorker.port.start();

Then to send message a call to postMessage() in the context of shared workers:

apiWorker.port.postMessage(data);

In the same way to listen to message events with onmessage():

apiWorker.port.addEventListener("message", function(e) {
	// 
}, false);

As you see you can invoke any method inside shared worker through the port object.

Let’s see an example which is based on the previous example, in this example we have two scripts that access the same worker, the first script send message to fetch posts and the second script send message to fetch photos.

Create a new folder project named “shared-workers” inside it create these files:

  • posts.html
  • photos.html
  • posts.js
  • photos.js
  • worker.js

posts.html

<!DOCTYPE html>
<html>
<head>
	<title>Posts</title>
</head>
<body>
	<button id="fetch_posts">Fetch posts</button>

	<section id="posts_wrapper">
		
	</section>

	<script src="./posts.js"></script>
</body>
</html>

photos.html

<!DOCTYPE html>
<html>
<head>
	<title>Photos</title>
</head>
<body>
	<button id="fetch_photos">Fetch photos</button>

	<section id="photos_wrapper">
		
	</section>

	<script src="./photos.js"></script>
</body>
</html>

posts.js

if(window.SharedWorker) {

	apiWorker = new SharedWorker('worker.js');

  	const fetchBtn = document.querySelector("#fetch_posts");

	 const postsWrapper = document.querySelector("#posts_wrapper");


	fetchBtn.onclick = () => {

		apiWorker.port.postMessage({start_fetch_posts: 1});
	}

	apiWorker.port.start();

	apiWorker.port.addEventListener("message", (event) => {

		if(event.data.data) {
			postsWrapper.innerHTML = renderPosts(event.data.data);
		} else if(event.data.error_message) {
			postsWrapper.textContent = event.data.error_message;
		}
	}, false);
}

const renderPosts = (posts) => {
	let html = "<h2>Latest Posts</h2>";

	posts.map(post => {
		html += `
			<article>
 				<h3>${post.title}</h3>
 				<p>${post.body}</p>
			</article>
			<hr/>
		`;
	});

	return html;
}

photos.js

if(window.SharedWorker) {

	apiWorker = new SharedWorker('worker.js');

	const fetchBtn = document.querySelector("#fetch_photos");

	const photosWrapper = document.querySelector("#photos_wrapper");


	fetchBtn.onclick = () => {

		apiWorker.port.postMessage({start_fetch_photos: 1});
	}

	apiWorker.port.start();

	apiWorker.port.addEventListener("message", (event) => {

		if(event.data.data) {
			photosWrapper.innerHTML = renderPhotos(event.data.data);
		} else if(event.data.error_message) {
			photosWrapper.textContent = event.data.error_message;
		}
	}, false);
}

const renderPhotos = (photos) => {
	let html = "<h2>Latest Photos</h2>";

	photos.map(photo => {
		html += `
			<article>
 				<h3>${photo.title}</h3>
 				<img src="${photo.url}" />
			</article>
			<hr/>
		`;
	});

	return html;
}

worker.js

self.addEventListener("connect", (event) => {

	const port = event.ports[0];

    port.addEventListener("message", (event) => {

       var url = "";

       if(event.data.start_fetch_posts) {
         url = "https://jsonplaceholder.typicode.com/posts";
       } else {
       	 url = "https://jsonplaceholder.typicode.com/photos";
       }

       // using javascript fetch api
		fetch(url).then(response => 
			response.json()
		).then(data => {
			port.postMessage({data: data.slice(0, 20)});
		}).catch(err => {

			port.postMessage({error_message: err});
		});

    }, false);

    port.start();

}, false);

As you see in the above code we have two scripts that access the same worker and this is the main difference between shared and dedicated workers, of course we can have as many scripts as you want for accessing the shared worker.

The first script (posts.js) represent our first main thread, this thread sends message flag (start_fetch_posts) to fetch posts, and to render them as we did with dedicated workers. I focus on the differences here as shown each method in worker instance must be invoked through the port object like apiWorker.port.postMessage().

Note that i opened port connection explicitly using apiWorker.port.start() before posting any messages.

Also we listen for the incoming message from the worker thread using onmessage() or using javascript addEventListener() event handler as shown:

apiWorker.port.addEventListener("message", (event) => {

		if(event.data.data) {
			postsWrapper.innerHTML = renderPosts(event.data.data);
		} else if(event.data.error_message) {
			postsWrapper.textContent = event.data.error_message;
		}
	}, false);

The second script (photos.js) represent our second main thread, this thread sends message flag (start_fetch_photos) to fetch photos, the rest of the code the same as posts.js

Now the worker thread (worker.js) at first we listen for the connect event, this is required step to insure that the main threads is already connected via port before sending any messages this is done by calling self.addEventListener(“connect”, callback).

Next inside the connect listener we have to get the active port that connected with event.ports[0]. Using this port we can listen to messages as shown:

port.addEventListener("message", (event) => {

       var url = "";

       if(event.data.start_fetch_posts) {
         url = "https://jsonplaceholder.typicode.com/posts";
       } else {
       	 url = "https://jsonplaceholder.typicode.com/photos";
       }

       // using javascript fetch api
		fetch(url).then(response => 
			response.json()
		).then(data => {
			port.postMessage({data: data.slice(0, 20)});
		}).catch(err => {

			port.postMessage({error_message: err});
		});

    }, false);

Then i check for the right flag so we can fetch either posts or photos and then posting the message again to the main thread using also the port object we just retrieved.

Now i run this through a local server and opened two browser tabs (http://localhost/shared-workers/posts.html) and (http://localhost/shared-workers/photos.html)

 

 

 

0 0 vote
Article Rating
Share this: