Puppeteer fullPage Screenshot Cuts Off Content: How to Fix It
Fix Puppeteer full-page screenshots that stop early or miss lower-page content. Diagnose clipping, lazy loading, nested scroll areas, layout, and version issues.
If a Puppeteer full-page screenshot stops at the viewport or leaves the bottom blank, first confirm that you are calling page.screenshot({ fullPage: true }) on the intended page and that no clip or element screenshot is restricting the capture. Then check whether the missing content has rendered, whether a nested element owns scrolling, and whether viewport-dependent CSS or your Puppeteer/Chromium version affects the result. fullPage captures the page’s rendered extent; it does not force lazy content to load or expand a nested scroll container.
Puppeteer’s ScreenshotOptions reference defines fullPage, clip, and captureBeyondViewport as separate options. Its screenshot guide shows the Page.screenshot() workflow.
1. Start with the screenshot call
Use an explicit full-page option and remove any inherited clip while diagnosing. A clip is a rectangle that limits the captured region. An element screenshot is a different operation and may not represent the whole document.
await page.screenshot({
path: 'capture.png',
fullPage: true,
});
Check wrapper functions and shared configuration too: the screenshot options may be assembled in another module. The current reference says captureBeyondViewport defaults to false when there is no clip and true when a clip is specified. Do not add it blindly: first remove unintended clipping and verify your installed versions.
2. Run a complete diagnostic capture
This runnable Node.js example records dimensions and saves both a normal viewport shot and a full-page shot. It uses a real URL you can replace. Install Puppeteer with npm install puppeteer, save this as capture.mjs, then run node capture.mjs.
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.setViewport({ width: 1365, height: 900, deviceScaleFactor: 1 });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
// Replace this with a selector that means the important content is ready.
await page.waitForSelector('body', { timeout: 15000 });
const before = await page.evaluate(() => ({
url: location.href,
viewport: { width: innerWidth, height: innerHeight },
document: {
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
bodyScrollHeight: document.body.scrollHeight,
},
scrollables: [...document.querySelectorAll('*')]
.filter(el => el.scrollHeight > el.clientHeight + 2)
.slice(0, 20)
.map(el => ({
tag: el.tagName,
id: el.id,
className: typeof el.className === 'string' ? el.className : '',
clientHeight: el.clientHeight,
scrollHeight: el.scrollHeight,
overflowY: getComputedStyle(el).overflowY,
})),
}));
console.log(JSON.stringify(before, null, 2));
await page.screenshot({ path: 'viewport.png' });
await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
await browser.close();
}
Interpret the output before changing the viewport. If document height is only the viewport height but a listed element has a larger scrollHeight, that element may own scrolling. If document height is large yet content is blank in the output, investigate rendering and loading state rather than screenshot dimensions.
3. Make sure below-the-fold content has rendered
Navigation reaching domcontentloaded or a network-idle state does not prove every application component or lazy image is ready. Many sites load content only when its region approaches the viewport. Scroll through the document in steps, wait for the particular content your page needs, then confirm image and component state before the final capture.
async function scrollDocumentToTriggerLazyContent(page) {
await page.evaluate(async () => {
const step = Math.max(300, Math.floor(innerHeight * 0.75));
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 120));
}
window.scrollTo(0, 0);
});
}
await scrollDocumentToTriggerLazyContent(page);
// Wait for images that are present in the DOM to finish loading or fail.
await page.waitForFunction(() =>
[...document.images].every(img => img.complete),
{ timeout: 20000 },
);
await page.screenshot({ path: 'full-page.png', fullPage: true });
The scroll-and-wait values are starting points, not guarantees. A page may append new sections while scrolling, use an application-specific loading signal, or keep images in an iframe. Prefer a meaningful readiness condition such as a known selector, a completed state attribute, or an expected section count. For images, complete includes failed loads; if image success matters, also inspect naturalWidth. Cross-origin frames may need separate handling and cannot be assumed ready because the top-level document is ready.
4. Find the element that actually scrolls
Look at the page’s scrollbar and the diagnostic list. A page can have a fixed-height shell with overflow: auto or overflow: scroll, while the document itself remains about one viewport tall. In that case, full-page capture follows the document’s dimensions and may not include all the content reachable by scrolling the shell.
When you identify a scroll container, trigger its lazy content explicitly. For example, replace .results-pane with the page’s actual selector:
const container = await page.$('.results-pane');
if (!container) throw new Error('Scroll container not found');
await page.evaluate(async (el) => {
const step = Math.max(250, Math.floor(el.clientHeight * 0.8));
for (let y = 0; y < el.scrollHeight; y += step) {
el.scrollTop = y;
await new Promise(resolve => setTimeout(resolve, 120));
}
el.scrollTop = 0;
}, container);
// Capture the container when that is the desired target.
await container.screenshot({ path: 'results-pane.png' });
Element capture is suitable when the desired output is that element. It is not equivalent to a whole-document screenshot. If the requirement is a single tall image of the container’s contents, inspect whether its content can be rendered into a document-sized region without changing the layout, or capture overlapping segments and stitch them in a separate image-processing step. Fixed and sticky descendants can appear repeatedly in stitched segments, so verify the output.
5. Keep viewport-dependent CSS stable
Do not resize the viewport to the page’s full height as the first fix. CSS such as 100vh, vw, responsive breakpoints, sticky positioning, and fixed headers can change layout when dimensions change. Keep the intended viewport width and height, compare computed document dimensions before and after capture, and test any resize workaround on the affected page.
- Set the same viewport dimensions for each reproduction.
- Check for
height: 100vh,position: fixed, sticky headers, and responsive rules. - Compare the top, middle, and bottom of the output with the rendered page.
- Check whether a fixed overlay or banner covers content rather than the screenshot being physically clipped.
6. Check versions and historical Chromium workarounds
Puppeteer’s screenshot behavior depends on the Puppeteer and browser versions in use. Record both versions and reproduce with a small page before applying a workaround from an old issue thread. A historical Puppeteer discussion mentions launching Chromium with --blink-settings=mainFrameClipsContent=false for a clipping case. Treat that flag as a version-specific experiment, not a default or guaranteed current fix.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
args: ['--blink-settings=mainFrameClipsContent=false'],
});
console.log('Puppeteer package version:',
(await import('puppeteer/package.json', { with: { type: 'json' } })).default.version);
console.log('Browser version:', await browser.version());
await browser.close();
If your Node version does not support JSON module imports as shown, read the installed package version from your package manager or lockfile. Compare captures with and without the flag using the same URL, viewport, and page state. Remove the flag if it does not change the result.
7. Screenshot options that matter for clipping
| Option | Use | Clipping note |
|---|---|---|
fullPage |
Capture the full page instead of only the viewport. | Set explicitly to true for a full-document shot. |
clip |
Capture a specified rectangular region. | Remove it when diagnosing an unexpected crop. |
captureBeyondViewport |
Controls capture outside viewport bounds. | Its documented default depends on whether clip is set; check the installed Puppeteer reference. |
type, path |
Choose image format and output path. | They do not make missing page content render. |
quality |
Set lossy image quality where supported. | Does not alter capture height; not applicable to PNG. |
omitBackground |
Hide the default background for transparency. | Does not expand the page. |
optimizeForSpeed |
Request speed-oriented screenshot handling. | Does not address loading, scroll ownership, or clipping itself. |
For a full-page PNG, { path: 'capture.png', fullPage: true } is usually the clearest starting point. Use a clip only when a crop is intentional.
8. Troubleshooting by symptom
| Symptom | Likely cause | Fix |
|---|---|---|
| Image ends exactly at viewport bottom | fullPage is missing or false, or an element/clip capture is used. |
Use page screenshot with fullPage: true; inspect inherited options. |
| Only a particular rectangle is present | A clip option constrains the output. |
Remove it and retest. |
| Page is tall but lower areas are white or incomplete | Lazy loading or app rendering has not finished. | Scroll to trigger loading, wait for the relevant selector/state, and check failed resources. |
| Document height is short while an inner panel scrolls | A nested overflow container owns the scroll. | Scroll that element and capture the intended element or construct an appropriate document-level view. |
| Layout changes or sections overlap after a resize | Viewport-relative CSS, breakpoints, fixed, or sticky positioning. | Preserve intended viewport dimensions and inspect computed styles. |
| Behavior differs after an upgrade or only in deployment | Puppeteer/Chromium versions or launch configuration differ. | Log both versions and flags; reproduce the same page state locally and in deployment. |
| Bottom is missing despite network idle | Network idle did not represent application readiness or lazy content state. | Wait on a specific DOM condition; avoid treating a generic delay as proof. |
| Some image slots are empty | Image request failed, frame restrictions apply, or the image has not loaded. | Inspect complete, naturalWidth, network errors, and frame contents. |
9. Performance, reliability, and cost
A full-page image can be much taller and larger than a viewport capture, so it takes more memory to encode and move through your pipeline. Very long pages may be unsuitable as one bitmap for downstream consumers. Capture only the needed page or element, choose JPEG/WebP when transparency is unnecessary and your workflow supports them, or split extremely tall content into verified sections. Lossy quality settings apply to supported lossy formats, not PNG.
Reliability improves when the capture waits for a page-specific condition instead of relying on a fixed sleep. Record the final URL, viewport, document dimensions, scroll owner, screenshot options, and browser version with failed captures. For repeatable jobs, pin a Puppeteer version and use its corresponding browser installation. There is no universal wait duration or browser flag that fixes every page.
Self-hosted Puppeteer has no per-screenshot API fee, but your runtime, browser processes, memory, storage, and maintenance have costs. Estimate capacity using the actual pages and concurrency you expect rather than assuming every full-page capture has the same cost. If you use a hosted screenshot service, compare its billing rules and features for your workload.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a screenshot or PDF. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. 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,
)
r.raise_for_status()
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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Does fullPage: true scroll the page automatically?
It requests a full-page capture, but it does not guarantee every lazy-loaded or asynchronously rendered component has appeared. Trigger and verify those states first.
Should I set captureBeyondViewport: true?
Not as a universal fix. Check whether a clip is present and consult the ScreenshotOptions reference for the Puppeteer version you actually run.
Can Puppeteer capture an element instead?
Yes. Element capture targets that element and is useful for a panel or component. It does not mean the same thing as capturing the whole document.
What details help diagnose a bug report?
Include a minimal reproducible URL or page, screenshot call and options, Puppeteer and browser versions, viewport dimensions, and whether the document or a nested element scrolls.


