Eugene's Blog

I can't believe it's blog!

More on 0ms timeouts

Once I wrote a blog post On JavaScript, EDP, and 0ms timeouts, which discussed a "smart" trend to pepper code with timeouts of … 0ms: setTimeout(f, 0). Authors of those "programming pearls" genuinely believed that this code reschedules a function call to the next available time slice after the code that set the timer was executed without introduction of a delay (after all it is 0ms!). In reality a typical delay was 10-20ms. With new generation of browsers it starts with 4ms and jumps to 1000ms for non-current/hidden tabs.

Obviously there are legitimate uses of timeouts: polling a server with some interval, throttling events, animation. The 0ms trick is not among them. And the animation is better served by Timing control for script-based animations.

This is the "0ms timeout" in all its glory:

function f(){
    // do something

// ...

// here it goes:
setTimeout(f, 0);

There are two problems with this approach:

  1. It makes debugging such code a nightmare, especially when you have several timers in-flight, and/or this particular timer invocation can be called from different places. The problem is that the history of such call is completely lost – all you see in debugger is a short call stack, which starts with setTimeout. It would be almost impossible to unravel why globals and/or closure (common ways to set up data for a timer callback function) are set this way.
  2. While "0" looks assuring, it is not 0ms. It does involves a timeout no matter what. Here is a program from my previous article to try it for yourself: test chained timeouts (try it with current, and non-current tabs).

Current breed of browsers restricts timer-based polling to 4ms for current tabs, which is an improvement on 10-20ms in previous versions. But if your user switches to another tab for a moment, timers will be executed once per second – 1000ms delay. This is probably fine for an animation, and for server polling – why bother updating something, if user cannot see it at the moment. It saves CPU and bandwidth making the current tab more responsive.

Even a delay of timer callback doesn’t sound too bad. Who cares? The real problem is in accidental or purposeful chaining of such callbacks. Working on a real application, which used a signal library to send light-weight low-level events between program pieces, I found that cascading signals (a handler of one signal sent another signal) created chains up to ~10 signals one after another. The total delay was ~100-200ms, which was noticeable by end users. The problem was that nobody could explain those delays. For end users those delays looked completely illogical: no major calculations, no I/O with server, yet you can clearly see how data is being updated piece by piece in front of their eyes.

The new policy of throttling timers of non-current tabs to 1FPS makes the 0ms timeout technique unsustainable in a general case. Some examples:

  1. If we accumulated a few heavy callbacks before user switched a tab, on her return we will have a workload of (potentially obsolete) tasks to perform. It would be perceived by an end user as a delay before things got normal again.
  2. If we produce temporarily more signals than we process (it happens all the time), and user switched, we can clog our timer queue with (potentially unnecessary) updates. Again, it means a delay. Users hate delays, especially when they are not expected logically.
  3. If our program involves timers to do time-specific tasks, they can interfere with "0ms timeouts". Frequently this interaction is unpredictable. For example, we may assume that all "0ms timeouts" are fast and executed way before our next time interval. Now this assumption is wrong, which can lead to incorrect results at the very least.

The "0ms timeouts" belong to a category called "stupid tricks". Try to avoid them, if you possibly can. Think about the reasons you do that. Maybe a direct call is better in this case? If you want to protect your call site from possible exceptions thrown by a callback function, think if you can just try/catch those exceptions.

If you want to cooperate with browsers, and make your web application behave differently when hidden, use Page Visibility API. This way you know what is going on, and can cancel or reschedule all time-consuming updates. Your servers and your users will thank you for that.