Timing and Synchronization in JavaScript
Timing issues are the source of some of the most devious bugs in JavaScript applications. Problems that never show up during development might surface when the application is used by an end-user on a slow computer or with low bandwidth. Such issues may also be intermittent and difficult to reproduce.
A simple example: consider a button with a click event-handler that modifies another element below it. If the user clicks the button before the element below has been parsed, the script will fail. The developer will never notice the issue, because he tests the page on a fast computer with a fast connection, where the whole page is rendered in one instant.
This article attempts to describe the various timing-related issues in JavaScript in current browsers.
The Basics
A browser window has a single thread that performs parsing of HTML, dispatching of events and execution of JavaScript code.
JavaScript code is executed in one of two ways:
- Top-level code in script-element executed during page-load
- Event-handler functions executed as part of an event dispatch
The browser initiates both types of execution, and they run in the same thread such that only one code unit is executing at a time.
Primarily the browser is event-driven (code is executed in response to user input), but during the page load phase, it is in addition driven by the parsing thread.
Event Flow
An event is a signal from the browser that some change in the window has happened or is going to happen very soon if you don’t do something about it.
An event handler is a JavaScript function that is registered to an object and an event name. When the corresponding event is fired on the object, all event handlers registered on the node are executed.
All event handler functions are executed sequentially, and each event is processed completely (including bubbling up through the DOM and performing the default action), before the next event is processed.
Default Action
The default action is a curiosity of the browser event model: It is what happens when no JavaScript is interfering. For example, the default action of the click
event on a link is to navigate to the URL. The default action of click on a checkbox is to check the box, and so on.
The default action is not an event handler in itself, and we cannot remove it or override it, as opposed to our custom events handlers. However, we can cancel it during the dispatch, using preventDefault()
(event.returnValue
in IE). If the default action is cancelled, all relevant event handlers will still be fired, but the default action will not be executed afterwards.
Dispatch Sequence
Events like load
are fired only on the corresponding object (window
or document
in this case). However, for events targeted on specific elements in the document, it is possible for event handlers on ancestor elements to get fired also.
Before an event is fired on the target, there is a capturing phase, where ancestors to the target node may intercept the event. Event capturing does not work reliably cross-browser, though.
Some events bubble, which means that after they have fired on the target element, they are fired in turn on each ancestor in the DOM tree, up to and including the document-object. This does works cross-browser.
The whole process of firing an event on all relevant elements and executing the default action is called the event dispatch.
For a non-bubbling event, the sequence of the dispatch is like this:
- Capturing phase: All "capturing" event handlers are fired on all ancestor elements, from the top down.
- The event is fired on the target element, which means that all event handlers registered on the element for the specific event are executed (in undefined order!)
- The default action is performed (if it wasn't cancelled in any of the handlers)
For a bubbling event, the sequence is like this:
- Capturing phase: All "capturing" event handlers are fired on all ancestor elements, from the top down.
- The event is fired on the target element
- Bubbling phase: The event is fired on all ancestor elements, from the target and upward.
- The default action is performed (if it wasn't cancelled in any of the handlers)
It's possible to cancel the bubbling of an event using stopPropagation()
(cancelBubble()
in IE), however the default action will still be executed. Cancelling bubbling and cancelling the default action are thus separate and independent operations.
The specific stages of the event model are explained in much greater detail (and with a nice illustration) in the DOM 3 Events specification.
There are some curious situations where the default action actually happens before the event dispatch – but may still be cancelled. For example, when a checkbox is clicked, the checkmark is rendered, and the checked
attribute is updated before the event dispatches. However, if the default action is cancelled during the dispatch, the update is rolled back during the default action phase; the checkmark is removed again, and the checked
attribute is flipped back.
Event Batches
Some events come in batches, where one user inputs results in several events being dispatched.
For example, when focus moves from one field to another, the blur
-event is fired on the one field, and focus
on the other. This happens conceptually at the same time (since it is in response to the same use input), but the events are still dispatched sequentially.
If an event bubbles, the whole event capturing/bubbling sequence is completed and the default action executed before the next event is dispatched.
An example of this is a mouse button being released over a button, when both the mouseup
-event and the click event are fired. The sequence is like this:
Mouseup-event dispatch
- Capturing phase for
click
– all capturing event handlers are executed. - Target: the event is fired on the target element.
- Bubbling phase for
mouseup
: the event is fired on all parent elements. - (There is no default action for a
mouseup
).
Click-event dispatch
- Capturing phase – all capturing event handlers are executed.
- Target: the click event is fired on the target element.
- Bubbling phase: the click event is fired on all parent elements.
- Default action for click is executed.
In each dispatch it is only possible to cancel the default action of the current event. For example, in a mouseup
event
handler, cancelling the current action has no effect, since mouseup
has no default action. It will not stop the click event from firing immediately after, since they are separate events.
However, the default action might trigger another event. In the case of the click event on a submit button, the default action is to submit the current form, which in turn will dispatch the submit event. So cancelling the default action for click in this case will prevent the next event from firing.
Event Queuing
Events are dispatched in response to user input (mouse or keyboard), or internal events like a page finishing loading. However, the event dispatching is asynchronous to the triggering input.
User input might happen while an event handler is still executing. In this case the actions are buffered, and when the event dispatcher is available again, the events corresponding to the buffered actions are dispatched. The events are always dispatched in the right order, but there might be a noticeable delay between the action and the event dispatch, if some event handler code is time consuming.
Internet Explorer and Mozilla appear completely unresponsive during the time when event handler scripts are executed. Even the browser toolbars seem to lock up. While the user can still, for example, click buttons, and the actions are buffered, there is no visual feedback. This might seem confusing, as the user might not realize that the action has been detected, and is likely to try clicking the button several times, which might have unintended consequences. Or the user might even believe that the browser has crashed, since it seems unresponsive.
Opera is much more responsive, giving visual feedback on user actions, such as clicking a button, while another script is executing. However, the events are still buffered and dispatched sequentially, as in the other browsers. Therefore the default actions of the event are not performed until the event dispatcher gets around to it. Again, this might seem confusing to users, although perhaps not as much as the locking up of IE and Mozilla.
The bottom line is that event handler scripts should never be time consuming. Be especially wary of synchronous XMLHttpRequest
requests, since they might cause a noticeable delay that blocks the browser or document window.
Nested Events
There is a special case where events are not sequenced, but nested. If an event if fired explicitly through script using the dispatchEvent()
-method (fireEvent()
in Internet Explorer), the event is dispatched immediately. The initial script will only continue when the dispatched event has finished (and the default action has executed).
Also the DOM mutation events, that are not supported by Internet Explorer, will be dispatched immediately and synchronously when the DOM is changed, for example when appendChild()
is called.
Timing of Rendering
Programmatic changes to the DOM or style sheet might not render immediately. It depends on the browser.
For example, if the background color of an element is changed through the DOM, the DOM will immediately reflect the change (and the DOM mutation event will be dispatched immediately and synchronously), but we do not know for certain when the browser engine will come around to actually rendering the changes visually on the screen. While it seems that in Mozilla and Internet Explorer the changes are postponed until the current event dispatch has completed, these changes seem to be rendered immediately in Opera.
Timeouts
The setTimeout()
method schedules a function to be execution after a specific amount of time has elapsed:
window.setTimeout(someFunction, 1000);
Timed scripts work somewhat like event handler scripts. Although they are executed in response to a timeout rather than some user input, they are still handled sequentially by the event dispatch thread just as user action events are.
Because of this, you cannot expect that the timeout will be executed at the designated time. If another event or event batch is executing, the timeout script will just be queued. Basically we are promised that the function will execute after at least a second, but it might take a lot longer than that.
This is a surprisingly useful feature. If a handler is registered with timeout 0, the handler is not executed, but queued immediately. It will be executed immediately after the current event dispatch (including the default action) has completed.
If a timeout is created in a event handler which is part of a batch (like blur
/focus, mouseup
/click
), the timeout handler is executed after all events in the batch have completed dispatch.
Non-user Events
Other non-user-initiated events are:
- Page load events
- Timeout events
- Callback when content is received from an asynchronous
XMLHttpRequest
These events are added to the event-dispatch queue the same way that user-initiated events are. This means, for example, that the XMLHttpRequest
response handler is not executed immediately when the content is received, but just queued in the event dispatch queue.
Alerts
Alert boxes (and the related confirm
and prompt
-boxes) have some strange properties.
They are synchronous in the sense that the script that initiates the dialog is suspended until the dialog is closed. The script waits for the alert()
-function to return before it continues.
The tricky part is that some browsers allow events to be dispatched while the dialog is visible and waiting for some user action. This means that while one script is suspended, waiting for the alert function to return, another function might be executed as part of a different event dispatch.
User interface events like mouseup
and click
will not fire during the alert, as the alert is modal and captures all user input, but non-user-initiated events like page load, timeout handlers, and asynchronous XMLHttpRequest
return handlers, might fire.
Page Loading
HTML pages are parsed and rendered progressively as the browser downloads the document.
Most external resources, such as images and plug-in media, are loaded asynchronously. When the parser encounters an img
-tag, or embed
, iframe
, or object
, a new thread is spawned. This downloads and renders the external resource independently from the parsing of the main page. Pages in frames and iframes are also loaded asynchronously.
External style sheets are a special case. Some browsers fetch them asynchronously (like images), some browsers fetches them synchronously, presumably to avoid having to re-render everything when the style-sheet arrives. (This is to prevent the flash of unstyled content which plagued early browsers.) In other words, don't rely on any particular behavior for this.
JavaScript Block Execution
Script elements are parsed synchronously. When script elements refer to external script files, the parsing of the page is halted until the external script has been completely downloaded, parsed and executed.
Inline JavaScript blocks are parsed and executed when the closing tag is encountered.
Execution of script blocks
A JavaScript block (inline script-block or external JavaScript file) is processed in two phases. First it is parsed and then executed. During the parsing stage, the basic syntax of the code is validated. If a syntax error is encountered, the script will not be executed.
During the execution stage all top-level statements are executed, meaning all code that is not part of a function. Top-level statements may contain forward references to functions declared in the same block, since function declarations are loaded during the parsing stage. This will work:
<script>
var x = getMagicNumber();
function getMagicNumber() { return 117; }
</script>
However, this will not work, since function expressions are evaluated at runtime:
<script>
var x = getMagicNumber(); // ERROR! getMagicNumber is undefined!
var getMagicNumber = function() { return 117; }
</script>
This will not work either, since each script block is executed immediately after the closing tag has been parsed:
<script>
alert(getMessage());
</script>
<script>
function getMessage() { return "Hello!"; }
</script>
Using Document.write()
A script may generate HTML output directly in the document using the document.write()
method. The generated output will be buffered until the block has finished executing. Then the buffered output is parsed. This output might (mind-bendingly!) contain script blocks, which are executed as part of the parsing.
The generated HTML output is inserted into the document immediately following the script block that generated the output.
DOM Construction
The parser constructs the DOM incrementally during page load. An empty element is inserted in the DOM when the tag is parsed. A non-empty element is inserted when the opening tag is parsed. For example, the body
element is available in the DOM as soon as the parser begins parsing the content of the element.
Note that the DOM might not correspond completely to the input HTML. Element like html
and head
will be constructed in the DOM even if they don't appear in the HTML.
If the HTML source is invalid, for example a title
element appearing in the body
element), the browser will rearrange the DOM to make it valid. In this case you cannot rely on the DOM tree to be constructed in order.
Deferred Script-block Loading
There is a drawback to the synchronous loading of script blocks: If lots of code has to be downloaded and executed while parsing the head of the document, there might be a noticeable lag before the page begins to render.
To alleviate this, we could have used the defer
attribute on script elements. This indicates that the browser is allowed to load the script asynchronously. However, we cannot be sure when the script actually will be executed, it could be before or after the page has finished rendering. Opera ignores the defer
attribute completely.
<script defer>
alert("this message will appear at some unpredictable time during page load");
</script>
Deferred scripts cannot use document.write()
, as they are not synchronized to the parser.
This is the catch: script blocks are always executed in the order they appear in the document, whether or not they have the defer attribute. So if a script element without the defer
attribute follows a script with defer
, the parser has to complete loading and executing of the deferred script before executing the un-deferred script. This eliminates the point of the defer
-attribute in the first place, meaning that non-deferred script blocks should always be placed before the deferred script blocks.
For these reasons the defer
attribute can't be relied on for the timing of script blocks. It only allows some browsers to continue parsing the document after a script block.
Progressive Rendering
The actual rendering of the visual display is not synchronous with the DOM construction. Timing of rendering during page load is quite unpredictable. Depending on the speed of the connection and the size of the page, the browser might wait until the whole page has finished loading before rendering anything or, in the case of a slow connection, might render the page one piece at a time.
Be aware that the user interface is responsive to user events as soon as the page starts rendering. This might lead to forward-reference problems, if an event handler refers to an element later in the document.
Example of dangerous code:
<button
onclick="document.getElementById('lamp').backgroundColor = 'yellow'">
Click here to turn on lamp!
</button>
<div id='lamp'>O</div>
The problem is that the 'lamp'-element might not be parsed when the button is clicked. An event handler should never refer to elements defined later in the document.
In more complex user interfaces, it might be unpractical to avoid forward references between controls on a page. Instead all controls could just be disabled by default, and only activated in the onload
event handler, where we are guaranteed that the whole page has loaded.
Note that onload
also waits for all images (and frames and so on) to finish loading. If there are large images on the page, this might take some time. A workaround is to activate the page on an inline script on the bottom of the page. This will execute when the page is loaded, but does not wait for external resources to load.
Long running scripts
Ideally, JavaScript code should never run very long, since it disrupts the user experience. Some times it is unavoidable, however. In this case, a "Please wait" message or a progress bar or similar should be displayed, to indicate that the browser is not broken. The problem is that the message must be made visible before the time consuming process runs.
Here is an example in pseudo-code:
headlineElement.innerHTML = "Please wait...";
performLongRunningCalculation();
headlineElement.innerHTML = "Finished!";
In Internet Explorer and Mozilla, the text "Please wait..." will never be shown to the user, as the changes are rendered only after the whole script has finished. In Opera, on the other hand, the "Please wait..." text is displayed while the calculation is running.
If we want the message to display in Internet Explorer and Mozilla, we have to release control to the browser UI, so the message is rendered before the calculation begins:
headlineElement.innerHTML = "Please wait...";
function doTheWork() {
performLongRunningCalculation();
headlineElement.innerHTML = "Finished!";
}
setTimeout(doTheWork, 0);
Now the setTimeout
trick ensures that the message is rendered before the work blocks the browser again. The browser is of course still blocked while the calculation is performed, so it's not a very elegant solution. If we want that, we have to chop the calculation into several functions, chained together with setTimeout
. This quickly gets complicated, though.
Race Conditions
Each window (and frame) has its own event queue.
In Opera, every window has its own JavaScript thread. This includes windows in iframe
s. The consequence is that event handlers initiated from different frames might execute at the same time. If these simultaneous scripts modify shared data (like properties in the top window), we have the possibility of race conditions.
I will not go into the hazards of race conditions, just point out that this may lead to very confusing bugs.
A solution would be always queueing handlers in the event queue in the top window, even if initiated by events from other frames.
Consider a page with an iframe
. The iframe
has a page onload
handler, which will execute a function in the containing page:
// bad onload function in frame:
window.top.notifyFrameLoaded()
This is dangerous, as the onload
might execute while the containing page is executing some other script. However, the function can be queued:
// good onload function in frame
window.parent.setTimeout(window.top.notifyFrameLoaded, 0)
The important part is using the setTimeout
on the parent window to enter the function into the parent window event queue.
Advice on Timing
- Don't have long-running scripts.
- Don't use synchronous
XMLHttpRequest
s. - Don't let scripts initiated from different frames manipulate the same global state.
- Don't use alert boxes for debugging, as they might change the logic of the program completely.
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.
http://www.facebook.com/permata.mutiara
Sunday, November 27, 2011