DEV Community

Cover image for Full-page screenshots that are actually complete: lazy loading, sticky headers, 100vh layouts and emoji
Tobby
Tobby

Posted on

Full-page screenshots that are actually complete: lazy loading, sticky headers, 100vh layouts and emoji

Disclosure: I build and sell one of the three screenshot tools compared below, a hosted screenshot Actor on Apify. This article was written with the help of AI; the numbers come from the public benchmark data linked at the end, and everything here works in a plain Playwright or Puppeteer script.

page.screenshot({ fullPage: true }) looks done until you look at the output: blank rectangles where lazy images should be, the sticky header stamped down the page, a cookie banner across the middle, and emoji as tofu boxes. Here is what broke when I captured 30 real sites, and what fixed it.

1. Lazy-loaded images: scroll first

Images below the fold load only when they come close to the viewport. Scroll the whole page in steps, wait for pending images, then go back to the top before capturing:

await page.evaluate(async () => {
  const step = window.innerHeight;
  const max = Math.min(document.documentElement.scrollHeight, 30_000);
  for (let y = 0; y < max; y += step) {
    window.scrollTo(0, y);
    await new Promise((r) => setTimeout(r, 150));
  }
  window.scrollTo(0, 0);
});
await page
  .waitForFunction(() => [...document.images].every((i) => i.complete), null, { timeout: 10_000 })
  .catch(() => {}); // a broken image should not fail the whole capture
Enter fullscreen mode Exit fullscreen mode

Cap the scrolling (I stop at 30,000 px) or infinite feeds will scroll forever, and record that you hit the cap, so nobody mistakes a truncated page for a complete one.

2. Sticky and fixed elements

Fixed headers repeat on every slice of a stitched screenshot, and fixed layers get painted in the wrong place. What worked for me:

  • position: sticky → force position: relative. At scroll position 0 the element sits in its natural spot anyway.
  • A fixed bar at the top of the viewport → switch it to absolute at its current coordinates, so it appears once, at the top.
  • Other fixed layers (chat bubbles, back-to-top buttons, bottom bars, modal backdrops with a huge z-index) → hide them.
  • Check again right before the capture. Some headers only become fixed after you scroll, and some "please disable your ad blocker" modals show up late.

3. Capture speed and 100vh layouts

Chrome can capture beyond the viewport, but on Wikipedia's long Emoji article that took about 14 seconds per slice. So by default I resize the viewport to the page height (in slices of up to 8,000 px) and capture what is on screen.

That breaks on layouts built from 100vh sections: make the viewport 8,000 px tall and a "full-screen" hero becomes 8,000 px tall too. When the page height changes on resize, I fall back to scrolling a normal-size viewport and stitching the slices. Pages where body, not the window, is the scrolling element (getemoji.com is one) need yet another path.

Here is the bottom half of vercel.com, which uses that kind of layout, captured by three tools (left to right: mine, apify/screenshot-url, cheapapi/website-screenshot-api). The middle one repeats the fixed hero over the footer; the right one is blank in the lower half:

vercel.com full-page captures from three tools

4. Emoji and CJK fonts in containers

Slim Docker images have no colour-emoji font, so 😀 becomes ☐, and Japanese or Chinese text turns into boxes. Two Debian packages fix it:

RUN apt-get update \
 && apt-get install -y --no-install-recommends fonts-noto-color-emoji fonts-noto-cjk \
 && fc-cache -f
Enter fullscreen mode Exit fullscreen mode

getemoji.com captured by three tools

5. Cookie walls and bot checks

Consent banners can be dismissed: block the consent manager's script, or click its "accept" button before capturing. CAPTCHAs are different. I don't try to bypass them; I detect the challenge page and report an error instead of a picture of it. (Careful: many normal pages load an invisible reCAPTCHA badge. Only treat a visible challenge as a block, or you will flag perfectly good pages. I learned that one this week.)

How three tools did on 30 sites

The same 30 pages (Stripe, The Guardian, BBC, Wikipedia, Vercel, getemoji, Japanese Wikipedia, Spiegel, IKEA and others), one run per tool on 18 Oct 2026:

mine apify/screenshot-url cheapapi
Complete and visually correct 24/30 19/30 18/30
Full page captured (28 reachable pages) 27/28 25/28 21/28
Bot-blocked pages (medium.com, unsplash.com) 2 errors, nothing billed 1 challenge-page image, 1 no output 2 challenge-page images

Caveats: one run each, with an earlier build of mine; 13 of the 30 rows were judged on captured height vs. true page height only, without visual inspection; the apify run was stopped at 29/30 for budget. Still imperfect in mine: booking.com shows grey skeleton placeholders, and The Verge has an empty ad band.

Per-site verdicts, run IDs, heights and the URL list are in the benchmark folder: github.com/tobbytools/website-screenshot-examples/tree/main/benchmark. The repo also has MIT-licensed calling examples for my tool.

Which sites break your screenshots? Drop them in the comments and I'll add them to the next test run.

Top comments (0)