<?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: Keny Alba</title>
    <description>The latest articles on DEV Community by Keny Alba (@kenyalba).</description>
    <link>https://dev.to/kenyalba</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%2F4094286%2F9e075295-9e67-4c34-a5e7-d99f3a9e3613.png</url>
      <title>DEV Community: Keny Alba</title>
      <link>https://dev.to/kenyalba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenyalba"/>
    <language>en</language>
    <item>
      <title>Auto-scroll is not `window.scrollTo`</title>
      <dc:creator>Keny Alba</dc:creator>
      <pubDate>Wed, 02 Sep 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/kenyalba/auto-scroll-is-not-windowscrollto-1fe3</link>
      <guid>https://dev.to/kenyalba/auto-scroll-is-not-windowscrollto-1fe3</guid>
      <description>&lt;h2&gt;
  
  
  The document you measure is not always the document you see
&lt;/h2&gt;

&lt;p&gt;This is the last post in a five-part series about recording a page while it moves. Capture models and clocks do not matter if the page does not move the way you think it does.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;window.scrollTo(0, y)&lt;/code&gt; is the demo. Production sites are scroll-snap, Lenis-class smooth engines, overflow-owned layouts, and WebGL scenes that never exposed &lt;code&gt;scrollHeight&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;document.documentElement.scrollHeight&lt;/code&gt; is a good default. It is not a contract.&lt;/p&gt;

&lt;p&gt;Marketing pages put the real scroll on an inner &lt;code&gt;div&lt;/code&gt; with &lt;code&gt;overflow: auto&lt;/code&gt;. Animation libraries put a hidden scroller in the tree and map it onto transforms. Canvas scenes consume wheel events and draw a camera. The window’s &lt;code&gt;scrollY&lt;/code&gt; barely changes. Auto-stop based on “near &lt;code&gt;scrollHeight - innerHeight&lt;/code&gt;” fires immediately, or never.&lt;/p&gt;

&lt;p&gt;I detect overflow boxes and drive their &lt;code&gt;scrollTop&lt;/code&gt; as well as the window. That over-fires on pages that have a small scrollable code sample. A short article with a modal or a widget can hijack the “page” height. The safer filter is: only trust a container that covers most of the viewport. I still miss some owners and still grab some widgets. It is a heuristic.&lt;/p&gt;

&lt;p&gt;Wheel events have to look like a user. Libraries check hover before they honor &lt;code&gt;wheel&lt;/code&gt;. Dispatching only on &lt;code&gt;document&lt;/code&gt; is not enough. I target the element under the pointer (or the simulated pointer), bubble a &lt;code&gt;wheel&lt;/code&gt;, and also hit the first canvas if the pointer is on an overlay. Isolated-world &lt;code&gt;scrollTo&lt;/code&gt; never entered that canvas’s event path.&lt;/p&gt;

&lt;p&gt;Smooth-scroll engines keep their own target in a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop. After I jump to the top, they interpolate back to where they were. The first seconds of a capture show the page sliding away from y = 0. The counter is a freeze: keep forcing top until preparation delay ends, then release. At the bottom, stop sending deltas. Footer widgets and WebGL listeners bounce if you keep ticking. Syncing the engine’s internal offset to the real &lt;code&gt;scrollY&lt;/code&gt; is a last poke, not a second scroller.&lt;/p&gt;

&lt;h2&gt;
  
  
  Snap is a feature until it is a fight
&lt;/h2&gt;

&lt;p&gt;CSS scroll-snap will fight a linear &lt;code&gt;scrollBy&lt;/code&gt; every frame. The page looks possessed: you ask for two pixels, it jumps a section, you ask again, it jumps back.&lt;/p&gt;

&lt;p&gt;For ordinary auto-scroll I disable &lt;code&gt;scroll-snap-type&lt;/code&gt; on the tree. The recording is a continuous move. That is the point.&lt;/p&gt;

&lt;p&gt;For section-based sites, that disable is the bug. Those pages &lt;strong&gt;are&lt;/strong&gt; the snap. I use a different driver: one synthetic wheel impulse, then silence, then another. Snap stays on. If the wheel does nothing, a deferred viewport-sized &lt;code&gt;scrollBy&lt;/code&gt; runs on a later frame - never in the same frame as the wheel, or you double-apply. If several impulses produce no DOM progress, I stop. A capped impulse count exists so a broken page cannot run forever.&lt;/p&gt;

&lt;p&gt;That second driver is not “smarter linear scroll.” It is a different machine: wait → impulse → wait. Mixing it into the rAF pixel loop is how you get both snap fighting and random jumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Height is a liar in both directions
&lt;/h2&gt;

&lt;p&gt;Lazy load grows &lt;code&gt;scrollHeight&lt;/code&gt; after you “finished.” If stop is already armed, I do not cancel it - pages that keep appending forever never end. If we are still in the settle window, I unlock and continue. A small threshold ignores animation jitter that is not real growth.&lt;/p&gt;

&lt;p&gt;Banners shrink the document. You can lock onto a ghost bottom, then sit there while the real footer is still off-screen. Unlock if we are no longer near the new bottom.&lt;/p&gt;

&lt;p&gt;“Near the bottom” as a percentage fails on long pages. A few pixels of footer is a fraction of a percent and still missing. I use a tight pixel tolerance, then a stagnation filet: if we asked to move and &lt;code&gt;scrollY&lt;/code&gt; did not, the browser may have hit a wall because height was overestimated (sticky chrome). One snap to max, then believe the wall.&lt;/p&gt;

&lt;p&gt;Smart pacing - pause on headings, wait for in-view images to decode - is extra policy on top of this, not a replacement. It still needs a real document height. On a canvas-only scene it has nothing to attach to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blind pages: do not auto-stop
&lt;/h2&gt;

&lt;p&gt;If the measurable scroll range is tiny compared to the viewport, I treat the page as blind. Constant speed, wheel events for the WebGL path, &lt;strong&gt;no&lt;/strong&gt; automatic end.&lt;/p&gt;

&lt;p&gt;The user hits Stop. That is not a missing feature. Auto-stop would cut a three-minute WebGL journey at second zero because &lt;code&gt;maxScroll&lt;/code&gt; was 40 pixels.&lt;/p&gt;

&lt;p&gt;Impulse mode uses the same blindness test: no “we reached the bottom,” because there is no bottom to trust. It still stops on no-progress or a max impulse count so a dead page cannot spin.&lt;/p&gt;

&lt;p&gt;Cinema cannot save this. A map needs a height. A 40-pixel document produces a poster of the first screen, then a camera with nowhere to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I will not pretend to drive
&lt;/h2&gt;

&lt;p&gt;Cross-origin iframes are other documents. I do not inject into every frame. If the product lives in an embed, outer-page auto-scroll is theatre.&lt;/p&gt;

&lt;p&gt;Horizontal overflow is a different axis. The driver is vertical.&lt;/p&gt;

&lt;p&gt;A hidden tab can stop painting. Capture goes black. Scroll may still be “working” in a world you cannot see.&lt;/p&gt;

&lt;p&gt;Overlay cleanup that hides &lt;code&gt;fixed&lt;/code&gt; nodes must spare canvases, main landmarks, and near-fullscreen layers. Otherwise you “clean” the WebGL scene you came to record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The series in one line
&lt;/h2&gt;

&lt;p&gt;A screenshot is a scan. Live video is a compositor diary. Cinema is a camera on a poster. Auto-scroll is a negotiation with whoever actually owns the scroll.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt; as that negotiation on the user’s machine. It still loses to iframes, hidden tabs, sideways pages, and scenes that never exposed a scrollbar. Those losses are the spec, not the backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If a site ignored &lt;code&gt;window.scrollTo&lt;/code&gt;, I want that engine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I turned this experiment into &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt;, a Chrome extension for creating local auto-scroll captures of websites. It is free to try, and I’d genuinely value feedback from developers who work with difficult pages.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>programming</category>
      <category>discuss</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A full-page PNG is a scan of viewports, not a screenshot of the document</title>
      <dc:creator>Keny Alba</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/kenyalba/a-full-page-png-is-a-scan-of-viewports-not-a-screenshot-of-the-document-16ia</link>
      <guid>https://dev.to/kenyalba/a-full-page-png-is-a-scan-of-viewports-not-a-screenshot-of-the-document-16ia</guid>
      <description>&lt;h2&gt;
  
  
  Why a single screenshot cannot exist
&lt;/h2&gt;

&lt;p&gt;This is the fourth post in a series about capturing scrolling pages. Live video and Cinema maps get the attention. The still image is where sticky headers and the last slice quietly ruin the file.&lt;/p&gt;

&lt;p&gt;I used to think “full page” meant one screenshot of a tall document. In a browser it means: scroll, wait, capture the visible tab, repeat, stack the bitmaps. Every step can disagree with the last.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;captureVisibleTab&lt;/code&gt; sees the viewport. There is no API that returns the layout tree as one PNG at device resolution. Long pages are a loop.&lt;/p&gt;

&lt;p&gt;Chrome also rate-limits how often you may call that API. The loop is slow on purpose. Racing it produces missing frames or errors, not a faster poster.&lt;/p&gt;

&lt;p&gt;Between two captures the page is allowed to change: lazy images, sticky classes applied by JavaScript, banners collapsing, fonts swapping. A “full page” PNG is a timeline of viewports glued together. If you do not treat it that way, you debug the glue.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last slice always overlaps - unless you crop
&lt;/h2&gt;

&lt;p&gt;Suppose the page is not an integer number of viewports tall. You scroll by one viewport each time. On the last step, the browser &lt;strong&gt;clamps&lt;/strong&gt; &lt;code&gt;scrollTo&lt;/code&gt;. You cannot place the viewport so that its top sits on “the remaining pixels.” You land on &lt;code&gt;totalHeight - viewportHeight&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The last bitmap therefore contains a band you already captured in the previous frame. Stack the images and the footer duplicates. I have seen copyright lines twice and a button that appears to sit on itself.&lt;/p&gt;

&lt;p&gt;The fix is geometric, not aesthetic. Compute the remainder - the true height of the last segment - and crop the last bitmap to that band, in &lt;strong&gt;captured pixels&lt;/strong&gt;, not CSS pixels. &lt;code&gt;captureVisibleTab&lt;/code&gt; returns a device-pixel buffer. Remainder and viewport height are CSS. The crop uses the ratio &lt;code&gt;bitmapHeight / viewportHeight&lt;/code&gt; or you cut the wrong amount on a retina display.&lt;/p&gt;

&lt;p&gt;If the crop fails, I keep the raw last frame. A duplicated band is worse than a failed export, but a failed export is worse than a slightly messy PNG. The crop is non-blocking.&lt;/p&gt;

&lt;p&gt;Frames cannot sit in the service worker as one giant message. Long pages used to die on memory and IPC size. Each viewport is written into the local encoder’s filesystem as it arrives. Assembly is a vertical stack at the end. The worker never holds the whole poster.&lt;/p&gt;

&lt;p&gt;Cinema maps use this same capture family. A map that is too tall for a single raster (browser canvas limits are on the order of tens of thousands of pixels) must be refused &lt;strong&gt;before&lt;/strong&gt; a multi-minute scan. Failure after the loop is the worst moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sticky chrome is a stamp, not a header
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;position: fixed&lt;/code&gt; bar exists in every viewport. If you stack raw captures, you print that bar at every height you visited. The page looks like it has a header every screen, because it did - on screen.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;position: sticky&lt;/code&gt; is sneakier. At the top of the document it may still be in normal flow. Your first pass “find all fixed nodes” sees nothing. After the user (or your loop) scrolls, a script adds a class and the bar becomes fixed. Frame 0 is clean. Frames 1..N are tattooed.&lt;/p&gt;

&lt;p&gt;The procedure that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At the top, mark nodes that are already fixed or sticky.&lt;/li&gt;
&lt;li&gt;Capture frame 0 with the nav in its legitimate place.&lt;/li&gt;
&lt;li&gt;Hide marked nodes with &lt;code&gt;visibility: hidden&lt;/code&gt;, not &lt;code&gt;display: none&lt;/code&gt;. Sticky elements still occupy flow until they stick. &lt;code&gt;display: none&lt;/code&gt; reflows the layout you are measuring. &lt;code&gt;visibility&lt;/code&gt; keeps geometry and stops the pixels.&lt;/li&gt;
&lt;li&gt;After every later scroll, scan again for nodes that &lt;strong&gt;became&lt;/strong&gt; fixed, mark them, wait a paint, then capture.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hiding starts after the first frame. The first frame is the only one where a top nav is allowed to exist in the poster.&lt;/p&gt;

&lt;p&gt;Scrollbars must be hidden for the whole scan. A scrollbar that appears and disappears changes the content width by a few pixels. &lt;code&gt;vstack&lt;/code&gt; then misaligns columns. That is independent of any “clean UI” feature.&lt;/p&gt;

&lt;p&gt;CSS transitions and scroll-triggered animation are frozen for the scan. A navbar mid-fade on a random frame is not a feature. Live video wants those animations. A poster does not. Different product, different freeze.&lt;/p&gt;

&lt;p&gt;Lazy images in the current viewport get a short wait, then we continue. An unbounded wait is a hung capture. Black bands in the poster are the failure mode of not waiting at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup has to run even when capture fails
&lt;/h2&gt;

&lt;p&gt;Injected styles, hidden sticky flags, and a forced scroll position must come off in &lt;code&gt;finally&lt;/code&gt;. If the loop throws on frame 12, leaving &lt;code&gt;cursor: none&lt;/code&gt; and frozen animations on the user’s tab is a bug they will remember longer than a missing PNG.&lt;/p&gt;

&lt;p&gt;Window resize is not part of the historical PNG path. It &lt;strong&gt;is&lt;/strong&gt; part of a Cinema map: the page must already be at output width so the bitmap aspect matches the video. A tall phone layout reflows; the map gets longer. That is why the height check exists before the loop, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a PNG is for
&lt;/h2&gt;

&lt;p&gt;A stitched PNG is the right artifact when someone needs a layout audit, a mockup, or a still for a ticket. It is the wrong artifact when they needed motion, hover, or a playing video. Cinema is “this PNG, then a camera.” Live is “never this PNG.”&lt;/p&gt;

&lt;p&gt;The header ghost and the doubled footer are not random quality issues. They are the scan model asserting itself. Once I named the last-slice clamp and the delayed &lt;code&gt;fixed&lt;/code&gt; class, the glue became boring - which is what you want from glue.&lt;/p&gt;

&lt;p&gt;The still export in &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt; is that scan, cropped and destamped as far as the page will allow. Some overlays still win. The geometry does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If a stitched screenshot reprinted a header on you, I want that page.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I turned this experiment into &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt;, a Chrome extension for creating local auto-scroll captures of websites. It is free to try, and I’d genuinely value feedback from developers who work with difficult pages.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>discuss</category>
      <category>performance</category>
    </item>
    <item>
      <title>The page and the encoder are different clocks</title>
      <dc:creator>Keny Alba</dc:creator>
      <pubDate>Sat, 29 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/kenyalba/-the-page-and-the-encoder-are-different-clocks-ge2</link>
      <guid>https://dev.to/kenyalba/-the-page-and-the-encoder-are-different-clocks-ge2</guid>
      <description>&lt;h2&gt;
  
  
  Three times, one file
&lt;/h2&gt;

&lt;p&gt;This is the third post in a series about auto-scroll capture. After choosing live versus a frozen map, the live path still has a quieter bug: my scroll loop, the compositor, and the video encoder do not share a clock.&lt;/p&gt;

&lt;p&gt;I spent too long trying to force them into one number. The recordings got worse.&lt;/p&gt;

&lt;p&gt;Auto-scroll in the page is a &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop. Each tick I compute a delta from real elapsed time, move the document, and hope the next paint includes that position.&lt;/p&gt;

&lt;p&gt;The compositor produces frames on its own cadence. A 144 Hz display does not paint 144 unique page states into a capture. Tab capture samples what is on screen when it can.&lt;/p&gt;

&lt;p&gt;The encoder then writes a compressed stream. For live WebM that stream is typically &lt;strong&gt;variable frame rate&lt;/strong&gt;: one encoded frame per captured paint, not a metronome. Many players treat VFR as stutter. The file is still a more accurate diary of capture than a resampled clip that invented timestamps.&lt;/p&gt;

&lt;p&gt;If I pretend these are one clock, I optimize the wrong layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I stopped smoothing the scroll delta
&lt;/h2&gt;

&lt;p&gt;The speed presets are defined in “pixels per tick” at a notional 60 Hz. Real ticks are not 60 Hz. Under load - high resolution plus a live encoder - a frame callback can arrive late.&lt;/p&gt;

&lt;p&gt;If I scale the scroll step by a large catch-up factor, the page &lt;strong&gt;teleports&lt;/strong&gt; in a single paint. The encoder, if it captures that paint, stores a jump. The screen may have felt only a hitch. The file looks like a cut.&lt;/p&gt;

&lt;p&gt;If I clamp catch-up too hard, the recording lags the intent of the speed preset. I chose a modest cap: better a slightly slower capture than a hundred-pixel jump in one frame.&lt;/p&gt;

&lt;p&gt;I also tried an exponential moving average on &lt;code&gt;dt&lt;/code&gt;. That made displacement even &lt;strong&gt;per painted frame&lt;/strong&gt; and uneven &lt;strong&gt;in real time&lt;/strong&gt;. Capture samples time, not frame index. A long frame that moves twice as far and stays on screen twice as long is the correct live motion. A smoothed &lt;code&gt;dt&lt;/code&gt; made the page fall behind and then sprint. I removed the smoother.&lt;/p&gt;

&lt;p&gt;Sub-pixel remainder matters here. Rounding every tiny step to an integer pixel creates a staircase that the encoder dutifully records. I carry the fraction and apply whole pixels when I have them. One &lt;code&gt;scrollBy&lt;/code&gt; per paint. Never a catch-up burst of several steps in the same callback - that burst is one filmed jump.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I do not constrain capture frame rate
&lt;/h2&gt;

&lt;p&gt;It is tempting to tell &lt;code&gt;getUserMedia&lt;/code&gt; a maximum frame rate so the file matches a UI setting.&lt;/p&gt;

&lt;p&gt;I tried it. On a high-refresh display, a forced period does not land on compositor beats. Chrome waits for the deadline, then takes the next paint. You skip two paints, then three, then two. Scroll advances in uneven steps for the entire take. That is the “page teleporting while I watch a smooth screen” failure, except now it is structural, not a load spike.&lt;/p&gt;

&lt;p&gt;Without a frame-rate cap, capture follows content. Gaps between encoded frames match the compositor more closely. The UI can still offer a cadence for &lt;strong&gt;offline&lt;/strong&gt; render and for transcode. Live capture does not get to fake that cadence at the sensor.&lt;/p&gt;

&lt;p&gt;Software VP8 in the live recorder is CPU-bound. When pixel throughput is too high, the encoder drops input frames irregularly. The compositor is fine. The file is not. The fix that worked was reducing &lt;strong&gt;definition&lt;/strong&gt;, not demanding a higher frame rate from a pipe that cannot carry it. A slightly softer image is easier to forgive than a slideshow labeled as 60 fps.&lt;/p&gt;

&lt;p&gt;Bitrate has to follow real width × height × actual capture rate. Indexing bitrate on a quality label while encoding native retina resolution starves bits per pixel. The picture goes soft and the encoder is still drowning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding is a fourth clock, on the same machine
&lt;/h2&gt;

&lt;p&gt;Service workers cannot run &lt;code&gt;MediaRecorder&lt;/code&gt;. Capture and transcode live in an offscreen document. FFmpeg is bundled as WebAssembly. No remote encode.&lt;/p&gt;

&lt;p&gt;Default live WebM is the recorder blob, VFR, download immediately. That is the file that matches the take.&lt;/p&gt;

&lt;p&gt;A constant-frame-rate re-encode exists as a playback convenience. It is slower, generational, and can exhaust WASM’s 32-bit heap on large clips. I would rather ship the raw take than crash the export. If a container conversion fails, I keep the original recording under an honest extension (WebM or Matroska, not a lying &lt;code&gt;.mp4&lt;/code&gt; name).&lt;/p&gt;

&lt;p&gt;GIF is a special case of the same memory physics. Building a palette from every source frame at full rate OOMs. I reduce temporal resolution &lt;strong&gt;before&lt;/strong&gt; palette generation. A 4K request is not a 4K GIF if the WASM heap cannot hold it.&lt;/p&gt;

&lt;p&gt;MP4 is fastest when the live recorder already produced H.264 and I can remux without a second encode. Full software x264 in WASM is a different, much slower job. Timeouts that assume “encode always finishes in N seconds” kill healthy long transcodes. An inactivity watchdog (no log, no progress) is the useful one. A ticker that pings “still working” forever hides a deadlock.&lt;/p&gt;

&lt;p&gt;Cinema, from the previous post, is the escape hatch: an offline clock, &lt;code&gt;frame / fps&lt;/code&gt;, hardware or WebCodecs encode, then a remux. It does not fix live VFR. It refuses the live time base.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell past me
&lt;/h2&gt;

&lt;p&gt;Do not make scroll catch up to a late &lt;code&gt;rAF&lt;/code&gt; with a huge step. The encoder will publish the lie.&lt;/p&gt;

&lt;p&gt;Do not force capture frame rate to flatter a settings menu. You will alias the compositor.&lt;/p&gt;

&lt;p&gt;Do not treat “1080p” as a promise about the live file. Treat it as a pixel budget and a bitrate, then tell the truth about VFR.&lt;/p&gt;

&lt;p&gt;Local encode is the product constraint, not an implementation detail. The machine that showed the page is the machine that compresses it. When it cannot, a smaller honest file beats a crashed tab.&lt;/p&gt;

&lt;p&gt;The live export in &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt; is the compositor’s diary. The Cinema path is a score I conduct. Mixing the metaphors in one pipeline is how the stutter got “mysterious.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If you have seen a smooth screen turn into a stuttering file, I want that case.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I turned this experiment into &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt;, a Chrome extension for creating local auto-scroll captures of websites. It is free to try, and I’d genuinely value feedback from developers who work with difficult pages.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Live capture vs a frozen map: two honest ways to record a scrolling page</title>
      <dc:creator>Keny Alba</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/kenyalba/live-capture-vs-a-frozen-map-two-honest-ways-to-record-a-scrolling-page-32b6</link>
      <guid>https://dev.to/kenyalba/live-capture-vs-a-frozen-map-two-honest-ways-to-record-a-scrolling-page-32b6</guid>
      <description>&lt;p&gt;This is the second post in a series about auto-scroll capture in a Chrome extension. The first post was the problem. This one is the fork I had to make: film the live tab, or render motion from a still image of the page.&lt;/p&gt;

&lt;p&gt;They look similar in the UI. They are not the same product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same intent, two physics
&lt;/h2&gt;

&lt;p&gt;I wanted a recording that moved down a site without my hand on the wheel. That sentence hides a choice.&lt;/p&gt;

&lt;p&gt;Either the file is a recording of &lt;strong&gt;what the tab painted&lt;/strong&gt;, in real time, while something else drives scroll.&lt;/p&gt;

&lt;p&gt;Or the file is a &lt;strong&gt;reconstruction&lt;/strong&gt;: capture the page once as a tall bitmap, then move a virtual camera across that bitmap on a clock I control.&lt;/p&gt;

&lt;p&gt;Both are local. Nothing is sent to a render farm. They fail in opposite ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live capture: the tab is the source of truth
&lt;/h2&gt;

&lt;p&gt;Live capture uses Chrome’s tab recording path. Auto-scroll runs in the page. The encoder writes whatever the compositor put on screen.&lt;/p&gt;

&lt;p&gt;If a video is playing, it is in the file. If a menu opens on hover, it is in the file. If a WebGL scene reacts to wheel input, that reaction is in the file. I am not inventing those pixels. I am sampling them.&lt;/p&gt;

&lt;p&gt;That honesty has a cost. The encoder is bound to wall-clock time. It cannot pause the universe to finish a heavy frame. When the software video encoder is overloaded, Chrome does not slow the page down. It drops frames. The screen can still look smooth while the file becomes a slideshow.&lt;/p&gt;

&lt;p&gt;Tab capture also has geometry quirks. It does not automatically mean “the quality label the user picked.” It films the tab in screen pixels, then fits that into a constraint box. A box with the wrong aspect ratio letterboxes the content: black bars that you will stitch into a map if you are not careful. I size the box from the tab’s real aspect and I never ask capture to upscale. Extra pixels are interpolation, paid for by the encoder.&lt;/p&gt;

&lt;p&gt;The live scroller and the recorder are only loosely coupled. Prepare the page, start recording, then start scroll - except when I need the load animation, in which case the recorder must start &lt;em&gt;before&lt;/em&gt; the reload. There is no shared frame index. Time in the file is “when the compositor happened to emit a frame,” not “tick N of my scroll loop.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Cinema: the map is the source of truth
&lt;/h2&gt;

&lt;p&gt;Cinema mode does the opposite. It builds a full-page still — the same family of problem as a stitched PNG — then treats that image as a set. A virtual camera has a position, a speed, an easing curve, and a shutter.&lt;/p&gt;

&lt;p&gt;Because the page is a bitmap, I can sample several camera positions inside one output frame and average them. That is real motion blur: the integral of the shutter, not a directional smear guessed from velocity. Timing is &lt;code&gt;frame index / fps&lt;/code&gt;. If I ask for a given cadence, I get that cadence. Dropped live frames are not part of this model.&lt;/p&gt;

&lt;p&gt;The map’s aspect has to be the output’s aspect &lt;strong&gt;before&lt;/strong&gt; capture. Format first, map second. If I resize the window after the map exists, zoom-1 no longer frames the page. The timeline of scroll positions can survive a recapture. The pixels cannot.&lt;/p&gt;

&lt;p&gt;What the map cannot contain, the render cannot show. Hover that was never open. A &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; that was paused or never in view. WebGL that is a live scene, not a screenshot. CSS that animates on a timer rather than on scroll position. Cinema is precise about framing and rhythm. It is silent about anything that needs the page to keep running.&lt;/p&gt;

&lt;p&gt;GIF export stays on the live path. Building a palette from a compositor-style render is a different, heavier job. I did not pretend Cinema could own every container.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I choose
&lt;/h2&gt;

&lt;p&gt;I treat this as a question about &lt;strong&gt;what must be true in the file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the demo is “this landing page has a looping hero and a scroll-triggered scene,” live is the only honest tool. Cinema would freeze the hero.&lt;/p&gt;

&lt;p&gt;If the demo is “move down this article with a stable speed and a readable blur,” Cinema is the tool. Live will give me variable frame timing and whatever the encoder survived.&lt;/p&gt;

&lt;p&gt;A stitched PNG is the third deliverable: no motion at all. It shares the map pipeline’s sticky-header problem, which is the next post. It does not share Cinema’s camera.&lt;/p&gt;

&lt;p&gt;I do not blend the two in one file. A hybrid that “mostly” uses a map and “sometimes” samples the live tab would lie about both clocks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;Live still cannot see into a cross-origin iframe. Cinema cannot either: the map is a screenshot of the outer page.&lt;/p&gt;

&lt;p&gt;Live still goes black if the tab is hidden. Cinema’s map capture has the same constraint while the still is being taken, then the render is offline.&lt;/p&gt;

&lt;p&gt;A page with no real scrollbar does not give Cinema a meaningful map height, and it does not give live auto-stop. That is a driving problem, not a compositing problem.&lt;/p&gt;

&lt;p&gt;The useful discipline was naming the fork in the product, not hiding it. “Record the tab” and “render a camera over a poster” are both capture. They are not interchangeable.&lt;/p&gt;

&lt;p&gt;If you ship site demos, the question is which lie you refuse: lost interactivity, or lost timing.&lt;/p&gt;

&lt;p&gt;ScrollFlow keeps both engines on auto-scroll for that reason. I would rather the user pick an honest mode than get a file that looks fine and explains nothing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;https://bit.ly/try-scrollflow&lt;/a&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>discuss</category>
      <category>showdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>How I built a browser extension that records a full web page while auto-scrolling</title>
      <dc:creator>Keny Alba</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:03:26 +0000</pubDate>
      <link>https://dev.to/kenyalba/how-i-built-a-browser-extension-that-records-a-full-web-page-while-auto-scrolling-2bcj</link>
      <guid>https://dev.to/kenyalba/how-i-built-a-browser-extension-that-records-a-full-web-page-while-auto-scrolling-2bcj</guid>
      <description>&lt;h2&gt;
  
  
  Why a screenshot was not enough
&lt;/h2&gt;

&lt;p&gt;I needed recordings of websites that moved the way a person would scroll them: a pause near a headline, a steady pass through a long article, a stop at the footer.&lt;/p&gt;

&lt;p&gt;A full-page screenshot showed the layout. It did not show motion, hover, lazy-loaded blocks filling in, or the page-load sequence. It was a poster of the site, not a demo of using it.&lt;/p&gt;

&lt;p&gt;A manual screen recording showed those things, and also my hand on the wheel. The speed was uneven. I skipped sections. The cursor wandered. A sticky header covered the title I meant to show.&lt;/p&gt;

&lt;p&gt;That gap is why I built &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt; as a Chrome extension. It drives the scroll itself and records on the user’s machine. It does not record every website correctly. The rest of this post is about why that is true.&lt;/p&gt;

&lt;h2&gt;
  
  
  A webpage is moving target
&lt;/h2&gt;

&lt;p&gt;A webpage is not a PDF. While you travel down it, the document keeps changing.&lt;/p&gt;

&lt;p&gt;Lazy-loaded images and extra sections arrive after you thought you had reached the bottom. A cookie banner or promo bar collapses, and the “bottom” you measured a second ago is no longer there. Sticky and fixed chrome sit in every viewport. If you stitch screenshots, that chrome stamps itself onto every slice.&lt;/p&gt;

&lt;p&gt;Scroll-snap, smooth-scroll libraries, and custom overflow containers often ignore a simple “scroll to Y” command, or they fight it with their own animation loop. Canvas and WebGL scenes may have no real scrollbar at all, so the browser cannot tell you when the story is over.&lt;/p&gt;

&lt;p&gt;Capture films what the tab is actually showing, not what your scroll function believes. If the encoder cannot keep up, frames disappear while the screen still looks smooth. If the tab is in the background, the recording can go black.&lt;/p&gt;

&lt;p&gt;The hard problem is not calling scroll. It is keeping a changing document, a browser capture API, and an encoder honest about what landed in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approach: record live, render when needed
&lt;/h2&gt;

&lt;p&gt;I did not send pages to a remote rendering backend. Recording and encoding run on the user’s device. The finished file downloads locally.&lt;/p&gt;

&lt;p&gt;There are two ways to produce motion, and they answer different questions.&lt;/p&gt;

&lt;p&gt;Live capture films the actual tab while auto-scroll runs. Animations, video, hover, and WebGL that are on screen appear in the file because they were really there. Timing is whatever the browser delivered that day. That is the path I use when the page is alive.&lt;/p&gt;

&lt;p&gt;Cinema mode captures the page once as a still full-page map, then flies a virtual camera over that image. Timing is controlled. Motion can look deliberate because the “page” is a bitmap. Hover menus, playing video, and WebGL are not in that bitmap, so they cannot appear. I use this when I need a clean move down a mostly static layout.&lt;/p&gt;

&lt;p&gt;A stitched full-page PNG is still useful when a still is the deliverable. It is a scan of viewports, with sticky chrome handled as a special case, not a screenshot of the whole document object.&lt;/p&gt;

&lt;p&gt;Exports stay on the machine: the live recording, a local conversion when a different container is needed, or a composited file from the map. If a conversion fails, the capture is not thrown away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where browser automation gets difficult
&lt;/h2&gt;

&lt;p&gt;Four cases ate most of the work.&lt;/p&gt;

&lt;p&gt;Lazy-loaded content lies about height. Auto-scroll can hit a fake bottom, notice that the document grew, and continue. It can also unlock if a banner collapsed and the real bottom moved. Infinite feeds and images that arrive late still surprise it. There is no complete answer, only a better guess than stopping on the first pause.&lt;/p&gt;

&lt;p&gt;Sticky and fixed UI reprints itself. For a full-page image I keep the header on the first slice and hide it on the rest, then look again after each step. Many navs are ordinary until you scroll, then they become fixed. Overlay cleanup hides chrome without deleting nodes, and it still guesses wrong on some widgets.&lt;/p&gt;

&lt;p&gt;Scroll-snap and custom scroll engines fight linear motion. Pixel-by-pixel scrolling on a snapped marketing site looks like the page is arguing with you. Ordinary auto-scroll turns snap off. A stepped, impulse-style move leaves snap on, because those sites use it as the section cut. Smooth-scroll libraries remember an old position and restore it after you jump to the top, so the start of a capture has to hold the page there until they settle. Overflow boxes that actually own the scroll need extra handling. Some pages still win.&lt;/p&gt;

&lt;p&gt;Canvas, WebGL, and dynamically rendered scenes often have no usable scrollbar. If the document barely scrolls, I keep moving at a constant speed and I do not auto-stop. The user has to end the recording. That is the honest outcome for a scene that never exposed a height. Cinema mode cannot invent a running canvas from a still map.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;I learned four things I wish I had believed on day one.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A screenshot and a recording are different products. Freezing animation and hiding sticky chrome is correct for a poster and wrong for a scroll-driven demo.&lt;/li&gt;
&lt;li&gt;The page, the compositor, and the encoder are three clocks. Forcing them into one number made the video look worse than accepting what the live tab actually produced.&lt;/li&gt;
&lt;li&gt;Local encoding is a feature and a limit. Work stays on the device. Heavy conversions can fail, so a lesser local file is better than a crashed export.&lt;/li&gt;
&lt;li&gt;Browser APIs dominate the design. A background script cannot record media. A side panel does not grant tab access the way a toolbar popup does. Hidden tabs go black. Those constraints decided the architecture more than any UI idea.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What still breaks
&lt;/h2&gt;

&lt;p&gt;Cross-origin iframes are a separate document. If the real page lives in a frame, auto-scroll may move the wrong thing or nothing useful.&lt;/p&gt;

&lt;p&gt;If you switch away from the tab, the capture can go black. There is a warning. There is no recovery.&lt;/p&gt;

&lt;p&gt;Horizontal pages are out of scope. The driver moves on the vertical axis.&lt;/p&gt;

&lt;p&gt;Complex WebGL and custom-scroll scenes with no real scrollbar do not auto-finish. You stop them by hand. Cinema mode will not reconstruct hover, video, or a running canvas from a frozen map.&lt;/p&gt;

&lt;p&gt;I do not claim this works on every website. The difficult pages are why the work exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback welcome
&lt;/h2&gt;

&lt;p&gt;If you have fought sticky headers in stitched screenshots, custom scroll fighting a programmatic move, or a recording that does not match what you saw on screen, I want those cases.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I turned this experiment into &lt;a href="https://bit.ly/try-scrollflow" rel="noopener noreferrer"&gt;ScrollFlow&lt;/a&gt;, a Chrome extension for creating local auto-scroll captures of websites. It is free to try, and I’d genuinely value feedback from developers who work with difficult pages.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>productivity</category>
      <category>discuss</category>
      <category>tools</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
