How to Screenshot a Page with Infinite Scroll Using Playwright
Load the content you need by scrolling, wait for the page to render it, then capture the loaded document with Playwright’s fullPage option.
Direct answer: fullPage: true captures the page’s currently loaded scrollable content; it does not itself scroll through an infinite feed to make the site load more. Use Playwright to scroll the page or its nested list container, wait for a site-specific signal that the next items rendered, repeat until you reach a defined limit, and then take the full-page screenshot.
1. Set up Playwright
This runnable Node.js example uses Chromium. Install Playwright and its browser first:
npm init -y
npm install playwright
npx playwright install chromium
Save the following as screenshot-infinite-scroll.js. It uses an example feed whose items have .feed-item and whose list is the document itself. Change the URL, selectors, and stopping rule for the target site. The script scrolls in steps, waits for the item count to grow, and stops at a maximum item count or when no new items appear within the timeout.
const { chromium } = require('playwright');
(async () => {
const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot-infinite-scroll.js https://example.com/feed');
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
const items = page.locator('.feed-item');
const maxItems = 100;
const maxRounds = 20;
const loadTimeoutMs = 8000;
for (let round = 0; round < maxRounds; round++) {
const before = await items.count();
if (before >= maxItems) break;
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
try {
await page.waitForFunction(
({ selector, previousCount }) =>
document.querySelectorAll(selector).length > previousCount,
{ selector: '.feed-item', previousCount: before },
{ timeout: loadTimeoutMs }
);
} catch (error) {
// No count increase within the limit: treat this as the feed's end.
// If the site exposes a loading indicator or end marker, wait for that
// signal instead (see the loading-signal section below).
break;
}
}
await page.screenshot({ path: 'page.png', fullPage: true });
console.log(`Saved page.png (${await items.count()} items found)`);
} finally {
await browser.close();
}
})().catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with:
node screenshot-infinite-scroll.js https://example.com/feed
The example’s timeout is a per-round bound, not proof that the feed has ended. On a slow or variable site, use a reliable page signal and tune the timeout to the application. A fixed sleep by itself can be too short when loading is slow and waste time when it is fast.
2. Choose a finite stopping rule
An endlessly loading feed has no finite “capture everything” state. Decide what the screenshot needs to include before writing the loop. Common limits are:
- Item count: stop after a known number of records.
- Target item: stop once a particular item or marker appears.
- Scroll depth: stop after a maximum number of scroll steps or document height.
- End marker: stop when the application displays a “no more results” element.
- Time or request budget: stop after a defined total duration or number of fetches.
Use more than one bound where practical. For example, a target item can be the normal stopping condition while a maximum number of rounds prevents an unexpected feed from running indefinitely.
3. Wait for the application’s loading signal
The best wait condition reflects what the site actually does. Playwright documents scrolling as a way to force an infinite list to load more elements. Its scrolling guide includes scrolling a footer into view and using the mouse wheel or an element’s scrollTop for a scrollable container. See the Playwright scrolling guide.
Wait for the item count to increase
If each record has a stable selector, the item-count wait in the runnable script is a useful pattern. For sites where records are replaced or virtualized, count growth may not work; wait for a specific new item or application state instead.
Wait for a loading indicator to disappear
await page.locator('[data-testid="loading-indicator"]').waitFor({
state: 'hidden',
timeout: 10000
});
This is appropriate only if that indicator reliably appears during each load and disappears when rendering finishes. If it is initially hidden, first wait for it to become visible or use another signal.
Wait for a known item
await page.getByText('The expected next record', { exact: true }).waitFor({
state: 'visible',
timeout: 10000
});
Use a unique text or selector that identifies the expected content. Avoid relying on text that might already be present before scrolling.
Wait for a site-specific JavaScript condition
await page.waitForFunction(() => {
const list = document.querySelector('[data-testid="feed"]');
return list?.getAttribute('aria-busy') === 'false';
}, { timeout: 10000 });
Replace the condition with a state the page actually exposes. A generic “network idle” wait can be a poor fit for feeds that keep analytics, polling, or streaming requests open; it may not represent completion of the content you need.
4. Scroll the correct surface
For a document-scrolling page, moving to the current bottom commonly triggers the next batch:
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
Some applications put the feed inside a fixed-height element with its own scrollbar. Scrolling the window then does not move that list. Set the container’s scroll position or move the pointer over it and use the wheel:
const feed = page.locator('[data-testid="feed"]');
await feed.evaluate(element => {
element.scrollTop = element.scrollHeight;
});
await page.locator('[data-testid="feed"]').hover();
await page.mouse.wheel(0, 700);
Repeat the scroll and loading wait until the chosen stop condition is met. The official scrolling documentation covers these approaches. For a nested container, make sure the loading trigger belongs to that container rather than the main document.
5. Capture the result
After the desired records are present and rendered, capture the loaded document:
await page.screenshot({ path: 'page.png', fullPage: true });
Playwright describes a full-page screenshot as the full scrollable page as if displayed on a very tall screen. See the screenshot guide and the Page screenshot API. This describes the screenshot extent; it does not mean Playwright will trigger every fetch in an infinite list for you.
| Goal | Approach | Consideration |
|---|---|---|
| Capture the visible viewport | page.screenshot({ path: 'viewport.png' }) |
Useful for one viewport or a sequence of screenshots taken while scrolling. |
| Capture all currently loaded document content | page.screenshot({ path: 'page.png', fullPage: true }) |
Scroll and wait for the required content first. |
| Capture a rectangular page region | Use the screenshot clip option. |
Coordinates and dimensions define the captured region. |
| Capture one element | await page.locator('.feed-item').screenshot({ path: 'item.png' }) |
An element screenshot is not a substitute for capturing the full loaded feed; scrollable elements may show only their current content. |
The API also supports options such as image type, scale, clipping, and animation handling. Check the API reference for the options available in your installed version.
6. Handle common edge cases
Lazy-loaded images
Some images load only when they approach the viewport. Scrolling through the feed can trigger them, but wait for the images you need to finish loading before capture if the site exposes a suitable signal. A screenshot taken immediately after new text appears may still contain empty image areas.
Virtualized lists
A virtualized list may remove off-screen records from the DOM as new ones appear. In that case, scrolling through the whole feed does not guarantee that every record remains in the document for one enormous full-page screenshot. Consider capturing viewport-sized sections as you scroll, or use an application-provided export if the requirement is a complete record archive. A full-page screenshot covers the page’s scrollable extent at capture time, not historical items already removed from the DOM.
Sticky headers and dynamic layout
Sticky elements, expanding cards, and images that change size can affect a very tall capture. Wait until the layout has settled and consider whether the sticky element should appear in every viewport segment or only once. If exact visual output matters, keep viewport and browser settings consistent.
Authentication and consent
When the feed requires a signed-in session, provide the browser context with the appropriate authentication state or navigate through the supported login flow. Consent dialogs can obscure content or intercept scrolling; handle them according to the target site’s intended behavior before the capture.
7. Reliability, performance, and cost
- Reliability: use observable application state rather than a sleep alone. Bound every loop and give navigation, waits, and the whole job finite time limits.
- Consistency: the amount of content and layout can change between runs. Define a stable stop condition and avoid capturing while records are still being inserted.
- Performance: each scroll-and-wait round adds time and may trigger more network and rendering work. Stop at the content needed; very long pages also produce larger image files and can require substantial browser memory.
- Output size: choose the viewport and image format appropriate to the use. If a single tall image becomes unwieldy, capture multiple viewport sections or a selected region.
- Cost: Playwright is browser automation you run in your own environment. Your practical costs come from the compute, browser runtime, storage, and any services the page itself requires; there is no universal per-screenshot Playwright price in the cited documentation.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot stops after the first screen | fullPage was omitted, or only the viewport was captured. |
Use fullPage: true after loading the desired content. |
| Screenshot is tall but missing later feed items | The feed was not scrolled far enough, or capture ran before the next batch rendered. | Scroll repeatedly and wait for a known item count, new record, loading indicator, or end marker. |
| Scrolling the page does not load items | The list uses a nested scroll container. | Scroll the container using its scrollTop or hover it and use page.mouse.wheel(). |
| The wait times out even though content loaded | The chosen selector/count does not reflect the site’s rendering behavior, or the list is virtualized. | Inspect the DOM and wait for a stable application signal or specific target record. |
| Loop runs forever | The feed has no finite end, or the stop condition never becomes true. | Add a maximum item count, scroll-round limit, depth limit, or elapsed-time bound. |
| Images are blank or incomplete | Images are lazy-loaded or still decoding when the screenshot starts. | Scroll them into view and wait for the site’s image-ready condition before capture. |
| Screenshot differs between runs | Content, browser, operating system, headless mode, hardware, or rendering settings changed. | Keep the browser and machine environment consistent and wait for dynamic content to settle. Playwright documents these sources of visual variation in its visual comparisons guide. |
9. Or skip the browser setup
If you need a screenshot without building and maintaining the scrolling browser workflow, ScreenshotNeo provides a website screenshot API and MCP server. One GET request captures a URL as PNG, JPEG, WebP, or PDF. Its full-page capture loads lazy images, but an infinite feed still needs a finite capture target; a screenshot service cannot make an unbounded list finite.
For a standard page capture, see the ScreenshotNeo API documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, and failed loads are never billed. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan.
Sign up free for 1,000 screenshots a month, with no card required.
10. FAQ
Does fullPage: true trigger infinite scrolling?
No. It captures the scrollable content currently available. Scroll and wait for the content you need before calling it.
Can Playwright capture an infinite feed in one image?
Only a finite amount of loaded page content can fit in a screenshot. Choose a stopping rule, and account for virtualized lists that remove older items from the DOM.
Should I use a fixed timeout after scrolling?
A short fixed delay can be useful for a known page, but it is not a reliable general completion signal. Prefer a condition tied to the content or loading state.
How do I make visual screenshots repeatable?
Use the same browser and machine configuration, control dynamic content where possible, and wait for the page to settle. For Playwright Test assertions, toHaveScreenshot() waits for consecutive screenshots to stabilize before comparison; see the visual comparisons guide.
Sources: Playwright scrolling guide, screenshot guide, Page screenshot API, and visual comparisons guide. Selectors and load signals depend on the site; no behavior is universal across infinite-scroll implementations.


