Events, Concurrency and JavaScript

Modern web apps are inherently event-driven yet much of the browser internals for triggering, executing, and handling events can seem as black box. Browsers model asynchronous I/O thru events and callbacks, enabling users to press keys and click mouses while XHR requests and timers trigger in code. Understanding how events work is critical for crafting high performance JavaScript. In this post we’ll focus on the browser’s built-in Web APIs, callback queues, event loops and JavaScript’s run-time.

Code in action. A button and event handler.

<button id="doStuff">Do Stuff</button>

<script>
    document.getElementById('doStuff')
        .addEventListener('click', function() {
                console.log('Do Stuff');
            }
        );
</script>

Let’s trace a Do Stuff click event thru browser and describe the components along the way.

webapi5.png
From Philip Robert’s diagram


 Browser Runtime

  1. User Interface - User clicks the Do Stuff button. Simple enough.

  2. Web APIs - The click event propagates thru the DOM’s Web API triggering click handlers during the capture and bubble phases on parent and child elements. Web APIs are a multi-threaded area of the browser that allows many events to trigger at once. They become accessible to JavaScript code thru the familiar window object on page load. Examples beyond the DOM’s document are AJAX’sXMLHttpRequest, and timers setTimeout() function[1].

  3. Event Queue - Next the event’s callback is pushed into one of many event queues (also called task queues). Just as there are multiple Web APIs, browsers have event queues for things like network requests, DOM events, rendering, and more[2].

  4. Event loop - Then a single event loop chooses which callback to push onto the JavaScript call stack[3]. Here’s C++ pseudo code for Firefox’s event loop.

while(queue.waitForMessage()){
    queue.processNextMessage();
}

[4]

Finally the event callback enters the JavaScript’s runtime within the browser.


 JavaScript Runtime

The JavaScript engine has many components such as a parser for script loading, heap for object memory allocation, garbage collection system, interpreter, and more. Like other code event handlers execute on it’s call stack.

5.  Call Stack - Every function invocation including event callbacks creates a new stack frame (also called execution object). These stack frames are pushed and popped from the top of the call stack, the top being the currently executing code[5]. When the function is returned it’s stack frame is popped from the stack.

Chrome’s V8 C++ source code of single stack frame:

/**
 *  v8.h line 1372 -- A single JavaScript stack frame.
 */
class V8_EXPORT StackFrame {
 public:
    int GetLineNumber() const;
    int GetColumn() const;
    int GetScriptId() const;
    Local<String> GetScriptName() const;
    Local<String> GetScriptNameOrSourceURL() const;
    Local<String> GetFunctionName() const;
    bool IsEval() const;
    bool IsConstructor() const;
};

[6]

Three characteristics of JavaScript’s call stack.

Single threaded - Threads are basic units of CPU utilization. As lower level OS constructs they consist of a thread ID, program counter, register set, and stack[7]. While the JavaScript engine itself is multi-threaded it’s call stack is single threaded allowing only one piece of code to execute at a time8.

Synchronous - JavaScript call stack carries out tasks to completion instead of task switching and the same holds for events. This isn’t a requirement by the ECMAScript or WC3 specs. But there are some exceptions like window.alert() interrupts the current executing task.

Non-blocking - Blocking occurs when the application state is suspended as a thread runs[7]. Browsers are non-blocking, still accepting events like mouse clicks even though they may not execute immediately.


 CPU Intensive Tasks

CPU intensive tasks can be difficult because the single-threaded and synchronous run-time queues up other callbacks and threads into a wait state, e.g. the UI thread.

Let’s add a CPU intensive task.

<button id="bigLoop">Big Loop</button>
<button id="doStuff">Do Stuff</button>
<script>
    document.getElementById('bigLoop')
        .addEventListener('click', function() {
            //  big loop
            for (var array = [], i = 0; i < 10000000; i++) {
                array.push(i);
            }
        });

    document.getElementById('doStuff')
        .addEventListener('click', function() {
            //  message
            console.log('do stuff');
        });
</script>

Click Big Loop then Do Stuff. When Big Loop handler runs the browser appears frozen. We know JavaScript’s call stack is synchronous soBig Loop executes on the call stack until completion. It’s also non-blocking where Do Stuff clicks are still received even if they didn’t execute immediately.

Checkout this CodePen to see.


 Solutions

1) Break the big loop into smaller loops and use setTimeout() on each loop.

...
    document.getElementById('bigLoop')
        .addEventListener('click', function() {
            var array = []
            // smaller loop
            setTimeout(function() {
                 for (i = 0; i < 5000000; i++) {
                     array.push(i);
                 }
            }, 0);
            // smaller loop
            setTimeout(function() {
                 for (i = 0; i < 5000000; i++) {
                     array.push(i);
                 }
            }, 0);
        });

setTimeout() executes in the WebAPI, then sends the callback to an event queue and allows the event loop to repaint before pushing it’s callback into the JavaScript call stack.

2) Use Web Workers, designed for CPU intensive tasks.


 Summary

Events trigger in a multi-threaded area of the browser called Web APIs. After an event (e.g. XHR request) completes, the Web API passes it’s callback to the event queues. Next an event loop synchronously selects and pushes the event callback from the callback queues onto JavaScript’s single-threaded call stack to be executed. In short, events trigger asynchronously but their handlers execute on the call stack synchronously.

In this post we covered browser’s concurrency model for events including Web APIs, event queues, event loop, and JavaScript’s runtime. I’d enjoy questions or comments. Feel free to reach out via Linkedin.


 References

[1] W3C Web APIs

[2] W3C Event Queue (Task Queue)

[3] W3C Event Loop

[4] Concurrency modal and Event Loop, MDN

[5] ECMAScript 10.3 Call Stack

[6] V8 source code include/v8.h line 1372.

[7] Silberschatz, Galvin, Gagne, Operating System Concepts 8th Ed. page 153, 570

[8] V8 source code src/x64/cpu-x64.cc


 
138
Kudos
 
138
Kudos

Now read this

The Anatomy of a JavaScript Function

Functions in JavaScript are like classes to Java. They’re the fundamental modular unit: the cell in life, the note in music, the word in language, the funky chicken in dance. JavaScript functions are full fledged objects, often called... Continue →