Full-Page Screenshot Algorithms
Learn native browser capture, scroll-and-stitch fallbacks, and production techniques for reliable full-page screenshots.
Direct answer: Prefer the browser’s native full-document screenshot capability when it is available. In Chromium, use the Chrome DevTools Protocol (CDP) Page.captureScreenshot method with captureBeyondViewport. In Puppeteer, use page.screenshot({ fullPage: true }). Firefox supports full-page capture in DevTools and through WebDriver BiDi. Use scroll-and-stitch only as a fallback for virtualized, lazily inserted, animated, or otherwise unstable pages that cannot be represented by one stable document bitmap.
1. What a full-page screenshot algorithm must do
A viewport screenshot contains only the pixels currently visible. A full-page capture includes content below and above that viewport, including sections that require scrolling. A reliable algorithm must:
- Choose a deterministic viewport width, height, and device scale factor.
- Wait for fonts, images, application data, and layout to settle.
- Account for lazy loading and virtualized content.
- Handle fixed and sticky elements without duplicating them on every tile.
- Preserve the intended responsive breakpoint, color scheme, locale, and timezone.
- Record browser and protocol versions so later captures are reproducible.
There are two main approaches:
| Approach | Strengths | Risks and limits |
|---|---|---|
| Native full-document capture | Simple, fast to operate, and avoids tile seams | Can mishandle virtualized lists, late DOM insertion, embedded frames, or browser-specific edge cases |
| Scroll-and-stitch | Works when a browser cannot produce a reliable single bitmap | More code; sticky UI, lazy loading, fractional offsets, animation, and layout shifts can create seams |
2. Native Chromium capture with CDP
The CDP Page domain exposes Page.captureScreenshot. Its options include a clip rectangle, png, jpeg, or webp format, JPEG quality, and captureBeyondViewport. The method returns base64-encoded image data. The protocol reference describes this option as “Capture the screenshot beyond the viewport.” See the Page domain reference.
Runnable Puppeteer implementation
Puppeteer provides a higher-level API over Chrome DevTools Protocol and WebDriver BiDi. Google documents full-page visual snapshots as a Puppeteer use case. Install it with npm install puppeteer.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.setViewport({
width: 1440,
height: 900,
deviceScaleFactor: 1
});
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 60000
});
await page.emulateMediaType('screen');
await page.evaluate(async () => {
if (document.fonts) await document.fonts.ready;
for (const image of Array.from(document.images)) {
if (!image.complete) {
await new Promise(resolve => {
image.addEventListener('load', resolve, {once: true});
image.addEventListener('error', resolve, {once: true});
});
}
}
document.documentElement.getBoundingClientRect();
});
await page.screenshot({
path: 'full-page.png',
fullPage: true,
type: 'png'
});
await browser.close();
})();
fullPage: true asks Puppeteer to capture the complete document. Keep Puppeteer and the browser version pinned in production: the CDP index warns that the tip-of-tree protocol changes frequently and has no backwards-compatibility guarantee.
Controlling format, quality, and clipping
await page.screenshot({
path: 'page.webp',
fullPage: true,
type: 'webp',
quality: 82
});
await page.screenshot({
path: 'region.png',
clip: {x: 0, y: 0, width: 1200, height: 1800},
type: 'png'
});
Use PNG when exact edges, text, or transparency matter. Use JPEG or WebP when file size matters. A clip rectangle is useful for a known document region, but it is not a substitute for waiting until the complete page has rendered.
3. A robust native-capture sequence
- Set the environment. Fix viewport dimensions, device scale factor, locale, timezone, color scheme, browser build, operating system, fonts, and motion settings.
- Navigate and wait. Wait for the application’s intended ready state, then wait for network activity, fonts, images, and application data.
- Trigger required content. If images load only after scrolling or an intersection observer fires, deliberately scroll through the document before the final capture.
- Freeze unstable visuals. Disable CSS transitions and animations where your test environment permits it. Pause carousels and video.
- Read layout. Force a layout read such as
document.documentElement.getBoundingClientRect()after content settles. - Capture natively. Use the browser’s full-document method and store metadata with the image.
- Validate. Exercise tall pages, lazy content, fixed headers, canvases, SVG, iframes, and responsive breakpoints.
4. Firefox and WebDriver BiDi
Firefox DevTools includes a full-page screenshot action and an element screenshot action; captures are saved to Downloads. Mozilla’s documentation says, “Use the screenshot icon … to take a full-page screenshot of the current page.” For automation, WebDriver BiDi defines browsingContext.captureScreenshot, including an option for the full scrollable page. See the Firefox screenshot documentation and the MDN BiDi reference.
Do not assume that a Firefox capture has the same pixels as Chromium. Mozilla documents that some automated paths re-render through a software drawSnapshot/CrossProcessPaint path instead of the WebRender compositor. For visual regression work, define whether you need DOM-rendered document output or the exact compositor framebuffer, then keep browser, OS, fonts, device scale, color profile, and motion settings fixed.
5. Scroll-and-stitch fallback
Use stitching when native capture clips content, mishandles embedded frames, or cannot represent a virtualized list in one stable layout. The algorithm is:
- Measure the viewport and document scroll height.
- Choose a scroll increment smaller than the viewport height so adjacent tiles overlap.
- At each step, record the CSS scroll offset and capture a viewport tile.
- Convert CSS offsets to image pixels using the device-pixel ratio.
- Place each tile on a canvas and remove the overlap.
- Mask or hide fixed and sticky elements so they are not repeated.
- Continue until the bottom of the document is covered, then crop to the measured document bounds.
Deterministic offsets are preferable. Feature-based image alignment can correct fractional scrolling or layout shifts, but it adds computation and can fail when content changes between tiles.
Stitching controls that prevent seams
- Overlap: Keep an overlap between neighboring tiles and discard duplicate edge regions.
- Sticky UI: Temporarily hide sticky headers, cookie bars, chat launchers, and other fixed elements, or mask their repeated regions before compositing.
- Lazy loading: Scroll deliberately, wait for newly visible resources, and capture only after layout stabilizes.
- Animation: Inject CSS that sets transition and animation durations to zero where acceptable.
- Scale: Convert every scroll offset with the same device-pixel ratio used for capture.
- Virtualized lists: Verify that off-screen records remain in the DOM or are rendered into a stable capture surface; otherwise stitching may capture the same rows repeatedly.
- Image limits: Very tall documents can exceed browser or image-library limits. Split the output into sections when the target environment cannot allocate one bitmap safely.
6. Cross-browser and page-feature comparison
| Concern | Native capture | Scroll-and-stitch |
|---|---|---|
| Browser coverage | Depends on each browser’s full-page API | Can work anywhere viewport screenshots and scrolling work |
| Fixed or sticky elements | Usually represented once by the browser’s document capture | Must be hidden or masked to avoid repetition |
| Lazy content | May be absent if it was never inserted | Can trigger loading tile by tile, with extra waits |
| Cross-origin frames | Browser implementation decides what is included | Frame boundaries and loading timing can produce gaps or mismatched tiles |
| Maximum dimensions | Bound by browser and image implementation | Bound by tile count, canvas allocation, and final bitmap limits |
| Visual fidelity | No seam alignment required | Seams and duplicated pixels are possible |
| Operational complexity | Lower | Higher |
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Bottom sections are blank | Lazy content never loaded | Scroll through the document, wait for resource and layout stabilization, then capture. |
| Header appears on every stitched tile | It is fixed or sticky | Hide it during capture or mask its repeated region before compositing. |
| Visible seams | Scroll offset, scale, or layout changed between tiles | Use deterministic increments, record CSS offsets, convert with one DPR, add overlap, and freeze animation. |
| Screenshot is shorter than the page | Virtualized content or late DOM insertion | Use a controlled scrolling pass; if the page cannot stabilize, capture a known viewport range or a rendered export. |
| Fonts or icons differ | Fonts were not ready or the environment changed | Await document.fonts.ready, install the same fonts, and pin browser and OS details. |
| Capture times out | Network never becomes idle or a request hangs | Use an application-specific ready selector, a bounded delay, and explicit request timeouts instead of waiting forever. |
| Firefox and Chromium images differ | Different layout engines or compositor paths | Compare within one pinned browser environment, or define a perceptual threshold across engines. |
8. Performance, reliability, and cost considerations
- Native capture generally requires fewer browser operations than stitching because it avoids repeated screenshots and compositing.
- Stitching cost grows with document height and the number of tiles. Keep overlap as small as reliability allows.
- Waiting for every network request can hang on analytics or streaming connections. Prefer a meaningful application-ready signal.
- Record URL, viewport, DPR, browser build, timestamp, capture method, and relevant wait settings beside every image.
- For visual regression, compare with an agreed pixel or perceptual threshold; do not treat an undocumented universal tolerance as correct.
- Measure speed, memory, maximum dimensions, and cross-browser behavior in your target environment. The available primary documentation does not establish universal benchmarks.
9. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. It accepts one GET request and returns PNG, JPEG, WebP, or PDF. See the ScreenshotNeo 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}`);
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
10. Implementation checklist
- Choose native full-document capture first.
- Pin compatible browser and protocol versions.
- Set deterministic viewport, DPR, locale, timezone, and color scheme.
- Wait for fonts, images, application data, and a stable layout.
- Exercise lazy content, fixed headers, canvases, SVG, iframes, and responsive breakpoints.
- Use overlap, masking, and duplicate-edge removal for stitching.
- Store capture metadata next to the output.
- Define whether the target is DOM output or compositor output.
11. FAQ
Should I always use scroll-and-stitch?
No. Native full-document capture is usually simpler and avoids seam alignment. Stitch only when native capture cannot represent the page reliably.
Does full-page mean the browser window becomes extremely tall?
Not necessarily. Native APIs can capture beyond the viewport without permanently resizing the visible window. Stitching captures normal-sized viewport tiles.
Why can two valid screenshots differ by a few pixels?
Browser version, fonts, device scale, color profile, animation timing, lazy loading, and compositor paths can all change raster output.
How do I capture only one component?
Use an element screenshot when your automation library supports it, or calculate the element’s bounding rectangle and pass a clip rectangle to the browser capture method.


