Skip to main content

21 steps to super speedy JavaScript

Speed up JavaScript
(Image credit: Pexels.com)

At first glance, parallel processing sounds like an invitation to free lunch – being able to use multi-core CPUs more efficiently should give your code a tremendous speed boost. Practical experience shows that this is not always the case: some problems are impossible to parallelise. In addition to that, parallel execution leads to an entire family of new problems that are not seen on single-core machines.

Parallel code can, in principle, be broken down into two groups of job. The first group is classic speed increase – if your program has to sift through 3000 images, splitting the job up means that more images can be processed at any one time. While definitely beneficial, this kind of task is rarely encountered in everyday web development.

The more common form of parallelisation job works around long-running tasks which do not require much CPU time. A good example for this would be waiting for a download or some kind of device input: if this is done in a parallelised fashion, the rest of the GUI does not need to be blocked. Given that users tend to have issues understanding 'why the button doesn’t click', this can lead to increased user satisfaction even if actual speed does not increase.

If you need some new resources to help you code smarter, check out our guide to the best web design tools of 2019.

01. Intervalled execution

var sleep = require('sleep');

function worker(){
    console.log("Worker up")
    sleep.msleep(1000)
    console.log("Worker down")
}

setInterval(worker, 2000);
while(1==1)
{
    sleep.msleep(250)
    console.log("Hello from main")
}>

Let us start out with a small example based on a well-liked favourite: the setInterval function. It takes a function reference and a numeric value delineated in milliseconds. After that, the function is periodically fired up whenever the delays timer expires.

02. Setting up for a fall

npm install sleep
npm example1.js

Using the sleep() function requires us to load the sleep module to a local npm project. It acts as an interface to the operating system’s sleep library – do not wonder if your workstation’s compiler is fired up during the deployment of the package.

03. Perils of multitasking

Speed up JavaScript: 03

(Image credit: Tam Hanna)

When running this program, you will find yourself confronted with output similar to the one shown in the figure accompanying this step. It is obvious that our worker never gets invoked – something must be wrong with the setInterval function.

04. Perils of multitasking, part 2

Modern browsers equip each tab with one JavaScript thread. A careful look at our loop reveals that it is endless. This means that it will run forever, and will never yield control. Given that setInterval() works with a message, its payload never gets run as the message handler is blocked from running.

05. Focus on troublemakers

const bar = () => console.log('bar')

const baz = () => console.log('baz')

const foo = () => {
  console.log('foo')
  setTimeout(bar, 0)
  baz()
}

foo()

Flavio Copes' blog, provides an extremely interesting bit of code illustrating the problem. When run, it yields the output shown in the figure due to the message queue being blocked until foo() relinquishes control of the main thread.

06. Introduce threading

Speed up JavaScript: 06

(Image credit: Tam Hanna)

Given that the underlying operating system is able to perform preemptive multitasking, let us harness its capabilities by spawning threads via a dedicated API called WebWorkers, which enjoy wide-ranging support. The figure accompanying this step shows the current listing for the feature.

07. Create an extra file

console.log("Worker says hello!");
console.log("Worker up");
while (1==1)
{
   console.log("x");
}

WebWorkers cannot kick off with a function payload. Instead, a new file is required which contains the code intended to run in the thread. In the case of our example, example7thread.js has the content shown accompanying this step.

08. Run the worker...

const worker = require('worker_threads');

var myWorker = new worker.Worker('./
example7thread.js');
var myWorker2 = new worker.Worker('./
example7thread.js');

Our worker is ready for prime-time. Specialities of the Node.JS runtime force us to include the worker threads module and pass in a relative path – problems not faced in the browser. Furthermore, do not wonder about the missing start() call – a web worker sets off when the instance comes online.

09. ...and understand its results

Speed up JavaScript: 09

(Image credit: Tam Hanna)

Message delivery usually requires some kind of interaction between the runtime and the rest of the operating system. Sadly, our endless loop blocks this process – which means that only one of the messages pops up before 'the big stall'.

10. Add the mask

Speed up JavaScript: 10

(Image credit: Tam Hanna)

const worker = require('worker_threads');

var myWorker = new worker.Worker('./
example7thread.js');
var myWorker2 = new worker.Worker('./
example7thread.js');


while (1==1)
{

}

Another experiment involves placing an empty loop after the two constructor invocations. In this case, the threads will never start working – the invocation message never arrives with the operating system.

11. Interprocess communication

While workers running tight endless loops do have the problems exhibited above, routines can communicate with one another. This is done via a message passing interface – think of it as a facility which lets you pass a message object from a sender to a recipient across thread boundaries.

12. Why message-pass?

In addition to the benefits of being able to coordinate threads efficiently (and reduced risk of 'collisions', also known as race conditions), message passing to the main thread is the only way to interact with the DOM, due to the difficulty of creating thread-safe GUI stacks.

13. Move to the browser

Implementing message passing in Node.JS is tedious. Move the code to the web – start out by creating a HTML harness that loads examplempi.js. Be aware that this code can only be run from a localhost web server due to DOM origin restrictions.

14. Set up a mailbox...

var myWorker = new Worker('./
examplempithread.js');
var myWorker2 = new Worker('./
examplempithread.js');


myWorker.onmessage=function (e)
{
	console.log(e);
}

myWorker2.onmessage=function (e)
{
	console.log(e);
}

Each worker exposes an onmessage property which takes up a function reference that must be invoked whenever a message pops up from the other end. Incoming messages are simply forwarded to the console of the browser from the main thread.

15. ...and feed it from the worker

postMessage("Worker says hello!");
postMessage("Worker up");
while (1==1)
{
   postMessage("x");
}

Sending messages to a mailbox is accomplished by invoking the post Message function. Keep in mind that a worker can also implement an onmessage event, which could receive information from whoever 'owns' the worker instance object.

16. Kick it off...

At this point in time, the code is ready to run. The developer console isn’t flooded with messages due to the high efficiency of the MPI interface, meaning data can travel around the system efficiently.

17. Async and await

Speed up JavaScript: 17

(Image credit: Tam Hanna)

Microsoft’s addition of the async and await keywords modified the history of C# and VisualBasic.Net. In principle, a method marked async is run cooperatively with the rest of the program. Results can then be harvested via the await function.

18. Replace our sleeper

const sleep = (milliseconds) => {
  return new Promise(resolve => 
setTimeout(resolve, milliseconds))
}

Given that browsers are under severe limitations when it comes to accessing native functions, we cannot reuse the sleep function mentioned before. It, furthermore, is suboptimal in that it halted the entire Node.JS runtime. The snippet accompanying this step provides a more effective approach.

19. Understand the code...

This code returns a promise object – it is another convenience class added to Java Script in an attempt to make multithreading easier. You will need to invoke its resolve method via a setTimeout, ensuring that the code has to 'sit out' quite a bit of time.

20. ...and deploy it somewhere

Await calls are permitted only inside of async functions. This means that the worker is in need of a rewrite – start off by invoking an async bearer function. It handles the rest of the code interaction.

const sleep = (milliseconds) => {
  return new Promise(resolve => 
setTimeout(resolve, milliseconds))
}
postMessage("Worker says hello!");
postMessage("Worker up");
worker()
async function worker(){
while (1==1)
{
   postMessage("x");
   await sleep(1000);
}
}

21. Harness the power of native!

Developers working on Node.JS code should not forget that they can – usually – also create completely native modules if extremely high performance is needed. These can not only take advantage of the various operating system APIs, but can also be written in programming languages that compile to machine code.

This article was originally published in issue 291 of creative web design magazine Web Designer. Buy issue 291.

Read more: