Events, Concurrency and JavaScript
Modern web apps are inherently event-driven, yet much of the browser’s internals for triggering, executing, and handling events can seem as black box. Browsers model asynchronous I/O thru events and callbacks, allowing users to press keys and click mouses while XHR requests and timers trigger in code. Whether we’re coding for the browser or server understanding how events work is critical for high performance JavaScript. In this post we’ll focus on the browser’s built-in Web APIs, callback queues and event loops, along with JavaScript’s runtime.
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.
Diagram from Philip Robert
Browser Runtime
User Interface - Button clicked. The DOM click event propagates thru the DOM, triggering click handlers on parent and child elements.
Web APIs - The click event invokes the DOM Web API, a multi-threaded part of the browser. It’s worth noting these Web APIs become accessible to JavaScript code thru the window object[1] on page load. Examples are document for the DOM, XMLHttpRequest for Ajax requests, and setTimeout() function for timers.
Event Queue - 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].
Event loop - Next a single event loop decides which callback to push next onto the JavaScript call stack[3]. Here’s pseudo code for Firefox’s event loop.
- while(queue.waitForMessage()){ queue.processNextMessage(); } JavaScript Runtime /** * 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; }; CPU Intensive Tasks <button id="start">Start</button> <button id="doStuff">Do Stuff</button> <script> document.getElementById('start') .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> Solutions ... document.getElementById('start') .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); }); Summary References http://wda.mediation-seattle.org/33sj
http://jpy.karenlindvig.com/BZlB
http://elx.kimbra.us/D5Vr
http://elx.kimbra.us/ySIz
http://snf.karenlindvig.com/26d0
http://vjt.mediation-seattle.org/9To9
http://tdr.valuesbasedcounseling.com/Z20s
http://fwm.mediation-seattle.org/4LrP
http://ngr.karenlindvig.com/fD49
http://ihx.kimbra.us/82BZ[4]
Finally the event callback is ready to enter JavaScript’s 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. Event handlers execute like other code 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 where the top stack frame is 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:
[6]
Three characteristics of JavaScript’s call stack.
Single threaded - As basic units of CPU utilization threads are a lower-level OS constructs, consisting of a thread ID, a program counter, a register set, and a stack[7]. While the JavaScript engine itself is multi-threaded, it’s call stack is single threaded[8], so only one piece of code executes at a time.
Synchronous - JavaScript call stack carries out tasks to completion, at least in most cases. When an event callback is pushed onto the call stack it executes to completion. This isn’t a requirement by the ECMAScript or WC3 specs and there are some exceptions such as window.alert() for example interrupts the current executing task.
Non-blocking - Blocking is when the application state is suspended as a thread runs[7]. When code executes in the JavaScript runtime it doesn’t suspend the browser environment from accepting inputs.
JavaScript’s call stack is single-threaded and synchronous so callbacks must execute sequentially making CPU intensive tasks challenging. This includes browser repaints on the UI thread.
Let’s add a CPU intensive event listener.
Click Start then Do Stuff. When Start handler is running the browser appears frozen. But JavaScript’s call stack is non-blocking. Even though Do Stuff didn’t log immediately the browser still accepts inputs.
Checkout this CodePen to see.
1) Break the big loop into smaller loops and use setTimeout() on each loop.
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.
Events trigger then execute in a multi-threaded area called Web APIs, outside the JavaScript runtime. 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. 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. If you have questions or comments please send them via Linkedin.
[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