Menu
Sign up

Puppeteer full-page screenshots: lazy images, sticky headers and size limits

Measured with Puppeteer 25.7: why lazy images stay blank, why headers land mid-page, pages fullPage cannot capture, and WebP and JPEG size limits.

Published November 6, 2026 · 5 min read

A Puppeteer full-page screenshot is await page.screenshot({ path: 'page.png', fullPage: true }), and on a simple page that is all it takes. On real pages four things go wrong: lazy images come out as empty boxes, sticky or fixed headers appear in the middle, pages that scroll inside an element come out one screen tall, and very tall captures in WebP or JPEG come back empty. Every number on this page was measured on 2026-11-06 with Puppeteer 25.7.0 in the official ghcr.io/puppeteer/puppeteer image, against small test pages served locally, so you can repeat each test.

For the basics — viewport, elements, retina, devices, waiting — see the Puppeteer screenshot guide. This page is only about the full page.

A full-page capture that works

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.goto(url, { waitUntil: 'networkidle2' });

// 1. Make lazy content load: native lazy images to eager, then scroll through.
await page.evaluate(() => document.querySelectorAll('img[loading="lazy"]').forEach((img) => { img.loading = 'eager'; }));
await page.evaluate(async (maxScreens) => {
  for (let i = 0; i < maxScreens; i++) {
    const before = window.scrollY;
    window.scrollBy(0, window.innerHeight);
    await new Promise((r) => setTimeout(r, 150));
    if (window.scrollY === before) break; // the real bottom
  }
  window.scrollTo(0, 0); // 2. back to the top, or sticky headers land mid-page
}, 30);

// 3. Wait for every image and web font.
await page.evaluate(() => Promise.all([
  ...[...document.images].map((img) => img.complete ? null : new Promise((r) => { img.onload = img.onerror = r; })),
  document.fonts.ready,
]));

await page.screenshot({ path: 'page.png', fullPage: true });
await browser.close();

The rest of this page explains each step and the cases it does not cover.

Lazy images stay blank

A full-page screenshot does not make the browser load what is below the fold. On a test page with ten loading="lazy" images below a 3,000 px spacer, none had been requested after goto with networkidle0, and none after a fullPage: true capture either. The same was true for ten boxes loaded by an IntersectionObserver — the pattern most JavaScript lazy loaders use. After scrolling through the page in screen-sized steps, all ten loaded in both cases.

Two ways to load them:

  • Switch native lazy images to eager. Setting img.loading = 'eager' made all ten images load without any scrolling. It does nothing for JavaScript loaders.
  • Scroll through the page. Works for both kinds. Step one screen at a time and pause briefly so observers fire; 150 ms per screen was enough on the test page, a slow site needs more.

Then wait for the images to finish (img.complete), as in the code above — the scroll starts the downloads, it does not wait for them.

Infinite scroll needs a cap

A page that appends content whenever you near the bottom never has a bottom. A scroll loop without a limit runs until your timeout. On a test page that adds five 800 px blocks per scroll, the loop above stopped at its cap of 20 screens after 3 seconds, with the page 16,080 px tall at that point. Choose the cap from what you need — the first ten screens of a feed, say — rather than "everything".

Sticky and fixed headers in the middle

A position: sticky or position: fixed header is drawn where it sits at the current scroll position. Captured from the top, a 60 px header appeared once, at the top. After window.scrollTo(0, 2000) — the last step of many scroll-through scripts — the same header was drawn at y = 2,000 in the full-page image, both for sticky and for fixed. Always scroll back to the top before the capture.

Headers that JavaScript moves on scroll (shrinking navbars, "show on scroll up") can still end up in odd places; pin them first:

await page.addStyleTag({ content: '.site-header, .sticky-cta { position: absolute !important; }' });

Pages that scroll inside an element

Some layouts never scroll the document: html and body are height: 100%; overflow: hidden, and a <main> with overflow: auto scrolls instead (common in dashboards and single-page apps). fullPage measures the document, so on a 400×600 viewport such a page with 5,000 px of content came out 400×600 — one screen.

Two fixes:

  • Capture the element. await (await page.$('main')).screenshot() captures the element's visible box; for its full content, take the next option.
  • Undo the inner scroll with CSS before the shot, so the content flows in the document:
await page.addStyleTag({ content: 'html, body { height: auto !important; overflow: visible !important; } main { height: auto !important; overflow: visible !important; }' });

How tall can it be? PNG yes, WebP and JPEG no

Articles about Puppeteer often mention a 16,384 px limit. With PNG we did not hit it: pages of 33,000 and 100,000 CSS px came out at full height with every block in the right place, and a 70,000 px page at deviceScaleFactor: 2 gave a correct 800×140,000 PNG in about 5 seconds. Turning off captureBeyondViewport (screenshot options) made no difference at 20,000 px.

The formats are another story. In the same setup:

Format Largest dimension that worked One pixel more
PNG 140,000 px (the largest we tried) —
WebP 16,383 px empty buffer
JPEG 65,500 px empty buffer

At the limit, page.screenshot() did not throw: it returned a buffer of 0 bytes, and with path it would write an empty file. The limit counts device pixels, so with deviceScaleFactor: 2 a WebP page breaks at 8,192 CSS px. 16,383 × 16,383 is the largest image WebP allows (WebP FAQ), and 65,500 px is libjpeg's maximum dimension. If you save tall pages as WebP or JPEG, check the length of the result, or clip long pages into several images:

const buffer = await page.screenshot({ fullPage: true, type: 'webp' });
if (buffer.length === 0) throw new Error('Capture too tall for WebP: use PNG or clip it');

Very tall PNGs are large in memory even when the file is small — 140,000 px of mostly flat colour was 0.6 MB as a file, but 800×140,000 pixels is about 450 MB as raw RGBA in whatever processes the image next. Pages with photos give much larger files.

When the page will not cooperate

Some failures are not about Puppeteer: cookie banners covering the content (click "accept" or hide them; the screenshot guide has a snippet), sites that block headless browsers, and pages whose height changes while you capture (carousels, live feeds). For the last, stop animations first with page.emulateMediaFeatures([{ name: 'prefers-reduced-motion', value: 'reduce' }]) and an addStyleTag that sets animation: none !important; transition: none !important on *.

Or let an API do the scroll

Shareglint's screenshot API does these steps for you on public pages: full_page: true with lazy_load: true scrolls through the page and waits for images, block_cookie_banners handles consent dialogs, and a full-page shot costs 2 renders of quota. Our cap is a full page of 16,000 pixels in height — so a WebP or JPEG never comes back empty, but a longer page is cut at that height, which your own Puppeteer with PNG does not have to do. The JavaScript screenshot API guide shows the request. For pages behind a login or on your own network, your own Puppeteer remains the right tool.

FAQ

How do I take a full-page screenshot in Puppeteer?

Call page.screenshot({ path: 'page.png', fullPage: true }) after page.goto. Before that, scroll through the page so lazy images load, wait for them, and scroll back to the top — otherwise lazy images stay blank and sticky headers can land mid-page.

Is there a maximum height for a Puppeteer full-page screenshot?

For PNG we found none in practice: Puppeteer 25.7 captured a 100,000 px page correctly. WebP stops at 16,383 px and JPEG at 65,500 px — above that, screenshot() returned an empty buffer without an error.

Why are images blank in my full-page screenshot?

They are lazy-loaded. A full-page capture does not trigger loading=lazy images or IntersectionObserver loaders — in our test none of ten loaded. Scroll through the page first, or switch native lazy images to eager, and wait until every image is complete.

Why does fullPage only capture the first screen?

The page scrolls inside an element, not the document: html and body are 100% high with overflow hidden and a main element scrolls. fullPage measures the document, which is one screen tall. Capture the scrolling element, or remove the inner scroll with CSS before the shot.