What is it?
Get On With It is a time-tracking app designed to improve your productivity by using the Pomodoro technique. The Pomodoro technique alternates sessions between twenty-five minutes of working and five minute breaks, which stops you from losing focus when working on a task. Get On With It also allows you to switch to a tabata timer, which is a technique used in fitness known as high-intensity interval training: this is similar to the Pomodoro technique, but mixes twenty seconds of exercise with ten-second breaks.
The timer also has notifications to show you when each session has ended, and allows you to pause and resume the timer as needed.
What's the stack?
The technology stack I used for Get On With It is React and Redux; the timer is a static site, so there is no backend server component. Redux seemed to fit well with the timer, since it meant I could send signals around the app to update the time, which made pausing the timer really easy. It also meant I could read the precise state of the app at any time, which was useful for changing between Pomodoro and tabata timers, and meant that the timer could keep running even when the views in the app changed.
I wrote the app in ES6 because why would you create a new project using older versions, and transpiled the app using Babel and Webpack. The styles were all created with Sass, which again was compiled using Webpack.
Running timers in the background
Timers in Javascript are always fun. Initially, I used a setTimeout
call to count down the remaining time. This worked fine as long as you kept the tab open, but as soon as you changed tab or minimised the browser, then updates slowed down and stopped happening every second. This turns out to be by design - not only is setTimeout
not accurate by design (ie setTimeout(() => something(), 1000)
is not guaranteed to run after a second depending on what else the browser is doing), it turns out to also be throttled if your tab is in the background. This is designed to avoid wasting processing power for inactive tabs, and so help improve battery life on phones and laptops, but means that setTimeout
isn't very useful for making a timer.
Delta time
To fix the inaccurate timer caused by setTimeout
, I changed the callback it triggered to check the delta time (the difference between now and the last time it checked) between callbacks.
// loads of code missed out for brevity
setTimeout(() => this.updateTimer(Date.now()), 1000);
updateTimer = timestamp => {
// if the time passed in is more than a second later than the last time the function was called,
// then do something
if(timestamp - lastTimestamp > 1000) {
// update timer
}
}
If you've ever played around with game code, this may look familiar - this splits the update call away from the processing speed (ie how fast the computer can call updateTimer
) and makes it use the time since the last function call instead.
This worked, but not very well. setTimeout
still wasn't being called regularly, so the update function often had more than a second between calls, and usually not a whole second. This meant that sometimes the timer would skip two seconds at once since the rounding errors accumulated, and I had to start tracking fractions of a second which made the state and the code quite confusing and overly-complicated. The timer was more accurate in actually keeping to time - so instead of ten minutes actually taking fifteen because setTimeout
wasn't being called every second, it took the correct amount of time - but looked more broken since it was quite obvious when the timer jumped a number in the countdown.
Web workers
The only real way to get a fairly-accurate timer which carries on running up-to-speed when your tab is inactive is to use a web worker. Web workers are separate Javascript threads which run independently from the main UI thread, so aren't affected by browser throttling. It was actually pretty simple to convert my code to use a worker script - I moved the timeout logic to a separate file and changed it to a setInterval
call (more on that in a bit), then changed my calling code to use the worker instead of a function. Workers can only be interacted with via a message system: you can post a message to your worker using worker.postMessage
and listen to messages back via worker.onmessage
. This is why I changed to a setInterval
- I could only tell the worker to start counting down or stop counting, not to schedule more setTimeout
s which I was doing previously as a basic form of throttling. Webpack makes loading workers really simple too: you just need to change your import to have worker-loader!
at the start to tell a plugin to load the file correctly.
Getting an accurate(ish) timer in Javascript was probably the trickiest part of this project, but did mean I got to use a lot of interesting APIs.
What's next?
I've got a long list of more features to add to the timer - I want to be able to track how many work sessions I complete per day, and add alarm sounds to the notifications so you don't miss the end of a session. The site's been live for almost two months as of writing this and seems to have picked up a few users already, so it'd be good to find out which features to prioritise from my users.