<?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: Sahil Nasariwala</title>
    <description>The latest articles on DEV Community by Sahil Nasariwala (@sahilnasariwala).</description>
    <link>https://dev.to/sahilnasariwala</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%2F4105529%2F9fb4b6da-ecd9-49ef-b2ad-a4ef21e62f5c.png</url>
      <title>DEV Community: Sahil Nasariwala</title>
      <link>https://dev.to/sahilnasariwala</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sahilnasariwala"/>
    <language>en</language>
    <item>
      <title>How I built auto-zoom-on-click screen recording inside a Chrome extension</title>
      <dc:creator>Sahil Nasariwala</dc:creator>
      <pubDate>Mon, 12 Oct 2026 09:05:28 +0000</pubDate>
      <link>https://dev.to/sahilnasariwala/how-i-built-auto-zoom-on-click-screen-recording-inside-a-chrome-extension-gdj</link>
      <guid>https://dev.to/sahilnasariwala/how-i-built-auto-zoom-on-click-screen-recording-inside-a-chrome-extension-gdj</guid>
      <description>&lt;p&gt;Screen recordings of a web app are hard to watch. The interesting part, the button you clicked, is a few dozen pixels on a 1440p frame. Tools like Screen Studio fix this by zooming in on every click, but they're desktop apps. I wanted that inside Chrome, with nothing uploaded anywhere, so I built &lt;a href="https://bimb.dev" rel="noopener noreferrer"&gt;Bimb&lt;/a&gt;, a Chrome extension that records a tab and turns every click into a smooth zoom.&lt;/p&gt;

&lt;p&gt;Here's how the pieces fit together, including the parts that took the longest to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Recording in Manifest V3
&lt;/h2&gt;

&lt;p&gt;An MV3 extension has a service worker, not a background page, and a service worker can't run &lt;code&gt;MediaRecorder&lt;/code&gt;. So recording happens in an &lt;strong&gt;offscreen document&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tab recording:&lt;/strong&gt; the service worker gets a stream id from &lt;code&gt;chrome.tabCapture&lt;/code&gt;, and the offscreen document turns it into a &lt;code&gt;MediaStream&lt;/code&gt; and records it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window or screen recording:&lt;/strong&gt; I first tried &lt;code&gt;chrome.desktopCapture&lt;/code&gt;, but its stream ids are tied to the frame that asked for them, so the offscreen document couldn't use one requested elsewhere. The offscreen document now calls &lt;code&gt;getDisplayMedia()&lt;/code&gt; itself and shows Chrome's own picker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recordings go into IndexedDB (with the &lt;code&gt;unlimitedStorage&lt;/code&gt; permission, so Chrome doesn't evict a long video). Nothing leaves the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Knowing where the clicks were
&lt;/h2&gt;

&lt;p&gt;Video alone doesn't tell you where someone clicked. During a tab recording, a content script runs in every frame it can reach and reports pointer events in that frame's viewport coordinates.&lt;/p&gt;

&lt;p&gt;Iframes make this harder than it sounds. A click inside an embedded iframe has to be mapped to top-page coordinates, which means knowing which &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; element that frame lives in. Chrome has no &lt;code&gt;runtime.getFrameId()&lt;/code&gt; (that's Firefox-only), so I use a small handshake:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The child frame registers a random token with the service worker.&lt;/li&gt;
&lt;li&gt;It posts only that token to its parent window.&lt;/li&gt;
&lt;li&gt;The parent's script matches the message source to an iframe element and tells the service worker it owns that token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page can see the token but never any coordinates. Frames can also hide inside &lt;strong&gt;closed&lt;/strong&gt; shadow roots, which &lt;code&gt;chrome.dom.openOrClosedShadowRoot()&lt;/code&gt; can reach.&lt;/p&gt;

&lt;p&gt;Some pages are off-limits: Chrome doesn't let extensions run scripts on &lt;code&gt;chrome://&lt;/code&gt; pages, the Web Store or other extensions. On those the video still records, but there are no clicks to zoom on.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Turning clicks into zooms
&lt;/h2&gt;

&lt;p&gt;Each click becomes a zoom segment, with two rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clicks close in time and space share one segment&lt;/strong&gt;, and the camera pans between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Far-apart clicks get their own zoom.&lt;/strong&gt; A long pan across the page while zoomed in is disorienting, so it's better to zoom out and back in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The zoom finishes arriving just &lt;em&gt;before&lt;/em&gt; the click and holds a moment after it, so the viewer sees the button before it's pressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A camera that's a pure function of time
&lt;/h2&gt;

&lt;p&gt;The camera is a function of time &lt;code&gt;t&lt;/code&gt; alone: given the zoom segments and &lt;code&gt;t&lt;/code&gt;, it returns the visible rectangle. It's built from eased keyframes, then clamped so the view never leaves the frame.&lt;/p&gt;

&lt;p&gt;One detail made a big difference: &lt;strong&gt;interpolate the zoom level in log space.&lt;/strong&gt; Going from 1x to 2x should feel the same as going from 2x to 4x, and plain linear interpolation makes zooming in feel fast at the start and sluggish at the end. Simplified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified: zoom level between two keyframes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;zoomAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;exp&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the camera keeps no state, the editor preview and the exported file are computed by the same code from the same inputs, so they can't drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A cursor that doesn't wobble
&lt;/h2&gt;

&lt;p&gt;Raw pointer samples come in at about 60 Hz and are jittery. Smoothing them with a normal filter adds lag, so the drawn cursor arrives at the button after the click has already happened.&lt;/p&gt;

&lt;p&gt;Instead, I resample the path at 120 Hz and run a critically damped spring over it forwards, then backwards (zero-phase, like &lt;code&gt;filtfilt&lt;/code&gt; in signal processing). The motion is smooth with no lag, and the cursor sits exactly on the click point at click time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Trimming dead time
&lt;/h2&gt;

&lt;p&gt;Most recordings have pauses: you reading, thinking, waiting for a page to load. Bimb finds stretches with no clicks, keys or pointer movement and suggests cutting them. It's plain gap analysis, no ML, and you confirm each cut.&lt;/p&gt;

&lt;p&gt;Cuts are non-destructive. One bug worth sharing: cutting a section introduced audio/video drift, because a cut boundary landed between video frames while the audio was cut at the exact sample. The fix was to snap cut spans to frame boundaries and crop audio chunks at those same boundaries.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Exporting in the browser
&lt;/h2&gt;

&lt;p&gt;Export runs in a worker:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decode the source at a constant frame rate.&lt;/li&gt;
&lt;li&gt;Draw each frame through the same scene code the preview uses: background, camera, cursor, click ripples, captions and blur regions.&lt;/li&gt;
&lt;li&gt;Encode with WebCodecs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://mediabunny.dev" rel="noopener noreferrer"&gt;Mediabunny&lt;/a&gt; handles demuxing and muxing for MP4 (H.264 + AAC/Opus) and WebM (VP9/VP8 + Opus), and &lt;code&gt;gifenc&lt;/code&gt; handles GIFs. A 1440p60 export runs entirely on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building something similar
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Make rendering a pure function of time.&lt;/strong&gt; It turned "the preview doesn't match the export" bugs into ordinary unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect frames and shadow roots everywhere.&lt;/strong&gt; Real apps (docs sites, dashboards, MDN) nest iframes inside shadow roots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the MV3 limits before designing.&lt;/strong&gt; Service workers can't record media, and capture ids are tied to a frame. Both changed my architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bimb is free to record and export with a watermark; &lt;a href="https://bimb.dev" rel="noopener noreferrer"&gt;Bimb Pro&lt;/a&gt; is a one-time license. You can &lt;a href="https://chromewebstore.google.com/detail/bimb/kcnafmncckkfemnmedbkdcjcmlbkidbf" rel="noopener noreferrer"&gt;get it on the Chrome Web Store&lt;/a&gt;. I'd love feedback, especially from anyone who has fought the same MV3 capture APIs.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>chrome</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Built an Event Loop Visualizer Because the Best One Is From 2014</title>
      <dc:creator>Sahil Nasariwala</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:22:04 +0000</pubDate>
      <link>https://dev.to/sahilnasariwala/i-built-an-event-loop-visualizer-because-the-best-one-is-from-2014-1k95</link>
      <guid>https://dev.to/sahilnasariwala/i-built-an-event-loop-visualizer-because-the-best-one-is-from-2014-1k95</guid>
      <description>&lt;p&gt;Every JavaScript developer has seen this:&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;timeout&lt;/span&gt;&lt;span class="dl"&gt;'&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;promise&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// start → end → promise → timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most of us know the answer.&lt;/p&gt;

&lt;p&gt;We know that microtasks run before the next task. We know &lt;code&gt;setTimeout(..., 0)&lt;/code&gt; doesn't actually mean "run this immediately." We know promises and the event loop are frequent interview topics.&lt;/p&gt;

&lt;p&gt;But then someone gives you a slightly uglier example and asks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Okay, what happens next?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And suddenly you're drawing boxes on a whiteboard.&lt;/p&gt;

&lt;p&gt;That's what made me build &lt;strong&gt;&lt;a href="https://jseventloop.tech/" rel="noopener noreferrer"&gt;The Event Loop, Visualized&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's an interactive course where you can actually watch the call stack, task queue, microtask queue, and Web APIs change step by step. You can slow it down, move backward, scrub through the timeline, and eventually paste in your own JavaScript and watch it get visualized.&lt;/p&gt;

&lt;h2&gt;
  
  
  But don't we already have event loop visualizers?
&lt;/h2&gt;

&lt;p&gt;Yes.&lt;/p&gt;

&lt;p&gt;That's actually where this project started.&lt;/p&gt;

&lt;p&gt;Before writing any code, I spent some time looking at what already existed.&lt;/p&gt;

&lt;p&gt;The first one everyone knows is &lt;strong&gt;Loupe&lt;/strong&gt;, from 2014. It's still great, and Philip Roberts' "What the heck is the event loop anyway?" talk is one of the resources that made the event loop click for an entire generation of developers.&lt;/p&gt;

&lt;p&gt;But JavaScript has changed quite a bit since then.&lt;/p&gt;

&lt;p&gt;Promises weren't part of the picture in the same way. &lt;code&gt;async/await&lt;/code&gt; didn't exist. Microtasks weren't something most developers were thinking about.&lt;/p&gt;

&lt;p&gt;There are newer tools too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSV9000&lt;/strong&gt; does a good job of showing microtasks and letting you step through execution, but it assumes you already understand what you're looking at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSFlow&lt;/strong&gt; supports promises and &lt;code&gt;async/await&lt;/code&gt;, but feels more like a playground than a course.&lt;/p&gt;

&lt;p&gt;And then there's Lydia Hallie's &lt;strong&gt;JavaScript Visualized&lt;/strong&gt; series, which is probably the best example of how much people enjoy learning JavaScript through animation. But they're still essentially videos/GIFs—you can't stop at a specific moment and ask, "Wait, why did this move here?"&lt;/p&gt;

&lt;p&gt;I kept noticing the same thing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tools were either good at teaching or good at experimenting. Not both.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I decided to try combining the two.&lt;/p&gt;

&lt;p&gt;Not another diagram.&lt;/p&gt;

&lt;p&gt;Not another article explaining the event loop.&lt;/p&gt;

&lt;p&gt;Something you could actually play with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first thing I wanted to get right: different explanations, same animation
&lt;/h2&gt;

&lt;p&gt;One thing I didn't want to do was create a "beginner version" and an "advanced version" as completely separate experiences.&lt;/p&gt;

&lt;p&gt;Instead, every scene has three levels of explanation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explain simply
&lt;/h3&gt;

&lt;p&gt;Think chef, kitchen, and VIP line.&lt;/p&gt;

&lt;p&gt;It's deliberately visual and avoids throwing terms like "microtask checkpoint" at someone who has never heard them before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer
&lt;/h3&gt;

&lt;p&gt;Now we talk about the actual things developers work with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call stack&lt;/li&gt;
&lt;li&gt;Web APIs&lt;/li&gt;
&lt;li&gt;Task queue&lt;/li&gt;
&lt;li&gt;Microtask queue&lt;/li&gt;
&lt;li&gt;Promises&lt;/li&gt;
&lt;li&gt;Timers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Under the hood
&lt;/h3&gt;

&lt;p&gt;This is where the browser's actual behavior comes in: task sources, microtask checkpoints, rendering opportunities, and the HTML specification.&lt;/p&gt;

&lt;p&gt;The interesting part is that &lt;strong&gt;the animation doesn't change&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Only the explanation changes.&lt;/p&gt;

&lt;p&gt;That means a beginner and an experienced JavaScript developer can watch the exact same animation and just choose how deep they want to go.&lt;/p&gt;

&lt;p&gt;It also turned out to be a useful constraint while building the content. I couldn't accidentally create three slightly different versions of the same lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture decision I liked the most
&lt;/h2&gt;

&lt;p&gt;This is probably the most interesting part of the project from an engineering perspective.&lt;/p&gt;

&lt;p&gt;My first thought was to instrument the browser.&lt;/p&gt;

&lt;p&gt;Something like:&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;const&lt;/span&gt; &lt;span class="nx"&gt;originalSetTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTimeout&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// record what happened&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;originalSetTimeout&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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;Intercept timers.&lt;/p&gt;

&lt;p&gt;Watch promise callbacks.&lt;/p&gt;

&lt;p&gt;Track the call stack.&lt;/p&gt;

&lt;p&gt;Emit events as the browser actually executes them.&lt;/p&gt;

&lt;p&gt;It sounds reasonable.&lt;/p&gt;

&lt;p&gt;It's also a pain if you want an interactive visualizer.&lt;/p&gt;

&lt;p&gt;Because now you're trying to control the actual event loop.&lt;/p&gt;

&lt;p&gt;You can't simply say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pause the browser right here."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Actually, go back three steps."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Let's replay this at 0.5× speed."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser isn't designed around those controls.&lt;/p&gt;

&lt;p&gt;So I stopped trying to record the real event loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I decided to record what the event loop should do instead.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A scene is just a script
&lt;/h2&gt;

&lt;p&gt;Instead of treating a scene as executable JavaScript, I represent it as a sequence of small actions.&lt;/p&gt;

&lt;p&gt;Something roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1
→ highlight line 1
→ push "console.log" onto stack
→ print "start"

Step 2
→ register setTimeout
→ move callback to Web APIs

Step 3
→ create promise callback
→ add it to microtask queue

...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visualizer doesn't care how the scene was created.&lt;/p&gt;

&lt;p&gt;It just receives the steps and renders them.&lt;/p&gt;

&lt;p&gt;The runtime state at any point can then be calculated from those steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;computeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;stepIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;RuntimeState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emptyState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;stepIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;steps&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="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And suddenly a lot of features became almost trivial.&lt;/p&gt;

&lt;p&gt;Want to go backward?&lt;/p&gt;

&lt;p&gt;Recalculate the state at the previous step.&lt;/p&gt;

&lt;p&gt;Want to jump to step 7?&lt;/p&gt;

&lt;p&gt;Calculate step 7.&lt;/p&gt;

&lt;p&gt;Want to scrub from step 2 to step 9?&lt;/p&gt;

&lt;p&gt;Calculate step 9 and animate the difference.&lt;/p&gt;

&lt;p&gt;No undo stack.&lt;/p&gt;

&lt;p&gt;No complicated event-sourcing system.&lt;/p&gt;

&lt;p&gt;No snapshots.&lt;/p&gt;

&lt;p&gt;Most scenes only have around ten or so steps, so recalculating the state is basically free.&lt;/p&gt;

&lt;p&gt;That one architectural decision gave me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Play&lt;/li&gt;
&lt;li&gt;Pause&lt;/li&gt;
&lt;li&gt;Step forward&lt;/li&gt;
&lt;li&gt;Step backward&lt;/li&gt;
&lt;li&gt;Scrubbing&lt;/li&gt;
&lt;li&gt;Speed control&lt;/li&gt;
&lt;li&gt;Deep links to specific steps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Then came the harder question: what about my own code?
&lt;/h2&gt;

&lt;p&gt;Hand-written scenes work really well for lessons.&lt;/p&gt;

&lt;p&gt;But eventually the obvious question appears:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Cool. But can I paste my code into it?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the whole point.&lt;/p&gt;

&lt;p&gt;So the playground uses &lt;strong&gt;Acorn&lt;/strong&gt; to parse the JavaScript into an AST and then compiles the supported code into the exact same step format used by the hand-written lessons.&lt;/p&gt;

&lt;p&gt;The visualizer doesn't know—or care—whether a scene came from a lesson or from the playground.&lt;/p&gt;

&lt;p&gt;It's just a sequence of steps.&lt;/p&gt;

&lt;p&gt;For now, the playground supports a deliberately limited subset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;console.log&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setTimeout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Promise chains&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;queueMicrotask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async/await&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Function calls&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also an 80-step limit.&lt;/p&gt;

&lt;p&gt;I don't want someone pasting an accidental infinite loop into an educational animation and making the browser regret its life choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  I also made a mistake here
&lt;/h3&gt;

&lt;p&gt;I didn't build the compiler first.&lt;/p&gt;

&lt;p&gt;I built the flagship scenes manually.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;promise-vs-setTimeout&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Microtask starvation&lt;/p&gt;

&lt;p&gt;&lt;code&gt;async/await, unmasked&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After building enough of them, I started noticing the patterns.&lt;/p&gt;

&lt;p&gt;Only then did I build the compiler.&lt;/p&gt;

&lt;p&gt;That turned out to be a much better approach.&lt;/p&gt;

&lt;p&gt;If I'd started by designing the compiler, I would have designed an abstraction based on what I &lt;em&gt;thought&lt;/em&gt; a scene needed.&lt;/p&gt;

&lt;p&gt;After building the scenes, I knew what it actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some small things ended up making a big difference
&lt;/h2&gt;

&lt;p&gt;The event loop is the main feature, but a few other decisions made the project feel much more complete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prediction mode
&lt;/h3&gt;

&lt;p&gt;Before showing the answer, the site asks you to predict the output.&lt;/p&gt;

&lt;p&gt;There's something useful about committing to an answer first.&lt;/p&gt;

&lt;p&gt;If you're wrong, you don't just see the correct output—you see exactly where your mental model diverged.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Break the loop"
&lt;/h3&gt;

&lt;p&gt;There's a small sandbox where you can intentionally block the main thread.&lt;/p&gt;

&lt;p&gt;You see an animation running normally.&lt;/p&gt;

&lt;p&gt;Then the blocking loop starts.&lt;/p&gt;

&lt;p&gt;And the animation freezes.&lt;/p&gt;

&lt;p&gt;It's much more memorable than reading:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Long-running JavaScript can block rendering."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You actually watch the UI stop responding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare snippets
&lt;/h3&gt;

&lt;p&gt;You can run two to four snippets against each other and watch their behavior side by side.&lt;/p&gt;

&lt;p&gt;The benchmarks run inside a Web Worker so the comparison doesn't block the very event loop you're trying to understand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Themes
&lt;/h3&gt;

&lt;p&gt;There are five themes inspired by tools like Monkeytype.&lt;/p&gt;

&lt;p&gt;The theme system uses CSS variables, and an inline pre-paint script prevents the page from flashing the wrong theme during startup.&lt;/p&gt;

&lt;p&gt;It's a tiny detail.&lt;/p&gt;

&lt;p&gt;But once you notice a flash like that, you can't unsee it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep links
&lt;/h3&gt;

&lt;p&gt;You can link directly to a specific point in an animation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/play/promise-vs-settimeout?step=5&amp;amp;level=simply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So instead of telling someone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Go to the promise vs setTimeout example and move to step 5."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can just send them the exact moment.&lt;/p&gt;

&lt;p&gt;I also added keyboard controls and reduced-motion support along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like to build next
&lt;/h2&gt;

&lt;p&gt;The obvious next step is &lt;strong&gt;Node.js's event loop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Browser JavaScript and Node.js share the same language, but their event loop behavior has important differences.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Phases&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;process.nextTick&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setImmediate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Timers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I want to put the browser and Node models next to each other and let people experiment with both.&lt;/p&gt;

&lt;p&gt;And because the visualizer is based on step scripts rather than browser instrumentation, the underlying architecture doesn't really care which event loop we're describing.&lt;/p&gt;

&lt;p&gt;That's probably my favorite thing about the project.&lt;/p&gt;

&lt;p&gt;What started as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I wish there was a better event loop visualizer."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;turned into a small experiment in building interactive technical education.&lt;/p&gt;

&lt;p&gt;If you want to try it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://jseventloop.tech/" rel="noopener noreferrer"&gt;The Event Loop, Visualized&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you've ever written:&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="o"&gt;=&amp;gt;&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as a fix and then quietly hoped nobody would ask you &lt;em&gt;why&lt;/em&gt; it worked...&lt;/p&gt;

&lt;p&gt;You might enjoy this project.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>learning</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
