Events and timing in-depth

  1. JavaScript is single-threaded
  2. Asynchronous events
    1. Stacked events
  3. The setTimeout(func, 0) trick
    1. Delay triggering to let parent work
    2. Give the browser time for job
  4. Synchronous events
    1. DOM mutation events are synchronous.
    2. Nested DOM events are synchronous.
  5. JavaScript execution and rendering
  6. Modal and synchronous calls
    1. Opera: iframes exception.
  7. Script taking too long and heavy jobs
  8. Summary

Internally, the browsers are event-driven. Most actions occur asynchronously and create an event which is appended to the queue.

They are taken from the queue and processed when the time permits. For example:

  • A script has finished loading.
  • Keypress, mousemove.
  • The window is resized.

Many events are integrated with JavaScript and many events are strictly internal.

JavaScript is single-threaded

There is only one JavaScript thread per window. Other activities like rendering, downloading etc may be managed by separate threads, with different priorities.

Web Workers

There is a Web Workers standard (incomplete at the time of writing) which defines the support for multiple JavaScript workers. A worker is an independent JavaScript subprocess.

Web workers are limited. They are able to execute JavaScript and exchange messages with the parent process, but they can’t access DOM.

Asynchronous events

Most events are asynchronous.

When an asynchronous event occurs, it gets into the Event queue.

The browser has inner loop, called Event Loop, which checks the queue and processes events, executes functions etc.

For example, if the browser is busy processing your onclick, and another event happened in the background (like script onload), it appends to the queue. When the onclick handler is complete, the queue is checked and the script is executed.

setTimeout/setInterval also put executions of their functions into the event queue if browser is busy.

In fact, most interactions and activities get passed through the Event Loop.

Stacked events

There are cases when multiple events are appended to the queue in a batch.

For example, mousedown followed by mouseup on the same screen location causes click event. The two events: mouseup and click events are appended to the queue at the same time.

The focus event may come stacked with mousedown.

The setTimeout(func, 0) trick

When setTimeout gets 0 as the last argument, it attempts to execute the func as soon as possible.

The execution of func goes to the Event queue on the nearest timer tick. Note, that’s not immediately. No actions are performed until the next tick.

Measure your setTimeout(.., 0) resolution

The following example demonstrates the average time until setTimeout(func, 0) executes.

Basically, it makes 1000 calls of setTimeout(.., 0), sums the time between them, then calculates the average.

Please, wait a bit after you run this one. It needs time, because measurements need time. The CPU load should be low.

var i = 0, diff = 0, d = new Date()

var timer = setTimeout(function() {
  diff += new Date() - d
  timer = setTimeout(arguments.callee, 0)
  if (i++==1000) {
    clearTimeout(timer)
    alert("Resolution: "+diff/i)
  }
  d = new Date()
}, 0)

Resolution of 10 ms means that a function scheduled with setTimeout(.., 0) executes after 10ms.

The setTimeout(.., 0) trick is used to execute the code after stacked events and fix timing-related problems.

Real-life examples follow.

Delay triggering to let parent work

An event triggers first on a child, then bubbles to it’s parents. But a child may want to trigger after the parent.

For example, there is a document.keypress which manages hotkeys, and the child want to process the event after hotkey management is complete.

Or, there is a drag’n’drop managed by document.mouse... handlers, and the child element wants to do it’s processing after drag’n’drop.

Event capturing is not supported in IE<9. And there can be other reasons to avoid it.

The recipe is usually setTimeout(.., 0). On the example below, the click is first processed by document.body, then input:

<input type="button" value='click'>

<script>
var input = document.getElementsByTagName('input')[0]

input.onclick = function() {
  setTimeout(function() { 
    input.value +=' input'  
  }, 0)
}

document.body.onclick = function() {
  input.value += ' body'
}
</script>

Note: setTimeout uses input.value, not this.value, because this = window at the time of call.

Give the browser time for job

Most of time, we can handle an event prior to browser default action. But sometimes we want the result of the browser action to process it.

For example, let’s create an input which uppercases it’s text:

<input id='my' type="text">
<script>
document.getElementById('my').onkeypress = function(event) {
  this.value = this.value.toUpperCase()
}
</script>

Type something to try it:

See? It doesn't work! The value is uppercased *except last char*, because the browser appends the char *after* `keypress` is processed. Of course, we could switch to `keyup`, it has full value. But then the char would show up as lowercased, and get uppercased on key release. That looks weird. Type to see (`keyup`): The solution is to use `keypress`, but apply uppercase in a timeout:
<input id='my-ok' type="text">
<script>
document.getElementById('my-ok').onkeypress = function() {
  var self = this
  setTimeout(function() {
    self.value = self.value.toUpperCase()
  }, 0)
}
</script>
Now works at it should:

The timeout executes after the char is appended, but soon enough to keep the delay invisible.

setTimeout(.., 0) is applied to execute the action after browser processing and other handlers.

Synchronous events

There are events which don’t use the event queue. They are called synchronous events and work immediately even when inside other handlers.

DOM mutation events are synchronous.

In the example below, the onclick handler changes an attribute of the link, which has a DOMAttrModified(onpropertychange for IE) listener.

Synchronous mutation events are processed immediately during onclick.

Click the link to see:

<a href="#">Click me!</a>

<script>
var a = document.body.children[0]

a.onclick = function() {
  alert('in onlick')
  this.setAttribute('href', 'lala')
  alert('out onclick')
  return false
}

function onpropchange() {
  alert('onpropchange')
}

if (a.addEventListener) { // FF, Opera
  a.addEventListener('DOMAttrModified', onpropchange, false)
}
if (a.attachEvent) { // IE 
  a.attachEvent('onpropertychange', onpropchange)
} 
</script>

The click processing order:

  1. alert('in onclick') works.
  2. The attribute is changed and the DOM mutation event is processed synchronously, immediately triggering onchange. It outputs alert('onpropchange').
  3. The rest of onclick executes, leading to alert('out onclick').

Nested DOM events are synchronous.

There are methods which trigger an immediate event, like elem.focus(). These events are also processed in a synchronous manner.

Run the example below and click on the button. Notice that onfocus
doesn’t wait onclick to complete, it works immediately.

<input type="button" value="click me">
<input type="text">

<script>
  var button = document.body.children[0]
  var text = document.body.children[1]

  button.onclick = function() {
    alert('in onclick')

    text.focus()
   
    alert('out onclick')
  }
 
  text.onfocus = function() {
    alert('onfocus') 
    text.onfocus = null  //(*)
  }
</script>

In the example above, the alert order is in onclick->focus->out onclick, that clearly demonstrates the synchronous behavior.

The line labelled () is required, because alert(message) focuses on the message window. When it is disposed, the browser refocuses back.

So without () the focus would be triggered one extra time after the alert.

Events are also processed immediately when triggered from JavaScript by dispatchEvent/fireEvent.

Usually event handlers are executed one by one. So we assume that one handler finishes before the other starts.

Synchronous events break this one-by-one rule, that may can cause side-effects.

For example, the onfocus handler may assume that onclick has completed the job.

There are two ways to fix it:

  1. Move text.focus() to the end of the onclick code.
  2. Wrap text.focus() into setTimeout(.., 0):
    button.onclick = function() {
      alert(1)
      setTimeout(function() { text.focus() }, 0)
      alert(2)
    }
    

The concrete way is chosen according to your architecture.

JavaScript execution and rendering

In most browsers, rendering and JavaScript use single event queue. It means that while JavaScript is running, no rendering occurs.

Check it on the demo below. When you press run, the browser may halt for some time, because it changes div.style.backgroundColor from #A00000 to #FFFFFF.

In most browsers, you see nothing until the script finishes, or until the browser pauses it with a message that ‘a script is running too long’.

The exception is Opera.

<div style="width:200px;height:50px;background-color:#A00000"></div>

<input type="button" onclick="run()" value="run()">

<script>
function run() {
  var div = document.getElementsByTagName('div')[0]
  for(var i=0xA00000;i<0xFFFFFF;i++) {
    div.style.backgroundColor = '#'+i.toString(16)
  }
}
</script>

In Opera, you may notice div is redrawn. Not every change causes a repaint, probably because of Opera internal scheduling. That’s because event queues for rendering and JavaScript are different in this browser.

In other browsers the repaint is postponed until the JavaScript finishes.

Again, the implementation may be different, but generally the nodes are marked as “dirty” (want to be recalculated and redrawn), and repaint is queued. Or, the browser may just look for dirty nodes after every script and process them.

Immediate reflow

The browser contains many optimizations to speedup rendering and painting. Generally, it tries to postpone them until the script is finished, but some actions require nodes to be rerendered immediately.

For example:

elem.innerHTML = 'new content'
alert(elem.offsetHeight)  // <-- rerenders elem to get offsetHeight

In the case above, the browser has to perform relayouting to get the height.
But it doesn’t have to repaint elem on the screen.

Sometimes other dependant nodes may get involved into calculations. This process is called reflow and may consume lots of resources if script causes it often.

Surely, there’s much more to talk about rendering. It will be covered by a separate article [todo].

Modal and synchronous calls like alert pause the JavaScript thread.

That causes related activities to freeze.

The example below demonstrates it.

  1. Press “Run”. The setInterval-based animation will start and and alert button will appear.
  2. Press the button, note that the animation stops.


<div style="height:20px;width:0px;background-color:green"></div>
<script>
var timer = setInterval(function() {
  var style = document.getElementsByTagName('div')[0].style
  style.width = (parseInt(style.width)+2)%400 + 'px'
}, 30)

</script>
<input type="button" onclick="alert('Hello!')" value="alert('Hello!')  [ iframe ]">
<input type="button" onclick="clearInterval(timer)" value="clearInterval(timer)">

When you press alert('Hello!'), the alert blocks JavaScript execution and blocks the whole UI thread. That’s how alert, confirm and prompt work. And there is only one thread. So, setTimeout/setInterval can’t execute while the thread is blocked.

Opera: iframes exception.

Usually, iframes run in the same thread with the page.

But there is an exception called Opera. Run the example above in Opera and press alert in the main window. The iframe animation will continue!
That’s because the example is actually running in an iframe.

Other browsers use single thread for whole tab, so the iframe animation is paused there.

Script taking too long and heavy jobs

JavaScript can be heavy.

In this case, the browser may hangup for a moment or come with a warning “Script is taking too long”.

We’d want to evade that. It can be done by split the job into parts which get scheduled after each other.

Then there is a “free time” for the browser to respond between parts. It is can render and react on other events. Both the visitor and the browser are happy.

The background color in the example below is changed once per tick. So the browser has the time to render it, and there are no hangups. Changes are applied incrementally.

Press the run button on the example to start.

<div style="width:200px;height:50px;background-color:#100"></div>

<input type="button" onclick="run()" value="run()">
<input type="button" onclick="stop()" value="stop()">

<script>
var timer

function run() {
  var div = document.getElementsByTagName('div')[0]
  var i=0x100000

  function func() { 
    timer = setTimeout(func, 0)
    div.style.backgroundColor = '#'+i.toString(16)
    if (i++ == 0xFFFFFF) stop()
  }

*!*
  timer = setTimeout(func, 0)
*/!*
}

function stop() {
  clearInterval(timer)
}
</script>

The internal order:

  1. setTimeout appends the func call to the event queue.
  2. The new call is scheduled on the next tick.
  3. The func executes and changes the div which appends a repaint request to the queue.
  4. The function finishes. The browser takes the next event from the queue which is repaint and executes it. Then it waits the next tick to execute one more func call (see step 2).
  5. Repeated until stop()

A delay may be increased from 0 to 100 ms, depending on your needs. The longer delay leads to less CPU load.

Evade the 'script is running for too long' warning

As an important side-effect, splitting the long job into parts which are executed by setTimeout helps to fix browser hangups and evade warnings.

For example, modern syntax highlighters employ such technique. When a visitor opens a large text, they highlight a part of it then call something like setTimeout(highlightNext, 50) which highlights the next part etc.

It would hangup otherwise, because the syntax highlighting takes time.

Summary

Most browsers use single thread for UI and JavaScript, which is blocked by synchronous calls. So, JavaScript execution blocks the rendering.

Events are processed asynchronously with the exception of DOM events.

The setTimeout(..,0) trick is very useful. It allows to:

  • Let the browser render current changes.
  • Evade the “script is running too long” warning.
  • Change the execution flow.

Opera is special in many places when it comes to timeouts and threading.

See also:

Tutorial

Donate

Donate to this project