<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NICK</title>
    <description>The latest articles on DEV Community by NICK (@devkit).</description>
    <link>https://dev.to/devkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4184977%2F80c7d530-b690-4bd6-aa08-f4c77c0ffb02.png</url>
      <title>DEV Community: NICK</title>
      <link>https://dev.to/devkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devkit"/>
    <language>en</language>
    <item>
      <title>setInterval can't keep time: building a metronome that doesn't drift</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Mon, 26 Oct 2026 00:46:29 +0000</pubDate>
      <link>https://dev.to/devkit/setinterval-cant-keep-time-building-a-metronome-that-doesnt-drift-372a</link>
      <guid>https://dev.to/devkit/setinterval-cant-keep-time-building-a-metronome-that-doesnt-drift-372a</guid>
      <description>&lt;p&gt;Open a metronome built with &lt;code&gt;setInterval&lt;/code&gt;, set it to 120 BPM, leave it running for two minutes, then play a click track next to it. They will not still be together.&lt;/p&gt;

&lt;p&gt;This is not a bug in the metronome. It is &lt;code&gt;setInterval&lt;/code&gt; doing exactly what it promises, which is "run this callback roughly every N milliseconds, if you can." A metronome needs something much stronger: "produce a sound at this exact point in time, no matter what else the page is doing."&lt;/p&gt;

&lt;p&gt;I ran into this when building &lt;a href="https://drumdesk.net" rel="noopener noreferrer"&gt;DrumDesk&lt;/a&gt;, a couple of browser-based practice tools. The fix is older than most of the frameworks I use, and it is worth writing down because the wrong version sounds &lt;em&gt;almost&lt;/em&gt; right — which is worse than sounding obviously broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways &lt;code&gt;setInterval&lt;/code&gt; loses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The main thread owns it.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; callbacks go into the same queue as your event handlers, layout, garbage collection, and third-party scripts. If the browser spends 180ms doing layout, your click is 180ms late. It does not get rescheduled to catch up — it just arrives late, and then the next one arrives on the new, slightly wrong schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Background tabs get throttled.&lt;/strong&gt; Browsers clamp timers in inactive tabs — historically to 1 second, and still to at least a few hundred milliseconds in most cases. A user who switches to their email to look up a tempo marking comes back to a metronome that is no longer a metronome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Errors accumulate.&lt;/strong&gt; &lt;code&gt;setInterval(fn, 500)&lt;/code&gt; does not mean "call &lt;code&gt;fn&lt;/code&gt; every 500ms of real time." It means "wait at least 500ms between calls." Every delay is added to the timeline permanently. Small delays are the norm, so the drift is monotonic — it only ever goes one way.&lt;/p&gt;

&lt;p&gt;The third one is what makes it insidious. A metronome that is 8ms fast sounds fine. A metronome that gets 8ms slower every bar is a different instrument after a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two clocks, and only one of them can be trusted
&lt;/h2&gt;

&lt;p&gt;The thing that changed my approach: &lt;strong&gt;Web Audio has its own clock.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AudioContext.currentTime&lt;/code&gt; is driven by the audio hardware, not by the main thread. It advances in real time even when your JavaScript is blocked, because the audio pipeline runs on a separate real-time-priority thread. If the main thread stalls for 200ms, &lt;code&gt;currentTime&lt;/code&gt; keeps moving and the sounds you already scheduled still fire exactly when you scheduled them.&lt;/p&gt;

&lt;p&gt;So the architecture becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A slow, sloppy timer&lt;/strong&gt; that periodically wakes up and asks "do I need to schedule more sounds yet?" It is allowed to be late. Nobody hears it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The audio clock&lt;/strong&gt; that decides exactly &lt;em&gt;when&lt;/em&gt; each sound plays. It is never late.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the pattern Chris Wilson described as "A Tale of Two Clocks" in 2013, and it is still the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler
&lt;/h2&gt;

&lt;p&gt;The trick is a lookahead queue. Every 25ms, look 150ms into the future and schedule everything that falls inside that window. Then go back to sleep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// when the next note should sound&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;LOOKAHEAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// seconds of future to pre-schedule&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// ms between wake-ups&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;LOOKAHEAD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* isRightHand */&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;stepSeconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// one subdivision&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;INTERVAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers, two different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;INTERVAL = 25&lt;/code&gt; can drift, jitter, or arrive 60ms late. It does not matter, because its only job is to keep the buffer full.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LOOKAHEAD = 0.15&lt;/code&gt; is the safety margin. Any main-thread stall shorter than 150ms is completely absorbed. A stall longer than that will produce a gap — but that is a genuinely broken page, not a busy one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;while&lt;/code&gt; loop is the important part. If the timer fires late, the loop runs several times in one go and catches up. Compare that to &lt;code&gt;setInterval&lt;/code&gt;, which can only ever fire once per tick and therefore falls further behind forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a click that doesn't sound like a click
&lt;/h2&gt;

&lt;p&gt;Scheduling a sample-accurate start time is half of it. The other half is making the sound not be a pop.&lt;/p&gt;

&lt;p&gt;A bare oscillator started and stopped abruptly produces a discontinuity in the waveform, which you hear as a click — an ugly one, distinct from the click you actually wanted. The fix is a fast envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.16&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;osc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isRight&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1180&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;790&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// start near zero&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exponentialRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 4ms attack&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exponentialRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// decay to silence&lt;/span&gt;

  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                               &lt;span class="c1"&gt;// small tail&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in there are worth stealing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It starts at &lt;code&gt;0.0001&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;exponentialRampToValueAtTime&lt;/code&gt; cannot ramp to or from zero — a zero target throws, and a zero start makes the ramp mathematically meaningless. &lt;code&gt;0.0001&lt;/code&gt; is the usual idiom. You will find this out the hard way otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The oscillator is stopped slightly after the envelope ends.&lt;/strong&gt; &lt;code&gt;dur + 0.01&lt;/code&gt; gives the gain node time to finish ramping before the source dies. Without it you can get a truncated tail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right and left hand get different pitches.&lt;/strong&gt; 1180 Hz against 790 Hz is roughly seven semitones apart — far enough to hear which hand is which, close enough that it still reads as one instrument. For rudiment practice this matters more than any visual notation: you hear a wrong sticking before you see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual can lag. It can't disagree.
&lt;/h2&gt;

&lt;p&gt;Once the audio is sample-accurate, the UI becomes the problem. If you highlight the current beat inside the scheduler loop, the highlight happens ~150ms &lt;em&gt;before&lt;/em&gt; the sound — because you scheduled it, you did not play it.&lt;/p&gt;

&lt;p&gt;The fix is to keep the visual on a separate timer that fires when the note is actually due:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderSticking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A visual that is 20ms behind the audio is invisible. A visual that is 150ms ahead makes the whole tool feel broken, even though the audio is perfect. Humans forgive latency; they do not forgive contradiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing: the context starts suspended
&lt;/h2&gt;

&lt;p&gt;Browsers will not let you make noise before a user gesture. &lt;code&gt;new AudioContext()&lt;/code&gt; creates a context in &lt;code&gt;suspended&lt;/code&gt; state, and every &lt;code&gt;start()&lt;/code&gt; on a suspended context is silently dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it on every play action rather than once at load — the context can be suspended again after a tab has been backgrounded, and "the metronome works until I switch tabs" is a confusing bug report to receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with the browser at all
&lt;/h2&gt;

&lt;p&gt;Because the alternative is asking someone to install an app to hear a click.&lt;/p&gt;

&lt;p&gt;The whole premise of these tools is that a drummer sits down for ten minutes, wants a metronome and a subdivision grid, and should not have to create an account, download 40MB, or watch a splash screen first. Browser audio was "not good enough" for long enough that the assumption stuck around. With a lookahead scheduler it is sample-accurate, and the latency budget belongs to the audio thread rather than to whatever else the page is doing.&lt;/p&gt;

&lt;p&gt;The tuner next to it uses the same clock for a different job: &lt;code&gt;f = base × (A4 / 440) × 2^(cents / 1200)&lt;/code&gt;, which is boring, correct, and instantly verifiable against a reference tone.&lt;/p&gt;

&lt;p&gt;If you want to hear the result rather than read about it, &lt;a href="https://drumdesk.net/tools/rudiment-trainer/" rel="noopener noreferrer"&gt;the rudiment trainer is here&lt;/a&gt; — 40 rudiments, left and right hand at different pitches, 40–220 BPM, subdivisions, and a random mode. No account, no install, nothing stored. It exists because the &lt;code&gt;setInterval&lt;/code&gt; version I wrote first was, to put it kindly, not a metronome.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>audio</category>
      <category>performance</category>
    </item>
    <item>
      <title>The 20-second rule: when a browser tool beats an app</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:27:06 +0000</pubDate>
      <link>https://dev.to/devkit/the-20-second-rule-when-a-browser-tool-beats-an-app-2ing</link>
      <guid>https://dev.to/devkit/the-20-second-rule-when-a-browser-tool-beats-an-app-2ing</guid>
      <description>&lt;p&gt;There is a threshold I stopped arguing with: if a job takes less than twenty seconds, I refuse to install anything for it.&lt;/p&gt;

&lt;p&gt;Not because installers are evil. Because the overhead is invisible until you count it. An installer is a download, a EULA, a restart, an update notification six weeks later, and one more icon competing for your attention. For a task I do twice a month, the tax is bigger than the task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually crosses the line&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tasks below are all under ten seconds of work once the page is open, which is why they live in a browser tab instead of a menu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a percentage change between two numbers I read somewhere&lt;/li&gt;
&lt;li&gt;a timestamp someone pasted in a Slack thread&lt;/li&gt;
&lt;li&gt;a subnet mask I want to sanity-check before touching a config&lt;/li&gt;
&lt;li&gt;the length of a string I just typed into a validation rule&lt;/li&gt;
&lt;li&gt;a currency or unit conversion, usually while reading documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these deserve an app. All of them are one search away from a site that wants my email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule, stated properly&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the tool does not need my data, does not need to run in the background, and does not need a history, it should not be installed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last clause is the interesting one. History is the only real reason to install a desktop tool: a timestamp converter that remembers my last ten inputs is genuinely better than one that does not. Everything without state should be a page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this gets uncomfortable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browser tools have a bad reputation because most of them are pageviews with a text box. You can usually tell within five seconds: if the page asks you to sign up before it shows a result, it is not a tool, it is a funnel. If it works with JavaScript disabled, it is probably a real calculator.&lt;/p&gt;

&lt;p&gt;The collection I keep on a pinned tab is &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;1024Toolkit&lt;/a&gt; — 45 single-purpose pages (converters, calculators, encoders, generators), no account, nothing to install. I pin it because the alternative is a search results page full of ads pretending to be tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part I did not expect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treating small jobs as browser jobs changed what I install, not how much I work. My machine has fewer programs on it than it did a year ago, and I have not missed one. The twenty-second rule quietly deleted a whole category of software from my life.&lt;/p&gt;

&lt;p&gt;If you want to test the rule, pick the smallest thing you did today — the one you would be embarrassed to install an app for — and see whether a page does it. It usually does.&lt;/p&gt;

</description>
      <category>calculator</category>
      <category>converter</category>
      <category>onlinetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>45 free browser tools that answer one question well</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:04:26 +0000</pubDate>
      <link>https://dev.to/devkit/45-free-browser-tools-that-answer-one-question-well-2hd7</link>
      <guid>https://dev.to/devkit/45-free-browser-tools-that-answer-one-question-well-2hd7</guid>
      <description>&lt;p&gt;Most calculator sites have a business model that works against the answer. You get four fields and a paywall, or a page that needs an account before it shows a number, or a result that arrives wrapped in three ad blocks. The question underneath never changed: &lt;em&gt;how much, how many, how long?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1024Toolkit&lt;/strong&gt; is my attempt at the opposite. It is a free collection of &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;45 browser tools&lt;/a&gt; for finance, health, engineering and development. Each tool answers one question well and runs entirely in your browser — no sign-up, no account, nothing uploaded, four languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "one question well" means in practice
&lt;/h2&gt;

&lt;p&gt;Take compound interest. The question is not "can you model monthly contributions with variable rates and a tax wrapper?" It is: &lt;strong&gt;what does £150 a month become at 6% over fifteen years?&lt;/strong&gt; One number, one curve, two fields. Anything more belongs in a spreadsheet.&lt;/p&gt;

&lt;p&gt;The same discipline shaped the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Position sizing&lt;/strong&gt; — what should this trade be, given my account and my risk per trade?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BMI&lt;/strong&gt; — one weight, one height, one classification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concrete volume&lt;/strong&gt; — how many bags for 2.4 m³, and how much does that cost?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron expressions&lt;/strong&gt; — what does &lt;code&gt;0 3 * * 1&lt;/code&gt; actually run, and when is the next run?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet maths&lt;/strong&gt; — what does a /27 leave me with?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why everything stays client-side
&lt;/h2&gt;

&lt;p&gt;Every calculation runs in JavaScript in your own tab. There is no server to send numbers to, which means there is also no database to breach and no "free tier" to upgrade from. It also means the tools keep working when a train tunnel eats your signal — as long as the page is open, the maths works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;Honestly: anyone who re-googles the same conversion more than twice a week. Developers get cron, subnet and timestamp tools; investors get compound interest and position sizing; DIY gets concrete and paint coverage; the gym gets BMI and plate math.&lt;/p&gt;

&lt;p&gt;The full set lives at &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;1024toolkit.com&lt;/a&gt; — one tab, 45 answers, no account.&lt;/p&gt;

</description>
      <category>calculator</category>
      <category>converter</category>
      <category>onlinetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>45 tiny tools, zero hassle: what I learned shipping one-page utilities</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:36:41 +0000</pubDate>
      <link>https://dev.to/devkit/45-tiny-tools-zero-hassle-what-i-learned-shipping-one-page-utilities-40om</link>
      <guid>https://dev.to/devkit/45-tiny-tools-zero-hassle-what-i-learned-shipping-one-page-utilities-40om</guid>
      <description>&lt;p&gt;Every few weeks I hit the same small wall.&lt;/p&gt;

&lt;p&gt;I need to know what &lt;code&gt;0 3 * * 1&lt;/code&gt; actually means. Or how many bags of concrete 2.4 m³ is. Or what&lt;br&gt;
£150 a month turns into at 6% over fifteen years. Nothing hard — just a question that should take&lt;br&gt;
ten seconds to answer.&lt;/p&gt;

&lt;p&gt;And every time, the same routine: land on a site that wants an account, or pushes my numbers to a&lt;br&gt;
server, or is more ad than page.&lt;/p&gt;

&lt;p&gt;So I started collecting the answers I kept needing as single static pages. That collection is now&lt;br&gt;
&lt;strong&gt;45 tools&lt;/strong&gt;, and it lives here: &lt;strong&gt;&lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;https://1024toolkit.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint I set myself
&lt;/h2&gt;

&lt;p&gt;One rule shaped everything else: &lt;strong&gt;a tool must answer one question, and it must work without me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No account.&lt;/strong&gt; Nothing to sign up for, nothing to remember, nothing to reset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing you type is stored.&lt;/strong&gt; Whatever you enter stays in the tab. (A couple of tools need live
data — the stock lookup, for instance — and fetch it through a thin proxy. Nothing you type is
saved there either.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One question per page.&lt;/strong&gt; If a tool needs a manual, it is the wrong tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline of "one question per page" sounds limiting. In practice it is the opposite — it&lt;br&gt;
makes every addition obvious. Either the tool collapses into a single input and a single result,&lt;br&gt;
or it does not belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in there
&lt;/h2&gt;

&lt;p&gt;45 tools, in four loose clusters plus a long tail of everyday ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finance&lt;/strong&gt; — compound interest, savings-rate, discount, options payoff, position sizing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering&lt;/strong&gt; — concrete volume, cut lists, triangle geometry, great-circle distance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health&lt;/strong&gt; — BMI, BMR, TDEE, heart-rate zones, A1C&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer utilities&lt;/strong&gt; — cron, regex, timestamps, time zones, subnets, SQL formatting, mock data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer ones are the reason I started. I got tired of opening a heavyweight site to decode a&lt;br&gt;
cron string I was staring at anyway.&lt;/p&gt;

&lt;p&gt;The whole site is also available in four languages (English, German, Japanese and Simplified&lt;br&gt;
Chinese), which turned out to be far less work than expected once the tools were small enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting the same thing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Ship the boring version first.&lt;/strong&gt; A page with an input, a label and a result is a legitimate&lt;br&gt;
tool. I have talked myself out of more tools by planning a "proper" version than by any other means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Name the tool the way people search for it.&lt;/strong&gt; "Cron expression parser" beats "TimeKit" every&lt;br&gt;
single time. The name is the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Almost no backend is a feature, not a shortcut.&lt;/strong&gt; Every server you do not run is a data breach&lt;br&gt;
you cannot have, a bill you do not pay, and a page that keeps working when something upstream is&lt;br&gt;
down. The handful of endpoints I do run exist only to keep an API key off the client — never to hold&lt;br&gt;
your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;These are small tools. None of them is clever. The value is that they are &lt;em&gt;there&lt;/em&gt;, they load&lt;br&gt;
instantly, and they never ask for anything.&lt;/p&gt;

&lt;p&gt;If you use one and it is missing an input you need, or the answer is wrong, I would genuinely like&lt;br&gt;
to know — that is the most useful feedback I can get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;https://1024toolkit.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
