<?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: Mehul Koshti</title>
    <description>The latest articles on DEV Community by Mehul Koshti (@mehul_koshti_ed13988bf3fc).</description>
    <link>https://dev.to/mehul_koshti_ed13988bf3fc</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%2F4173991%2F4e65958d-0582-44a0-a115-842aed57b211.png</url>
      <title>DEV Community: Mehul Koshti</title>
      <link>https://dev.to/mehul_koshti_ed13988bf3fc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mehul_koshti_ed13988bf3fc"/>
    <language>en</language>
    <item>
      <title>Why Your Browser-Based Video Export Freezes When You Switch Tabs (and How to Fix It)</title>
      <dc:creator>Mehul Koshti</dc:creator>
      <pubDate>Fri, 09 Oct 2026 18:04:00 +0000</pubDate>
      <link>https://dev.to/mehul_koshti_ed13988bf3fc/why-your-browser-based-video-exportfreezes-when-you-switch-tabs-and-howto-fix-it-2aoc</link>
      <guid>https://dev.to/mehul_koshti_ed13988bf3fc/why-your-browser-based-video-exportfreezes-when-you-switch-tabs-and-howto-fix-it-2aoc</guid>
      <description>&lt;p&gt;If you're building anything that records a  with MediaRecorder — a video editor, a&lt;br&gt;
whiteboard exporter, a game clip recorder — there's a bug you will eventually ship without&lt;br&gt;
realizing it. It doesn't show up in testing, because you're staring at the tab while you test. It&lt;br&gt;
only shows up for real users, because real users tab away while they wait.&lt;br&gt;
Here's what happened, how I found it, and how I fixed it.&lt;br&gt;
The bug report&lt;br&gt;
A tester using a browser-based video editor I work on (multi-track timeline, text/shape&lt;br&gt;
overlays, export to WebM) reported something strange:&lt;br&gt;
I created a video with 5+ slides. After exporting, the file only shows one slide&lt;br&gt;
instead of all of them. Reproducible in VLC, Windows Media Player, and Films &amp;amp;&lt;br&gt;
TV.&lt;br&gt;
"Reproducible across every player" was the key clue. If it were a codec or container problem,&lt;br&gt;
different players might handle it differently. Here, every player agreed: the file genuinely only&lt;br&gt;
contained one frame's worth of content, repeated for the full duration.&lt;br&gt;
How the export actually worked&lt;br&gt;
Like most browser-based editors without a server-side rendering pipeline, export worked by&lt;br&gt;
literally playing the project in real time and recording the canvas as it played:&lt;br&gt;
const videoStream = canvas.captureStream(fps) // auto-samples the canvas at &lt;code&gt;fps&lt;/code&gt;&lt;br&gt;
const recorder = new MediaRecorder(combinedStream, { mimeType: 'video/webm;codecs=v&lt;br&gt;
recorder.start(100)&lt;br&gt;
setPlayhead(0)&lt;br&gt;
setPlaying(true) // kicks off the normal requestAnimationFrame preview loop&lt;br&gt;
The preview loop is a standard rAF ticker:&lt;br&gt;
function tick() {&lt;br&gt;
const now = performance.now()&lt;br&gt;
const delta = (now - lastRef.current) / 1000&lt;br&gt;
lastRef.current = now&lt;br&gt;
setPlayhead(prev =&amp;gt; {&lt;br&gt;
const next = prev + delta&lt;br&gt;
renderFrame(next) // draws clips/text/shapes onto the canvas&lt;br&gt;
return next&lt;br&gt;
})&lt;br&gt;
requestAnimationFrame(tick)&lt;br&gt;
}&lt;br&gt;
This works perfectly in every manual test, because manual testing means staring at the&lt;br&gt;
export progress bar.&lt;br&gt;
The actual cause&lt;br&gt;
A 20-slide, 40-second export takes roughly 40 real seconds to record, because it's literally&lt;br&gt;
real-time playback. Nobody stares at a progress bar for 40 seconds — they switch tabs,&lt;br&gt;
check Slack, glance at something else, and switch back when it's done.&lt;br&gt;
requestAnimationFrame is throttled or fully suspended by the browser the moment a tab is&lt;br&gt;
backgrounded. It's a deliberate, documented browser optimization for battery/CPU — not a&lt;br&gt;
bug in the browser.&lt;br&gt;
So the sequence was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User clicks Export.&lt;/li&gt;
&lt;li&gt;Recording starts, playback starts via the rAF loop.&lt;/li&gt;
&lt;li&gt;User switches tabs after ~3 seconds (while slide 1 is on screen).&lt;/li&gt;
&lt;li&gt;The tab backgrounds → requestAnimationFrame stops firing → the canvas stops
being redrawn → it stays frozen on slide 1.&lt;/li&gt;
&lt;li&gt;canvas.captureStream(fps) doesn't know or care that the canvas stopped
changing. It keeps sampling at the configured fps regardless — it just samples the
same frozen pixels, over and over, for the rest of the export.&lt;/li&gt;
&lt;li&gt;MediaRecorder faithfully records exactly what it's given: 40 seconds of slide 1.
The exported file's duration was correct. Its content was a single frame, repeated. That's why
every player agreed on what was wrong — there was nothing ambiguous to disagree about.
The fix: stop relying on rAF for export
The preview loop can stay rAF-based — that's genuinely the right tool when a human is
watching and the tab is guaranteed foregrounded. Export needs its own loop, driven by
something that isn't gated on tab visibility the same way.
setInterval is still throttled in background tabs (browsers clamp it, typically to once per
second after a few minutes hidden), but critically, it keeps firing — it doesn't fully suspend
the way rAF does. And since each tick computes elapsed time from a real wall-clock delta
rather than assuming a fixed frame interval, a throttled tick still renders the correct frame for
that moment — it just produces fewer distinct frames during the backgrounded stretch
instead of freezing entirely.
Combined with manual frame capture ( captureStream(0) + track.requestFrame() instead
of letting the browser auto-sample), the export loop became fully self-contained:
const videoTrack = canvas.captureStream(0).getVideoTracks()[0] // 0 = manual mode
let last = performance.now()
let time = 0
const stepExport = () =&amp;gt; {
const now = performance.now()
time = Math.min(totalDuration, time + (now - last) / 1000)
last = now
renderFrame(time) // draw the correct frame for &lt;code&gt;time&lt;/code&gt;
videoTrack.requestFrame() // push exactly this frame into the recording
if (time &amp;gt;= totalDuration) {
clearInterval(intervalId)
recorder.stop()
}
}
const intervalId = setInterval(stepExport, 1000 / fps)
No more dependency on the preview's rAF loop, no more silent freeze when a tab loses
focus.
Verifying it, not just believing it
Code review alone doesn't prove a timing bug is fixed — you have to reproduce the actual
failure mode. I built a small 4-slide test project (distinct solid colors, 5 seconds each),
triggered export, and then deliberately backgrounded the tab for the entire 20-second export
by opening and focusing a second blank tab — the exact thing the original tester must have
done.
Then I pulled frames from the output at several timestamps and sampled pixel color:
Time Color shown Expected
1–2s Green Green (0–5s)
7s Blue Blue (5–10s)
12s Yellow Yellow (10–15s)
17–19s Purple Purple (15–20s)
All four slides, in the correct sequence, despite the tab being backgrounded the whole time.
That's the actual test that matters — not "does it compile," but "does it survive the exact realworld behavior that broke it."
The takeaway
If your app does anything time-based while backgrounded — canvas recording, animations
syncing to real time, polling that assumes regular intervals — don't assume
requestAnimationFrame is a safe default just because it's the "correct" tool for rendering. It's
the correct tool for rendering to a user who's looking at the screen. The moment you need
something to keep working while nobody's looking, you need a loop that doesn't know or
care whether the tab is visible.
I found and fixed this while working on &lt;a href="https://codaiman.com/toolhub/video-editor-pro" rel="noopener noreferrer"&gt;ToolHub's Video Editor Pro&lt;/a&gt; — a free, browser-based
video editor with multi-track timeline editing, transitions, keyframe animation and WebM
export, no signup required&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>powerapps</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
