Creating an “Accurate” JavaScript Timer

In Vanilla JavaScript and in React

In JavaScript, the way to schedule something to happen at a specific time in the future is with the setTimeout or setInterval methods. For example:

setTimeout(doSomething(), 2000);

Will call the function ‘doSomething’ 2000 milliseconds (or 2 seconds) from now. However, this is not entirely true. To understand why, we need to discuss the JavaScript event loop.

JavaScript Event Loop

While executing code, JavaScript instructions are placed on the call stack. If they can be executed right away, they are. If not, the next instruction is placed on the call stack. When an asynchronous instruction is encountered, it is removed from the call stack and placed in the event table until what the asynchronous instruction is waiting for occurs. At this point, it is placed in the event queue where is waits until the call stack is empty. Once the call stack is empty, the asynchronous instruction is placed back on the call stack and executed.

For more information on how this works see my article on The JavaScript Event Loop

An Asynchronous example

When you make a call to setTimeout, your browser does not wait around for the delay time you specified in the setTimeout method to pass. Instead, your call to the setTimeout method is moved to the event table where it waits for the delay time to pass. Once the delay time has passed, it is moved to the event queue where it waits for the call stack to be empty. Once the call stack is empty, the function you passed to setTimeout will be placed on the call stack and executed.

The following code illustrates how this works:

console.log(‘first’);
setTimeout(() => console.log(‘second’), 0);
console.log(‘third’);

Since the time passed to the setTimeout method is zero milliseconds you might expect the following to be logged to the console:

“first”
“second”
“third”

What actually happens, is the setTimeout is passed from the call stack to the event table, where it waits for the specified delay time to lapse. In this case, since the delay time is zero, it is immediately passed to the event queue where it waits for the call stack to be empty. For the code above, the call stack will not be empty until after “third” has been logged to the console, so this code actually produces:

“first”
“third”
“second”

Since every call to setTimeout has to wait for the call stack to be empty before it can execute the function you pass to it, it will usually execute this function a few milliseconds later than the delay time you specify. This may not sound like much at first, but if you let a timer function run for several minutes it will be noticeably delayed.

How to fix it

The way to fix this is to measure how late each call to setTimeout actually is, and then adjust the next call accordingly. For example: if your delay time is 1000 milliseconds, but the first function call actually happens in 1002 milliseconds, then your next delay time should be 998 milliseconds. It is important that you compare the time of the function calls to the time they should have been called based on the time your timer function was first called. For example, if your delay time is 1000 milliseconds, the second function call should be 2000 milliseconds after the timer function was called, so you should compare the time of the actual second function call to this time. The third function call should be 3000 milliseconds after the timer function was called, and so on…

JavaScript implementation

Here is an implementation of this timer function in JavaScript. This is based on a gist by Alex Wayne. I have simplified his code and added comments to explain the code.

const accurateTimer = (fn, time = 1000) => {
// nextAt is the value for the next time the timer should fire.
// timeout holds the timeoutID so the timer can be stopped.

let nextAt, timeout;
// Initilzes nextAt as now + the time in milliseconds you pass
// to accurateTimer.

nextAt = new Date().getTime() + time;

// This function schedules the next function call.
const wrapper = () => {
// The next function call is always calculated from when the
// timer started.

nextAt += time;
// this is where the next setTimeout is adjusted to keep the
//time accurate.

timeout = setTimeout(wrapper, nextAt — new Date().getTime());
// the function passed to accurateTimer is called.
fn();
};

// this function stops the timer.
const cancel = () => clearTimeout(timeout);

// the first function call is scheduled.
timeout = setTimeout(wrapper, nextAt — new Date().getTime());

// the cancel function is returned so it can be called outside
// accurateTimer.
return { cancel };
};

You call this function by passing as parameters the delay time in milliseconds, and the function you want to be called each time the delay time has passed. The delay time is optional; if omitted, it defaults to zero.

let timer = accurateTimer(() => console.log(‘do something’), 1000);

Assign the return value of accurateTimer to a variable so you can cancel the timer like this:

timer.cancel();

Here is a simple JavaScript timer I created using CodePen:

As a React Component

Here is the same code implemented as a React Component. Again, I have added comments to explain the code:

export default class Heartbeat extends Component {
constructor(props) {
super(props);
// nextBeat is the value for the next time the timer should
// fire.
// timeout holds the timeoutID so the timer can be stopped.

this.state = {
nextBeat: new Date().getTime() +
this.props.heartbeatInterval,
timeout: null
};
}

// When Heartbeat is mounted the value of nextBeat is initilized
// and the first call to the beat method is scheduled.

componentDidMount() {
const nextBeat = new Date().getTime() +
this.props.heartbeatInterval;
this.setState({
timeout: setTimeout(this.beat, nextBeat
— new Date().getTime())
});
}
  // When Heartbeat is unmounted the timer is stopped.
componentWillUnmount() {
clearTimeout(this.state.timeout);
}
  beat = () => {
// Calculates the time when the next function call should
// occur.

const nextBeat = this.state.nextBeat +
this.props.heartbeatInterval;
// Schedules the next call to the beat method, adjusted by
// time the last call was off.

const nextTimeout = setTimeout(this.beat, nextBeat
new Date().getTime());
// Adjusts the state for the next call to the beat method.
this.setState({
nextBeat: nextBeat,
timeout: nextTimeout
});
// Calls the function you passed as a prop
this.props.heartbeatFunction();
};
  // Since the render function cannot be empty, we just render a
// fragment.

render() {
return <Fragment />;
}
}
Heartbeat.propTypes = {
// The delay time in milliseconds.
heartbeatInterval: PropTypes.number,
// The function to call every time the delay time expires.
heartbeatFunction: PropTypes.func.isRequired
};
Heartbeat.defaultProps = {
// If no delay time is provided the default is 1000 milliseconds
// (1 second).

heartbeatInterval: 1000
};

When using React Heartbeat with your component, first import it:

import Heartbeat from ‘react-heartbeat’;

Then render it in your component’s render function:

render() {
return (
<Heartbeat heartbeatFunction={this.count} heartbeatInterval={1000} />
);

React Heartbeat accepts 2 props:

  1. heartbeatFunction: this is the function you want to call after each heartbeatInterval.
  2. heartbeatInterval: this is the time interval in milliseconds you want to be between each call to heartbeatFunction

heartbeatInterval is optional. If you do not provide this prop the default is 1000 milliseconds (1 second).

To stop the timer, stop rendering it. Here is an example of how you can do this:

In the state of your component add a ‘paused’ property as a boolean.

this.state = {
paused: true,
timer: 0
};

Then in the render function set a variable using the ternary operator to ‘null’ if paused is true, and set it to the React Heartbeat component if paused is false. Then render this variable in your component like this:

render() {
const heartbeat =
this.state.paused === true ? null : (
<Heartbeat heartbeatFunction={this.count} heartbeatInterval={50} />
);
return (
<Fragment>
{heartbeat}
</Fragment>
);
}

For an example of this component in action see my Pomodoro Timer. The source code for this project is here.

As an npm Module

I turned this React component into an npm module. If you would like to use it in your projects, you can find it here.

Conclusion

By now you should understand how to create an “accurate” JavaScript timer. This timer will always be accurate within a few milliseconds from the time it was started. This assumes that you are not doing any heavy processing over a long period of time. If your website is keeping your computer so busy that the call stack is never empty, then I am not aware of any method of keeping accurate time in JavaScript. Of course, such a website would be unresponsive and pretty useless.

Related Stories: