cd /news/developer-tools/the-scrollheight-lie-how-i-finally-g… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-35727] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

The scrollHeight Lie: How I Finally Got Full-Page Screenshots Right with Playwright

A developer building knallhart.dev, a tool that takes full-page screenshots of websites for AI critique, found that relying on document.body.scrollHeight for scrolling is unreliable due to lazy-loaded content and infinite scroll. They developed a more robust method that scrolls step by step and monitors window.scrollY for changes, combined with waiting for image loads, to ensure accurate full-page captures.

read3 min views1 publishedJun 21, 2026

If you've ever tried to take a full-page screenshot of a modern website

programmatically, you've probably run into the same wall I did:

document.body.scrollHeight

lies. Constantly.

Lazy-loaded images, infinite scroll sections, sticky headers, scroll-triggered

animations β€” all of these mess with the page's reported height in ways that

make scrollHeight

an unreliable signal for "have I reached the bottom of

this page yet?"

I was building knallhart.dev, a tool that takes a

full-page screenshot of any website and sends an AI-generated critique via

email. The screenshot step turned out to be the hardest part of the entire

build β€” harder than the AI integration, harder than the payment flow.

My first approach was the obvious one: read scrollHeight

, calculate how

many scroll steps are needed, scroll that many times, done.

const pageHeight = await page.evaluate(() => document.body.scrollHeight);
const steps = Math.ceil(pageHeight / 600);

for (let i = 0; i < steps; i++) {
  await page.evaluate((y) => window.scrollBy(0, y), 600);
  await page.waitForTimeout(200);
}

This worked on simple static pages and broke immediately on anything modern.

Pages with lazy-loaded content report a short initial scrollHeight

, then

grow as you scroll β€” so the calculated step count is wrong before you've

even started. Pages with infinite scroll never stop growing at all.

Instead of trying to calculate the destination upfront, I scroll step by

step and watch whether window.scrollY

is still changing. If it stops

changing for a few consecutive checks, I've actually hit the bottom β€”

regardless of what scrollHeight

claims.

let lastScrollY = -1;
let noChangeCount = 0;
const maxAttempts = 40; // safety cap, ~8 seconds

for (let i = 0; i < maxAttempts; i++) {
  await page.evaluate(() => window.scrollBy(0, 600));
  await page.waitForTimeout(200);

  const currentScrollY = await page.evaluate(() => window.scrollY);

  if (currentScrollY === lastScrollY) {
    noChangeCount++;
    if (noChangeCount >= 3) break; // confirmed: no more progress
  } else {
    noChangeCount = 0;
    lastScrollY = currentScrollY;
  }
}

await page.evaluate(() => window.scrollTo(0, 0));

This is more robust because it doesn't trust any single height value β€” it

trusts observed behavior over time. A page that's still content

will keep moving scrollY

; a page that's truly done won't, no matter how

confusing its scrollHeight

is.

Triggering the scroll isn't enough on its own β€” lazy-loaded images often

need a moment to actually finish after they enter the viewport.

I added an explicit wait for image load events before taking the final

screenshot:

\javascript

await page.evaluate(async () => {

const images = Array.from(document.querySelectorAll("img"));

await Promise.all(

images.map((img) => {

if (img.complete) return Promise.resolve();

return new Promise((resolve) => {

img.addEventListener("load", resolve);

img.addEventListener("error", resolve);

setTimeout(resolve, 5000); // don't hang forever on one broken image

});

})

);

});

\

Don't trust any single DOM measurement as an endpoint signal on a page you

don't control. Modern websites are dynamic enough that almost any static

value can lie to you at some point. Behavior over time β€” does this keep

changing or not β€” is a much sturdier signal than asking the DOM "are we

there yet?"

If you're curious what this screenshot pipeline turned into:

knallhart.dev β€” it roasts your website with AI

and emails you three things that are actually wrong with it.

Happy to go deeper on any part of this if useful.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @knallhart.dev 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/the-scrollheight-lie…] indexed:0 read:3min 2026-06-21 Β· β€”