Max Schmitt

November 28 2013

setInterval / setTimeout slows down on TAB change

For my current project, I'm using setInterval and setTimeout for central parts of my application.

The thing is, that you can usually run these functions reliably at about 5-50ms accuracy. However, if you switch TABs or minimize the browser window, these timers slow down a lot. As far as I know, you can then only trust them running every 1000ms or so. My guess is that this is a performance feature of modern browsers.

They make the assumption that if the particular web app isn't visible for the user, they can slow down its timers to save performance. This is pretty reasonable in general, but sometimes you just need to keep things running.

How to prevent the setInterval / setTimeout slow down on TAB change

The solution to this problem is a great HTML5-feature called "web workers". You've probably heard of them before: they allow your Javascript-application to run multiple threads. Here is a short overview.

Starting position: calling a simple function with vanilla setInterval

JS

setInterval(function () {
console.log('hi')
}, 500)

This works great and prints "hi" to the console every ~500ms to the console. However, once you change TABs, "hi" will only be printed every ~1000ms.

Goal: calling any function with worker-enhanced setInterval

JS

workerTimer.setInterval(function () {
console.log('hi')
}, 500)

This is how we want to call our function so that it prints "hi" every ~500ms even if we switch TABs or minimize the browser window.

Of course this workerTimer-object isn't provided by vanilla Javascript so we have to create it. It's not complicated at all, so here's a very simple implementation for setInterval:

worker-timer.js

JS

var worker = new Worker('timer-worker.js')
var workerTimer = {
id: 0,
callbacks: {},
setInterval: function (cb, interval, context) {
this.id++
var id = this.id
this.callbacks[id] = { fn: cb, context: context }
worker.postMessage({
command: 'interval:start',
interval: interval,
id: id,
})
return id
},
onMessage: function (e) {
switch (e.data.message) {
case 'interval:tick':
var callback = this.callbacks[e.data.id]
if (callback && callback.fn) callback.fn.apply(callback.context)
break
case 'interval:cleared':
delete this.callbacks[e.data.id]
break
}
},
clearInterval: function (id) {
worker.postMessage({ command: 'interval:clear', id: id })
},
}
worker.onmessage = workerTimer.onMessage.bind(workerTimer)

As you can see at line 1, our worker is contained in a separate script. You can hack together an inline-implementation but I wanted to keep it simple for now.

This is what the worker-script looks like:

timer-worker.js

JS

var intervalIds = {}
self.onmessage = function (e) {
switch (e.data.command) {
case 'interval:start':
var intvalId = setInterval(function () {
postMessage({
message: 'interval:tick',
id: e.data.id,
})
}, e.data.interval)
postMessage({
message: 'interval:started',
id: e.data.id,
})
intervalIds[e.data.id] = intvalId
break
case 'interval:clear':
clearInterval(intervalIds[e.data.id])
postMessage({
message: 'interval:cleared',
id: e.data.id,
})
delete intervalIds[e.data.id]
break
}
}

That's about it. Now if you call workerTimer.setInterval, a native setInterval-function will actually be called in a separate thread. The callback you provide is still being called in the main thread though, so you get all the flexibility of the native setInterval.

If you're missing setTimeout-functionality for workerTimer, you can easily extend the object and the worker-script. If more people find this useful, I'll create a github-repository so that people can contribute.

I hope this helped, see you soon! :)