The list is virtualized
Rows outside the viewport may be removed as new rows appear. No single DOM state contains the whole list, so one full-page image is the wrong artifact.
A full-page surface is not the same thing as a user scrolling through it. IntersectionObserver, native `loading=lazy`, virtualized lists and image decoding all attach different readiness semantics to viewport movement.
The page height is correct but image boxes below the fold are blank, low-resolution placeholders remain, or later list rows never exist in the DOM.
Scroll one viewport at a time, wait for the page's own assets, stop on stability or a ceiling, and return to the top before capture.
async function settleLazyContent(page, maxHeight = 30_000) {
let stable = 0;
let previousHeight = 0;
for (let step = 0; step < 80; step++) {
const state = await page.evaluate(async (ceiling) => {
scrollBy(0, innerHeight);
await document.fonts?.ready;
await Promise.all([...document.images].map((img) => img.decode().catch(() => {})));
return { y: scrollY, height: document.documentElement.scrollHeight, ceiling };
}, maxHeight);
stable = state.height === previousHeight ? stable + 1 : 0;
previousHeight = state.height;
if (stable >= 2 || state.y + 900 >= Math.min(state.height, maxHeight)) break;
await page.waitForTimeout(400);
}
await page.evaluate(() => scrollTo(0, 0));
}Rows outside the viewport may be removed as new rows appear. No single DOM state contains the whole list, so one full-page image is the wrong artifact.
A successful HTTP response does not guarantee pixels are decoded. Waiting on `img.decode()` catches a class of blank placeholders that network idle misses.
Some applications scroll an inner container or animate with transforms. Moving `window` does not trigger the observer attached to that container.
Infinite feeds require an explicit row, selector, step or height limit chosen by the product consuming the artifact.
A bounded full-page PNG after the main document has entered each viewport band. The algorithm returns to the top so ordinary sticky headers paint in their initial position.
One successful, non-cached capture costs one credit. Failures, including platform failures, cost zero; cache hits cost zero.
A required marker is still absent after the render actions complete.
HTTP 500 · terminal
The requested document and scale exceed the output pixel ceiling.
HTTP 400 · terminal
The runner above carries this exact configuration into the playground — no retyping, no starting over.
Written by PageCapture Engineering. Reviewed by PageCapture render maintainers. Verified 2026-08-12 by running the published code against a fixture.
The recipe and snippets are checked in CI; the documented sequence matches `settle()` and the bounded `scrollThroughPage()` implementation used by the worker.
Sources support the browser and API behaviors named above. PageCapture-specific limits and billing are taken from the public contract; external sources are used for the underlying browser behavior.
Defines native and scripted lazy-loading behavior.
Defines the bounded scroll and slicing controls.