← All posts

Full Page Screenshot API: Fixing Lazy Load & Sticky Headers

September 11, 2026

Most tutorials on capturing a full page screenshot show a single line — fullPage: true — and call it done. In production, that's where trouble starts. Screenshot pipelines that work in a demo start returning blank rectangles, duplicated headers, and half-loaded feeds the moment they touch a real marketing site, e-commerce catalog, or social feed. This article covers the three failure modes that actually break full-page capture, why each happens at the rendering level, and how a purpose-built full page screenshot API removes the need to hand-maintain scroll-and-stitch scripts.

Viewport vs. Full-Page Screenshots: What's Actually Different

A viewport screenshot captures exactly what's visible in the browser window — the pixels inside, say, a 1280x800 frame, nothing above or below the fold. A full-page screenshot is not a second camera pointed at a longer canvas; it's a reconstructed render. Puppeteer and Playwright both implement fullPage: true by temporarily resizing the viewport (or stitching a series of scrolled frames) to match the page's full scrollHeight, then rendering that expanded document in one pass.

That means full-page capture inherits every dynamic behavior of the live page — lazy loading, scroll-triggered animations, sticky positioning — compressed into a single render pass that never actually scrolls the way a human would. The difference isn't just dimensions; it's whether the browser ever executes the scroll-driven JavaScript modern pages depend on to look complete. That gap is the root cause of all three problems below.

Problem 1: Lazy-Loaded Images Render as Blank Rectangles

Most image-heavy pages use loading="lazy" or a custom IntersectionObserver to defer offscreen images until they're about to enter the viewport. When Chromium expands the viewport height to capture the full page in one shot, it never triggers the scroll events or intersection callbacks that would normally load those images — it jumps straight to the final height without passing through the intermediate scroll positions IntersectionObserver watches for. The result: images below the initial fold render as blank gray rectangles or broken placeholders, even though the page looks fine to a human visitor.

The common workaround is a manual scroll-then-wait loop: scroll to the bottom in fixed increments, pause a few hundred milliseconds at each step to let the observer fire and the image decode, then scroll back to the top before calling fullPage: true. It works, but it's fragile. Image decode time varies with file size and network latency, so a fixed delay that works for a CDN-hosted thumbnail gallery will clip a page loading large hero images from a slow origin. Get the timing wrong and you're back to blank images that only work for the exact page you tested against.

Problem 2: Sticky Headers Duplicate or Float Mid-Page

position: fixed and position: sticky elements are defined relative to the viewport or scroll container, not the document. When a headless browser stitches a full-page render by resizing the viewport instead of physically scrolling and compositing frames, that fixed header gets painted at its anchored position in the final, tall viewport — which may now sit hundreds of pixels below where a human would ever see it, or repeated at each stitched slice depending on the rendering engine's approach. Either way, you end up with a duplicated header floating mid-page or a phantom nav bar hovering over content it was never meant to sit above.

The standard fix is a CSS injection step before capture: set display: none (or strip position: fixed) on the offending selector, take the screenshot, then optionally restore it. This works but requires knowing the exact selector for every site you're capturing — a fragile assumption when screenshotting arbitrary third-party URLs, client sites, or pages that ship a redesign without warning. It's solvable per-site, but not at scale without constant selector maintenance.

Problem 3: Infinite Scroll Has No Real Bottom

Infinite-scroll feeds — a Twitter/X timeline, a Reddit feed, a Pinterest-style masonry grid — load more content dynamically as scrollHeight approaches the bottom of the loaded DOM. That means there is no fixed "full page" height to expand to. A capture tool that scrolls to trigger lazy content will keep triggering more content, because the height it's chasing keeps growing. Left unbounded, a full-page capture on this kind of page either hangs, times out, or grabs an arbitrary partial state depending on when the timeout fires.

Developers typically work around this with a height-plateau detector: scroll down, record document.body.scrollHeight, wait, scroll again, and stop once the height hasn't changed across two or three consecutive checks — treating that as "stable enough." It's a reasonable heuristic, but it has a hard ceiling: virtualized lists that unload offscreen items to save memory can report a stable height while still cycling in new content, giving false confidence that the page has finished loading.

How a Full Page Screenshot API Handles All Three

Each failure mode stems from the same root issue: fullPage: true alone gives Chromium no instructions about how to reach a stable, fully-rendered state before it takes the shot. A managed full page screenshot API solves this by exposing the workarounds developers already write by hand as request parameters instead of custom code.

A scroll-before-capture option walks the page in real increments — the same motion a user makes — so IntersectionObserver callbacks fire and lazy images decode before the shot fires, addressing the blank-image problem without a hardcoded delay. A selector-hiding parameter lets you name (or auto-detect) sticky/fixed elements to suppress before capture, solving the duplicate-header issue without maintaining per-site CSS overrides. And a scroll-iteration limit or networkidle-style wait condition gives infinite-scroll pages a defined stopping point instead of an open-ended timeout.

The practical upside is fewer moving parts: no self-hosted Chromium fleet to patch when a browser update changes stitching behavior, no per-client selector list to keep current, and no fixed-delay guesswork that breaks on slow networks. Teams currently maintaining their own Puppeteer or Playwright scroll-and-stitch scripts can see the exact migration path in Puppeteer to API Migration: A Step-by-Step Runbook.

If you're tired of babysitting scroll timers and selector lists across every site your pipeline touches, you can hand the whole problem to a single endpoint. Call Browsevra's screenshot API with a full_page parameter (plus scroll-wait and selector-hiding options where needed) and get back a correctly rendered image — no custom stitching code required. Check the Docs for the exact request format, or see Pricing if you're comparing the cost against a self-hosted headless browser fleet. Start at browsevra.

Frequently Asked Questions

Why does my full-page screenshot have blank spaces where images should be?

Blank spaces happen because lazy-loaded images use loading="lazy" or an IntersectionObserver that only fires when an image scrolls into view. Full-page capture resizes the viewport to the document's full height in one step, so those scroll-triggered events never fire and the images never load before the screenshot is taken. Scrolling through the page in real increments before capturing fixes it.

How do I stop a sticky header from appearing multiple times in a full-page screenshot?

Hide or unset the position: fixed/sticky element with a CSS injection right before the capture runs, then restore it afterward if needed. The header duplicates because it's anchored to the viewport, not the document, so it gets repainted at its fixed position when the browser expands to full-page height. A screenshot API that accepts a selector-hiding parameter removes the need to do this manually per site.

Can you take a full-page screenshot of an infinite-scroll page like Twitter or Reddit?

Yes, but only by defining a stopping point, since these pages have no true bottom — scrollHeight keeps growing as you scroll. A height-plateau check (stop once height stabilizes across a few scroll checks) or a scroll-iteration cap gives you a usable, if not infinite, capture. Without one of these limits, the capture will hang or time out.

What's the difference between a viewport screenshot and a full-page screenshot?

A viewport screenshot captures only the visible browser window at its set dimensions. A full-page screenshot expands the render to the page's full scrollable height and captures the entire document in one pass, which means it inherits every scroll-dependent behavior — lazy loading, sticky positioning, infinite scroll — that a viewport shot never triggers.

Does Puppeteer's fullPage: true option trigger lazy-loaded images automatically?

No. fullPage: true resizes the viewport to the document's full height without physically scrolling through it, so IntersectionObserver-based lazy loading never fires and offscreen images stay unloaded. You need a separate scroll-and-wait step before calling the screenshot function to force those images to load first.

How long should I wait after scrolling before capturing a screenshot?

There's no universal fixed delay — it depends on image size and network speed, which is why hardcoded waits are fragile. A safer approach waits for a networkidle-style condition or checks that images have finished decoding rather than assuming a fixed millisecond value will work across every site.