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.
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.
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.
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:
var worker = new Worker('timer-worker.js')var workerTimer = {id: 0,callbacks: {},setInterval: function(cb, interval, context) {this.id++var id = this.idthis.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)breakcase '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:
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] = intvalIdbreakcase '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! :)
Hi, I’m Max! I'm a fullstack JavaScript developer living in Berlin.
When I’m not working on one of my personal projects, writing blog posts or making YouTube videos, I help my clients bring their ideas to life as a freelance web developer.
If you need help on a project, please reach out and let's work together.
To stay updated with new blog posts, follow me on Twitter or subscribe to my RSS feed.