Precise synchronization of scalable clocks in different browsing contexts
Let’s start with a clock running in real time in the current browsing context. (Its display resolution is 10 milliseconds, but that was an arbitrary choice.)
Now, let’s open another clock instance in a different browsing context (e.g. a new window (tab or popup), an
iframe, or a
WebWorker). For this blog post, we’ll choose a popup window. (your browser may block this popup, in which case tell it allow it).
The next steps will work correctly only after opening the popup window.
The clocks are now updating themselves as fast as the browser permits. But, to be able to get more meaningful time readings, let’s stop the automatic updates and let’s update the clocks only when manually requested by pushing the button. Note that the time still progresses normally in the background (you can confirm this by repeatedly updating). You also can start the again.
The clocks internally do not use any kind of loop or
setTimeout() or something equivalent. The time is
simply calculated from values from the
performance browser API whenever the clock is updated (e.g.
manually). In other words, if the clock does not update, no calculation at all takes place.
After a manual update, the two clocks show the same time.
This is possible because the
latency between pushing the “Manual Redraw” button and the reception of this
message in the popup window is less than the display resolution (it is approx. 1 millisecond on a desktop browser). However, this can still cause a one-higher
number in the popup when the time value wraps around to the next 10 ms unit. But this only because of the message latency, and not because the clocks are desynchronized. The clocks are always synchronized because
performance.now() is a monotonic clock with a fixed absolute time offset per browsing context.
So far, there is nothing special or difficult about this example. The MDN documentation⮺ of
performance.timeOrigin shows example code to achieve this.
However, if we want to synchronize the clocks including a varying clock scale factor (e.g. double speed, half speed, etc. while the clocks are running), the implementation becomes more difficult (but still straightforward to solve with a bit of maths).
To demonstrate, select one of the following clock scale factors:
The two clocks remain exactly synchronized even after varying the scale factor.
One application of this technique is in real-time, time-discrete simulations running in different browser threads for performance reasons, that need to remain synchronous even when scaling the flow of time, e.g. in a scenario of slow motion or time lapse.
This technique remains precise even with the jitter of
requestAnimationFrame which can be in the order of milliseconds. This is demonstrated by the fact that, as already mentioned, no calculation at all takes place when the clock is not updated; the clock updates may thus arrive in any, not even periodic, interval.