• A tour of JavaScript timers on the web
  • Original author: Nolan Lawson
  • Translation from: Aliyun Translation Group
  • Text link: github.com/dawn-teams/…
  • Translator: Lingnuma
  • Proofread by: Ye Shu, Jing Xin, Mian Yun

JavaScript timer tour

Pop quiz: What’s the difference between JavaScript timers?

  • Promises
  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • requestIdleCallback

More specifically, if you immediately sort these timers, do you know in what order they fire?

If not, you’re probably not alone. I’ve been writing JavaScript and programming for many years, once working for a browser manufacturer for over two years, and it’s only recently that I’ve really learned about timers and how to use them.

In this article, I’ll give you a high-level overview of how these timers work and when to use them, along with Lodash’s useful Debounce () and Throttle () functions.

Promises and microtasks

Let’s start with this, because it’s probably the easiest. A Promise callback, also known as a “microTask,” runs at the same frequency as the MutationObserver callback. QueueMicrotask () would have the same result if it had not been excluded from the specification and entered the browser realm.

I’ve written a lot about Promise. It’s worth noting, however, that promises can be easily misunderstood because they don’t leave the browser free. That’s because it’s in an asynchronous callback queue, but it doesn’t mean the browser can render, or process input, or do whatever else we want the browser to do.

For example, suppose we have a function that blocks the main thread for 1 second:

function block() {
  var start = Date.now()
  while (Date.now() - start < 1000) { /* wheee */ }
}
Copy the code

If we call this function with a set of microtasks:

for (var i = 0; i < 100; i++) {
  Promise.resolve().then(block)
}
Copy the code

This will block the browser for 100 seconds. This is the same as the following operation:

for (var i = 0; i < 100; i++) {
  block()
}
Copy the code

After any synchronization task is completed, MicroTasks will execute it immediately. There is no time for other work in between. So, if you want to break down a long running task into microtasks, you may not want to do so.

SetTimeout and setInterval

They are two brothers: setTimeout runs the task after X milliseconds, and setInterval runs the task every X milliseconds.

Many sites like Confetti use setTimeout(0) all over the place. To avoid blocking the browser main thread, the browser must set setTimeout(/*… */, 0) Add mitigation measures.

This is why many of the tricks in CrashMyBrowser.com no longer work, such as calling two other settimeouts in setTimeout that call more settimeouts and so on. I described some of these mitigation approaches from the margins in Improving Input Responsiveness in Microsoft Edge.

Loosely speaking, setTimeout(0) is not really executed after 0 milliseconds. It is usually executed within 4 milliseconds. It is sometimes executed in 16 milliseconds (when the Edge is charging). It is sometimes limited to 1 second (example: when running in a background TAB). These are the capabilities that browsers must have to perform useless setTimeout to prevent uncontrolled web pages from hogging the CPU.

So setTimeout does allow the browser to do some work (unlike MicroTasks) before the callback function is called. However, if you want to do input or render operations before a callback, setTimeout is generally not the best choice, as it only occasionally allows other operations to be done before a callback. There are now better browser apis that hook more directly into the browser rendering system.

setImmediate

Before moving on to using “Better browser apis,” there’s one thing worth mentioning here. Called setImmediate for lack of a better word… It’s strange. If you look it up at caniuse.com, you’ll find that only Microsoft browsers support it. But it also exists in Node.js. What the hell is this thing?

SetImmediate was originally proposed by Microsoft to solve the aforementioned setTimeout problem. Basically, setTimeout has been abused, and setImmediate(0) is really just setImmediate(0), not a thing that is limited to 4 milliseconds. You can view some Discussion about it from Jason Weber back in 2011.

Unfortunately, setImmediate is only used by IE and Edge. Part of the reason it’s still used is because it works so well in Internet Explorer, which allows input events such as keyboard input and mouse click “skip queue” to be executed before the setImmediate callback, whereas setTimeout doesn’t have the same magic in Internet Explorer. (Edge finally solved this problem, as detailed in the previous article).

Also, the fact that setImmediate exists in Node means that many “Node-polyfilled” codes use it in browsers without really knowing what it’s doing. The distinction between Process. nextTick and setImmediate in Node is confusing, and even the official Node documentation states that names should be swapped. For the purpose of this article, however, I’ll focus on browsers rather than Nodes, since I’m not a Node expert.

Minimum rule: If you know what you’re doing and are trying to optimize IE’s input performance, use setImmediate. If not, don’t bother. (or only used in Node)

requestAnimationFrame

Right now, we have the most important alternative to setTimeout, a timer that actually hangs in the browser rendering loop. By the way, if you don’t know about browser event loops, I highly recommend this lecture by Jake Archibald.

RequestAnimationFrame basically works like this: it’s a bit like setTimeout, but it’s called the next time the browser redraws, rather than waiting for some unpredictable amount of time (4ms, 16ms, 1sec, etc.). Now, as Jake pointed out in his talk, there is a small problem that in Safari, IE and Edge 18 versions below, he executes after the style/layout calculation. But let’s ignore it, because it’s not a very important detail.

I think requestAnimationFrame is used like this: Whenever I know I’m going to change a browser style or layout — for example, changing a CSS property or launching an animation — I put it in the requestAnimationFrame (abbreviated rAF here). This ensures a few things:

  1. I’m unlikely to mess with the layout because all the DOM changes are queued and coordinated.
  2. My code naturally ADAPTS to the browser’s performance characteristics. For example, if here a device with a low configuration is trying to render some DOM elements, rAF will naturally slow down from the usual 16.7ms (on a 60Hz screen) interval, so it won’t crash the device as it would with a lot of setTimeout or setInterval running.

This is why animation libraries that don’t rely on CSS transformations or Keyframes, such as GreenSock or React Motion, are usually changed in rAF callbacks. If an element is animating between opacity: 0 and opacity: 1, it makes no sense to queue for a billion callbacks to process every possible intermediate state, including opacity: 0.0000001 and opacity: 0.9999999.

Instead, you’re better off just using rAF and letting the browser tell you how many frames you can draw in a given period of time and calculate for that particular frame. Thus, slower devices will naturally end up with a slow frame rate and faster devices with a fast frame rate, which is not possible with an API like setTimeout that is independent of the browser’s drawing speed.

requestIdleCallback

RAF is probably the most useful timer in toolkit, but the requestIdleCallback is also worth mentioning. Browser support is not great, but there is a Polyfill that works well (using rAF underneath).

In many cases rAF is similar to requestIdleCallback. (Abbreviated to rIC from here)

Like rAF, rIC naturally ADAPTS to the browser’s performance characteristics: if the device is overloaded, rIC may delay. RIC is different in that it fires in the browser’s idle state, for example, when the browser determines that it has no other tasks, microtasks, or input events to process, and you’re free to do whatever you want. It also gives you a “deadline” to track the budget used, which is a nice feature.

Dan Abramov has a great talk at Iceland JSConf 2018 in which he shows how to use rIC. In conversation, a WebApp calls rIC every time the user types, and it updates the render state in the callback. This is great because a fast typing user will cause keyDown/KeyUp events to fire very quickly, but you don’t want to re-render the page for every key.

Another good example is the “remaining character count” indicator on Twitter or MastoDon. In Pinafore, I use rIC for my operations because I don’t really care if the indicator is re-rendered for every input I make. If I’m typing fast, it’s best to type accordingly first so I don’t lose the flow.

In Pinafore, the little bar below the input box and the “remaining characters” prompt update as you type.

I noticed that rIC was a bit flawed in Chrome. In Firefox, it runs whenever I intuitively think the browser is idle and ready to run some code. (This is also true in Pollyfill.) But in Chrome’s Android Mobile mode, I noticed that every time I touched scroll, it delayed the rIC for a few seconds, even after I had touched the screen, the browser didn’t do anything. (I suspect that’s what I’m seeing.)

Update: Alex Russell from the Chrome team has informed me that this is a known bug and should be fixed soon!

Anyway, rIC is another good tool. I tend to think of rAF for critical rendering work and rIC for non-critical rendering work.

Debounce and throttle

There are two non-browser-built methods, but they are useful and worth knowing. If you’re not familiar with them, here’s a great guide to CSS tricks

The standard use of debounce is in the resize callback. When the user resizes the browser window, there is no need to update the layout with every resize callback because it is triggered too often. Instead, you can debounce hundreds of milliseconds, which ensures that the callback is triggered after the user has processed the window size.

Throttle, on the other hand, is something I use more. For example, the Scroll event is a great use example. Again, it doesn’t make sense to update the view state for every Scroll callback because the frequency is so high (it varies from browser to browser, from input method to input method). Using Throttle, you can regulate this behavior and ensure that it fires only every X milliseconds. You can adjust the timing of Lodash’s Throttle (or Debounce) method to start the delay at the end or not at all.

Instead, I don’t use Debounce in scrolling scenarios because I don’t want the UI to update only after the user explicitly stops scrolling. This can be frustrating and confusing to the user, and trying to scroll to keep updating the UI state (for example, in an infinite scrolling list).

I use throttle for various user inputs and some scheduled tasks, such as IndexedDB cleanup. Perhaps one day it will be built into the browser.

conclusion

Here’s my quick look at the various timers in browsers and how to use them. I may have missed some because there are some special features (postMessage or Lifecycle Events, anything else?) . But hopefully this will at least give me a good overview of how I see timers in JavaScript.