Published Build3 min read
Three browser APIs, not a tighter loop: what a countdown timer teaches about drift and leases
A dev.to teardown of a vanilla-JS fullscreen timer ends up documenting three failure modes that recur in any system where local state has to agree with the outside world: tick drift, an auto-released lease, and a...
Written for builders.See today for builders
What happened
- A developer publishing as alexdev2 wrote on dev.to about building a fullscreen countdown timer as a plain-JS canvas widget, which powers the timer pages on blankscreen.io.
- The author names three parts that turned out more interesting than expected: keeping accurate time without setInterval drift, stopping the screen from sleeping mid-countdown via the Screen Wake Lock API, and a clean alarm beep with WebAudio and no audio files.
- The naive implementation shown is a setInterval with a 1000 ms delay that decrements a remaining-seconds variable by 1 and re-renders.
- The author states setInterval is not a metronome: the browser throttles it in background tabs, each tick can arrive late even in the foreground, and over a 30-minute timer those errors accumulate into visible drift.
- The fix given is to stop counting ticks and measure elapsed real time instead: store performance.now() and subtract the actual delta, in seconds, from the remaining time each frame.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer publishing as alexdev2 posted a teardown on dev.to of a fullscreen countdown timer written in plain JavaScript, the canvas widget behind the timer pages on blankscreen.io [1]. It is worth reading not because countdowns are hard, but because the three parts the author flags as more interesting than expected map onto three failure modes that show up in much larger systems: drift between a local tick and real time, a lease that gets revoked without asking you, and a capability that exists only at the moment it is granted [2].
The naive version is a setInterval firing every 1000 ms and decrementing a seconds counter [3]. The objection is not aesthetic. According to the post, browsers throttle intervals in background tabs, ticks arrive late even in the foreground, and over a 30-minute timer those errors accumulate into visible drift [4]. Asking for 1000 ms more politely does not help, because the counter is measuring how often it got called, not how much time has passed. The fix is to stop counting ticks: record performance.now() and subtract the actual delta each frame [5], driven from requestAnimationFrame, which in this build also draws the progress bar and the end-of-timer flash [6]. The author reports the displayed time then tracks the wall clock regardless of frame rate or minor jank [7]. That is the same discipline as preferring a monotonic reference over a local counter anywhere else. Formatting is separate and fiddly: hours are shown only when the total is at least 3600 seconds, with the remainder ceilinged and zero-padded [8].
The screen sleeping mid-countdown is a lease problem. The post requests the Screen Wake Lock behind a feature check and a try/catch, failing silently when it is denied or unsupported [9]. Two operational details matter more than the call itself. The lock is auto-released when the tab loses visibility, so the code re-requests it on visibilitychange when the timer is still running [10]. And it has to be requested inside the user gesture that starts the timer, then released on pause or reset so a stopped timer is not holding someone's screen on [11]. Support is not universal, so the author treats it as progressive enhancement and never lets a rejection break the countdown [12].
The alarm is a permissions problem dressed as an asset problem. Shipping an alarm.mp3 buys a network request, autoplay-policy trouble, and a file that can fail to load exactly when it is needed [13]. Instead the beep is synthesized: a sine oscillator at 880 Hz stepping down to 660 Hz at 0.18 s, with gain ramped to 0.35 over about 20 ms and back to near-silence by 0.5 s to avoid clicks [14], the oscillator stopping at 0.52 s [15]. That is a 520 ms two-tone beep [1], repeated on a 900 ms interval until the user dismisses it [16], leaving roughly 380 ms of silence between beeps [2]. The load-bearing detail: create or resume the AudioContext inside the start gesture, not at page load, because browsers block audio that is not tied to interaction, and a context spun up at zero gets its beep swallowed [17]. Priming a capability early because you cannot acquire it later is not a browser quirk; it is the same reasoning as opening a connection before you need to write to it.
The whole widget is around 80 lines [18]. None of those lines are a tighter loop.
What to watch: whether the wake lock re-request on return to visibility [10] and the still-live AudioContext [17] both survive a user who starts a timer and immediately switches tabs, since that is the path where all three mechanisms are exercised at once. The post does not quantify the drift it is correcting, only that it becomes visible over 30 minutes [4], so anyone porting this should measure their own before and after.
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A developer publishing as alexdev2 wrote on dev.to about building a fullscreen countdown timer as a plain-JS canvas widget, which powers the timer pages on blankscreen.io.
- [2]
The author names three parts that turned out more interesting than expected: keeping accurate time without setInterval drift, stopping the screen from sleeping mid-countdown via the Screen Wake Lock API, and a clean alarm beep with WebAudio and no audio files.
- [3]
The naive implementation shown is a setInterval with a 1000 ms delay that decrements a remaining-seconds variable by 1 and re-renders.
- [4]
The author states setInterval is not a metronome: the browser throttles it in background tabs, each tick can arrive late even in the foreground, and over a 30-minute timer those errors accumulate into visible drift.
- [5]
The fix given is to stop counting ticks and measure elapsed real time instead: store performance.now() and subtract the actual delta, in seconds, from the remaining time each frame.
- [6]
The tick function is driven by requestAnimationFrame and renders to a canvas, because the same loop also draws a progress bar and a time's-up flash; the timekeeping approach is described as framework-agnostic and works for updating a DOM node.
Sources & coverage · 1 publisher
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- dev.toAlex OliinykAug 13Building a fullscreen countdown timer in vanilla JS: accurate time, Wake Lock, and a WebAudio alarm
Cited in this coverage: dev.to post by alexdev2

