Sunday, February 14, 2016

JavaScript is Single Threaded

JavaScript is single-threaded, that is a well publicized fact. And we are reminded that we should write short operations to avoid problems. But why? More importantly, what is a simple "world-view" of how the JavaScript environment functions in a browser?

Frankly there are plenty of complex diagrams about all the gory details of what happens inside a browser for a page life-cycle. I, however, want a simple, functional view to explain in the JavaScript world the nuts and bolts you need to be an efficient programmer.

JavaScript Program Life-Cycle

So there are three phases to JavaScript in both the browser and NodeJS.
  1. Compiling JavaScript
  2. Executing "bootstrap" JavaScript code
  3. Responding to events
Modern JavaScript engines first compile the code, and compilation can fail with syntax errors. If JavaScript code is loaded into the application while it is running (yes, that is possible) then it to needs to be compiled when that happens.

"Bootstrap" code is any free-standing JavaScript code that the compiler has encountered that needs to be executed. Again, if JavaScript code is loaded into the application while it is running any free-standing code will be executed.

The free-standing code can register event handlers. In the browser this could be against events generated by the Document Object Model (DOM), in Node.js these handlers are generally registered for I/O events.

Browser Integration and Page Rendering

My first picture of the browser looks like this: read the page and create the DOM, executes inline scripts as they are encountered, and keep repeating that until the end of the page. Finally, render the DOM in the browser window:
Most programmers are aware that JavaScript "bootstrap" code in the middle of a page can only reference DOM elements that came before it, and the following example proves it. But if you download and run the examples by launching this example from the index.html page, you will notice something else. Even though the DOM has been built up to the point of the JavaScript, the browser does not render any of the elements until the end of the document is reached! Warning: simply reloading the page hides that effect because you are looking at the previous output until the browser is ready to render the page again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<!DOCTYPE html>
<!--
delayed-rendering.html
-->
<html>
    <head>
        <title>Simple Page Lifecycle</title>
    </head>
    <body>
        <h1>Simple Page Lifecycle and Browser Rendering</h1>
        <p id="firstParagraph">The loop messages in the script following this paragraph will be written to the console.
            Notice that the first time you view this page the messages appear on the console before the two paragraphs
            are rendered.
            When you refresh the page the same thing happens, but you may be fooled because the previous rendering
            is displayed until the new DOM is ready to be rendered.</p>
        
        <script>

            console.log('Big loop starting');

            var bigArray = [];

            for (i = 0; i < 100000000; i++) {

                bigArray.push(i);

                if (i % 1000000 == 0) {

                    console.log('Pushed', i);
                }
            }

            console.log('Big loop finished');
                        
            var firstParagraph = document.getElementById('firstParagraph');
            
            console.log('first paragraph (null means is not yet built)', firstParagraph);

            var lastParagraph = document.getElementById('lastParagraph');
            
            console.log('last paragraph (null means is not yet built)', lastParagraph);
            
        </script>
        
        <p id="lastParagraph">This text has been rendered after the script in the browser.</p>
    </body>
</html>


This is certainly nasty when the JavaScript takes a long time to execute and the page load stalls in front of the user. Clearly that is one big reason that we should avoid CPU-bound JavaScript.

Event Handling

So what about the event handlers? After the page is rendered the browser waits for something to appear in the event queue. If there is a JavaScript handler defined for the event then it executes it. The event could trigger internal stuff in the browser too, or that could be squelched by the JavaScript. After each event is processed the changes to the DOM are rendered. and the cycle starts all over again:
That is a pretty simple model when you look at it this way, there are only four things the browser is doing, supported by the DOM in the middle. And there is only two ways into the JavaScript. Granted, the browser really has more stuff going on, but this is all that we need to focus on to build efficient JavaScript programs.

Events are single-threaded to completion

Many programmers are under the impression that anything they do with the DOM happens immediately, be it through the low-level API or jQuery simplifying it. Well, changes to the DOM are immediate. But rendering the changes is not. It only appears that way if small snippets of JavaScript code respond to the events quickly.

The following example proves the cycle described above is correct and that event handlers must complete before the DOM is rendered. I does not matter if the event handler changes the DOM, writes to the console, or calls other functions. It must run to completion before the DOM is rendered or other events are handled.

The click event handler for the "Start" button on this page kicks off a really big loop, and even though it makes changes to the DOM every 10,000,000 iterations nothing is rendered and no other events are processed until that event handler finishes.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<!DOCTYPE html>
<!--
event-handlers.html
-->
<html>
    <head>
        <title>Single-Threaded Event Handlers, Interval Timers, and Delayed Rendering</title>
    </head>
    <body>
        <h1>Single-Threaded Event Handlers, Interval Timers, and Delayed Rendering</h1>
        <p>Open the JavaScript console in your browser before clicking the "Start" button!</p>
        <p>This page uses a big loop to prove that:</p>
        <ol>
            <li>The click event handler for the "Start" button runs to completion before the DOM is rendered; you cannot see the iterations increment in the DOM.</li>
            <li>Only one 1/10th second interval timer event will occur; notice it appears after the click events (if there are any).</li>
            <li>Click events for the "Click Me!" button are queued and processed after the first event handler finishes; they show up at the end on the console.</li>
        </ol>
        <p>Click the "Start" button to start the big loop, and then click the "Click Me!" button once or more before that loop completes.</p>
        <p><button id="startBtn">Start</button> <button id="clickMeBtn">Click Me!</button></p>
        <p>Loop iteration: <span id="loopIterationFrame"></span><br/>
            Interval timer: <span id="intervalTimerFrame"></span><br/>
            Click me clicked: <span id="clickMeClickedFrame"></span></p>
        
        <script>
                
            // Initialize variables for the interval timer that runs during the big loop.
            
            var intervalTimer = null;
            var intervalCount = 0;
            var stopIntervalTimer = false;
            
            // Keep a count of the clicks on clickMeBtn.
            
            var clickMeClicked = 0;
        
            // Retrieve the DOM elements.
            
            var startBtn = document.getElementById('startBtn');
            var clickMeBtn = document.getElementById('clickMeBtn');
            var loopIterationFrame = document.getElementById('loopIterationFrame');
            var intervalTimerFrame = document.getElementById('intervalTimerFrame');
            var clickMeClickedFrame = document.getElementById('clickMeClickedFrame');
            
            // Add event listeners to the buttons.
            
            startBtn.addEventListener('click', onStart);
            clickMeBtn.addEventListener('click', onClickMe);
            
            function onStart() {
                
                // Listener for the start button: start everything.
                
                // Register the interval timer for 1/10th seconds occurances.
                
                console.log('Registering interval timer');
                
                intervalTimer = setInterval(onInterval, 100);
                
                // Run the big loop.
                
                console.log('Big loop starting');
                
                var bigArray = [];
                
                for (i = 0; i < 100000000; i++) {
                       
                    bigArray.push(i);
                    
                    if (i % 1000000 == 0) {
                        
                        showIteration(i);
                    }
                }
                
                showIteration(i);
                
                console.log('Big loop finished');
                
                // Raise the flag to stop the interval timer.
                
                stopIntervalTimer = true;    
            }
            
            function onClickMe() {
                
                // Listener for the clickMe button: log the button click to the DOM frame.
                
                clickMeClickedFrame.innerHTML = ++clickMeClicked;
                console.log('Click Me clicked', clickMeClicked, 'times');
            }
            
            function onInterval(e) {
                
                // Log the interval timer to the DOM frame.
                
                intervalTimerFrame.innerHTML = ++intervalCount;
                console.log('Interval timer');
                
                if (stopIntervalTimer) {

                    console.log('Stoping interval timer');
                    clearInterval(intervalTimer);
                }
            }
            
            function showIteration(iteration) {
                
                // Log the iteration to the DOM frame.
                
                console.log('Pushed', iteration);
                loopIterationFrame.innerHTML = iteration;
            }
            
        </script>
    </body>
</html>

If you click the "Click Me!" button those events cannot be processed until the event handler with the loop finishes. Events generated from browser elements do queue, so there will be a queued event for every time the button was clicked.

Before the loop starts an interval timer is registered with an event handler. It is supposed to trigger every 1/10th of a second, but interval events will not occur while you are inside of an event handler! So the first interval event is put into the queue after the big loop handler finishes, and it is placed in the queue after any click events on the "Click Me!" button.

Also important:  only a single interval event queues. If there is a interval event in the queue and another one arrives it is dropped. Unfortunately interval events do not deliver an event object so there is not any way to see when the first one actually entered the queue. But as I stated previously, we can see that the first interval event enters the queue after the first event handler finishes, and after the other click events have been queued.

Keep it Tiny: In and Out

Since CPU-bound JavaScript anywhere is going to stall your program, keep everything small. Bootstrap the program quickly or the user will see a delayed load. Handle events quickly or the user may see the browser stutter. Do not do significant amounts of DOM processing, unless it is OK to show the user the combined results at the end. Moving an element slowly across the screen will not work, unless it is done across a series of events.

If you have to do complex calculations offload them to a server and use AJAX. If you really need to do them in the browser or you are writing a server-side NodeJS application then consider using Web Works, and that is a topic for another day. The best-practice is to perform as much asynchronously as possible, and that also is a topic for another day.

Download the code at http://askjoelit.com/SingleThreadedJavaScript.zip. The project has a node server built in to serve the pages; use "npm install" to load the dependencies and then "npm start" to launch the server. Display the index page in a browser at http://localhost:8080.


References

See the references page.


No comments:

Post a Comment