Infinite scroll is a page pattern that loads and appends the next batch of content when the visitor scrolls near the bottom, so the page has no fixed end and no page numbers.
Social feeds, search results, image galleries and product listings use it. This entry is part of the screenshot and rendering glossary. It is related to lazy loading, but the difference matters for captures: lazy loading fills in content that is already in the page, while infinite scroll adds new content and makes the page taller.
A plain explanation
A paginated list has a "next page" link. An infinite list replaces the link with a trigger near the bottom. When the trigger comes into view, a script fetches the next batch and appends it. The new items push the trigger down again, and the cycle repeats as long as the visitor scrolls and the server has items.
For a person this feels smooth. For a capture it means two things. The page is only as long as the scrolling that happened before the capture. And if the feed never ends, "capture the whole page" has no answer. You have to decide how much to capture.
How the page grows
We built a feed that appends 10 posts (172 px each) whenever a sentinel element at the bottom comes within 400 px of the viewport. The server never runs out of posts. Then we measured the page with Puppeteer at an 800 x 600 viewport.
Three observations:
- The first load already fetched twice. The sentinel starts near the top of an empty feed, so it fired once before the first batch arrived and once more after. The page opened with 20 items, not 10.
- A full-page screenshot changed nothing. It drew 3,453 px and the page stayed at 20 items, because the capture does not scroll.
- Growth was steady and had no end. About 10 items every 2 to 3 steps. Our loop stopped only because of the 12,000 px limit we set.
How it works technically
Most current feeds use an IntersectionObserver on a sentinel element below the last item, with a rootMargin so the fetch starts before the visitor reaches the end. Older feeds listen to the scroll event and compare scrollY + innerHeight with the page height. Some use a "load more" button after a few batches, and some remove items from the top as the list grows (list virtualization), so a tall capture can still miss the first posts.
For automation, three facts follow. The trigger only fires when the viewport moves, so you must scroll. Each batch is a network request, so you must pause between steps. And you need your own stop rule: a height limit, a step limit, or "the height did not grow and we are at the bottom".
How to capture it in Puppeteer, Playwright and RenderScreenshot
Puppeteer. Scroll one viewport at a time, stop on a limit or at the real end, then capture a fixed height.
import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const MAX_STEPS = 15; // a feed never ends, so cap the work const MAX_HEIGHT = 12_000; // and the image height const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const browser = await puppeteer.launch(); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto(url, { waitUntil: 'networkidle0' }); let height = await page.evaluate(() => document.documentElement.scrollHeight); for (let step = 0; step < MAX_STEPS && height < MAX_HEIGHT; step++) { await page.evaluate(() => window.scrollBy(0, window.innerHeight)); await sleep(400); // time for the fetch and the new items const next = await page.evaluate(() => document.documentElement.scrollHeight); // No growth and already at the bottom: the feed has stopped loading. const atBottom = await page.evaluate(() => window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2); if (next === height && atBottom) break; height = next; } await page.evaluate(() => window.scrollTo(0, 0)); await page.screenshot({ path: 'feed.png', clip: { x: 0, y: 0, width: 1280, height: Math.min(height, MAX_HEIGHT) }, captureBeyondViewport: true, }); console.log('captured height', Math.min(height, MAX_HEIGHT)); } finally { await browser.close(); }
Playwright. The mouse wheel sends real wheel events, which some feeds listen for.
import { chromium } from 'playwright'; const url = process.argv[2] ?? 'https://example.com'; const MAX_STEPS = 15; const MAX_HEIGHT = 12_000; const browser = await chromium.launch(); try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 } }); await page.goto(url, { waitUntil: 'networkidle' }); let height = await page.evaluate(() => document.documentElement.scrollHeight); for (let step = 0; step < MAX_STEPS && height < MAX_HEIGHT; step++) { await page.mouse.wheel(0, 800); // real wheel events, which some feeds listen for await page.waitForTimeout(400); const { next, atBottom } = await page.evaluate(() => ({ next: document.documentElement.scrollHeight, atBottom: window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2, })); if (next === height && atBottom) break; height = next; } await page.evaluate(() => window.scrollTo(0, 0)); // Playwright has no captureBeyondViewport switch; a clip larger than the viewport works with fullPage. await page.screenshot({ path: 'feed-pw.png', fullPage: true, clip: { x: 0, y: 0, width: 1280, height: Math.min(height, MAX_HEIGHT) } }); console.log('page height', height); } finally { await browser.close(); }
RenderScreenshot. Turn on capture.scroll.simulate for a full-page capture, and set scroll.max_height as the stop rule (default 16,000 px). scroll.step and scroll.delay control the distance and the pause. See capture options.
curl -X POST https://api.renderscreenshot.com/v1/screenshot \ -H "Authorization: Bearer $RS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/feed", "capture": { "mode": "full_page", "scroll": { "simulate": true, "step": 800, "delay": 500, "max_height": 12000 } } }' \ --output feed.png
Tested on 2026-10-15 with Node 22.18.0, Puppeteer 25.14.0 (Chrome 155) and Playwright 1.64.1 (Chromium 156), against https://example.com and the local test feed. Both scripts stopped at the 12,000 px cap on the feed and after one step on example.com. The RenderScreenshot request follows the API docs and was not run for this page.
Common mistakes
- No stop rule. A loop that scrolls "until the end" runs until the timeout on a feed that never ends.
- One jump to the bottom. It skips the middle and triggers only one batch per jump.
- No pause. The fetch needs time. Without a pause, the height check sees no growth and stops early.
- Huge images. A 50,000 px PNG is slow to encode and hard to view. Cap the height, or split it into several images.
Related terms
- Lazy loading: fills content in place; it does not add height.
- IntersectionObserver: the API most feeds use for the trigger.
- Full-page screenshot: what a capture of the whole page means.
The deep guide, with bounded capture rules and virtualized lists, is screenshotting infinite scroll pages. To capture feeds with a height cap built in, get a free API key.