Further Adventures of

The Event Loop

  • Erin Zimmer
  • twitter.com/ErinJZimmer
  • ejzimmer.github.io/event-loop-talk

What actually is the Event Loop?

The Browser
Web APIs
The Browser
JavaScript Engine
Web APIs
^
JavaScript Engine

while (true) {
 task = taskQueue.pop();
 execute(task);
}
					
  • What's a task?
  • What's a task queue?
  • How do tasks
    get in the task queue?


					
^

JavaScript, why are you like that?

JavaScript is single-threaded

- But remember, the JS engine is only one part of the browser

Browsers are multi-threaded

As well as running your JavaScript, a browser could be - keeping track of mouse and keyboard events - making network requests - handling timers - disk operations And any of these things could be generating tasks, via callbacks
The Browser
In fact, this idea is the very heart of the asynchronous programming we all know and love so much
setTimeout(myCallbackFunction, 3000);
Hey WebAPIs! Could you wait 3 seconds and then run my callback function?
No worries! You keep doing your thing, I'll take care of this.

while (true) {
 task = taskQueue.pop();
 execute(task);
}
					
So that's basically how tasks and task queues work. Of course, in real life, it's a bit more complicated than that

The Rendering Pipeline

Long running tasks will cause your browser to start dropping frames and, much like this cat, your app just won't run right
function repeat(reps, action) {
 action();
 if (--reps) {
  setTimeout(() => repeat(reps, action));
 }
}					

/*  job.js  */
onmessage = function(e) {
 for (let i = 0; i < e.data.reps; i++) {
  e.data.action();
 }
}

/*  app.js  */
const worker = new Worker('job.js');
worker.postMessage(action, reps);
					

Why don't web workers interfere with rendering?

You all remember when we were talking about long-running tasks and I said you could use web workers, because they were guaranteed not to interfere with the rendering pipeline? So, given everything we now know about how the event loop works in browsers, how can I be so sure? Because I read the spec...
Each WorkerGlobalScope object has a distinct event loop, separate from those used by units of related similar-origin browsing contexts.
Each web worker has its own event loop.
Main browser window
job.js
  • No user interactions
  • No rendering pipeline
  • No DOM at all!
If you've ever had a look at your Activity Monitor or Task Manager while Chrome is running, you probably saw something like this... Chrome likes to run things in lots of different processes. If you have a look at the Chrome Task Manager, you can see what each of those processes is actually doing. (Do it) And you would see that you've got a process for each tab, each browser extension, and a couple of extras. The important thing here, is that we have a process for each tab. Because each of those tabs is running in a separate process, they must have their own event loop. This means that if we start some resource heavy process in one tab, none of the others are affected. This is a big advantage of this process per tab model. Each tab is isolated so it doesn't affect the security or performance of the other tabs.
It doesn't have to be this way though. If you open all the same tabs in Firefox and check out the task manager, you see something like this: just four processes for all of your tabs. This means, if you've got a bunch of tabs open, some of them will be sharing an event loop. So, there is potential for some badly behaved tab to interfere with the performance of other tabs, but it also means you've got some RAM left over to do other things.
So, generally speaking, it's up the browser whether it runs your JS in its own event loop, or makes you share with other apps. There is an important exception to this rule - shared browsing contexts always share an event loop
<a target="_blank" href="button.html"></a>
There's a good reason for this - these windows have access to each others' DOM - demo changing background colour on opener - so they need to be on the same event loop, else there would be issues window.opener.document.body.style.backgroundColor & window.opener.document.querySelector('iframe').contentWindow.document.body.style.backgroundColor This isn't a security issue, because it only works on same-origin windows and frames - if you try it on cross-origin windows...
<a target="_blank" rel="noopener" href="button.html"></a>

while (true) {
 task = taskQueue.pop();
 execute(task);

 if (isRepaintTime()) repaint();
}
					
An event loop has one or more task queues.

bool did_work = delegate->DoWork();
if (!keep_running_)
 break;
did_work |= delegate->DoDelayedWork(&delayed_work_time_);
if (!keep_running_)
 break;
if (did_work)
 continue;
did_work = delegate->DoIdleWork();
if (!keep_running_)
 break;
					

Multiple task queues

  • Queues can be executed in any order
  • Tasks in the same queue must be executed in the order they arrived
  • Tasks from the same source
    must go in the same queue

while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 if (isRepaintTime()) repaint();
}
					

Microtasks

A task that happens between tasks
Between one task and the next, or between task and rendering

const observer = new MutationObserver(callback);
const myElement = document.getElementById('stegosaurus');
observer.observe(myElement, ({ subtree: true }));
					
Potentially lots of things happening Changes to DOM, want to run things related to changing DOM before window renders again

const myPromise = new Promise((resolve, reject) => { ... });
myPromise.then(callback).catch(errorCallback);
					
Performance reasons Esp catch -> want error handling to happen after stuff, but before anything else

window.queueMicrotask(callback);
					
Only in Chrome, generally intended for people writing frameworks

Tasks
vs
Microtasks


while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 while (microtaskQueue.hasTasks())
  doMicrotask();

 if (isRepaintTime()) repaint();
}
					

Animation Frame Callback Queue


requestAnimationFrame(callback);
					
Why would we want to do this? Well, imagine we wanted to make a nice animation of a box moving along a path.
while (box.style.right < screen.width) {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime)
}

					
If you did that, what you would get is this...
function move() {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime);
 if (box.style.right < screen.width)
  setTimeout(move);
}
					
function move() {
 const elapsedTime = Date.now() - startTime;
 box.style.left = calculateX(elapsedTime);
 box.style.top = calculateY(elapsedTime);
 if (x < screen.width)
  requestAnimationFrame(move);
 }
}

while (true) {
 queue = getNextQueue();
 task = queue.pop();
 execute(task);

 while (microtaskQueue.hasTasks()) 
  doMicrotask();

 if (isRepaintTime()) {
  animationTasks = animationQueue.copyTasks();
  for (task in animationTasks) 
   doAnimationTask(task);
		
  repaint();
 }
}
					

Node

libuv
It still consists of an engine and a bunch of supporting APIs. Although, in Node, they're not called Web APIs, because, obviously, that would be weird. Instead they're called the unicorn velociraptor library. Or libuv.
  • No DOM
  • Limited user interactions
  • No windows
The event loop itself is much simpler - no DOM - just straight up JS, no rendering pipeline, no animation frame queue - user interactions when you ask for them, none of this hanging around letting them click wherever the whim takes them - no windows = no sharing event loops, worrying about cross-origin anything - there are workers, but as we saw before, they're dead simple
setImmediate(callback)
setTimeout(callback, 0)
setImmediate(callback)
process.nextTick(callback)
setImmediate():
do something on the next tick
process.nextTick():
do something immediately

while (tasksAreWaiting()) {
 queue = getNextQueue();

 while (queue.hasTasks()) {
  task = queue.pop();
  execute(task);

  while (nextTickQueue.hasTasks()) 
   doNextTickTask();

  while (promiseQueue.hasTasks()) 
   doPromiseTask();
 }
}
					
  • Don't block rendering
  • Use web workers
  • Always use rel="noopener"
  • Promises beat tasks
  • Animate with requestAnimationFrame
setTimeout(fn, 0)

Thanks! (rawr)