How to Screenshot a Full Webpage in Chrome Using JavaScript
Use Puppeteer’s fullPage option to capture a whole page in Chrome. Learn when to use CDP or an extension, and how to handle lazy content and capture errors.
For JavaScript that controls Chrome, use Puppeteer and set fullPage: true:
await page.screenshot({ path: 'full-page.png', fullPage: true });
This captures beyond the visible viewport. It is browser automation code, not JavaScript to paste into a webpage console. If you need a Chrome extension, the built-in chrome.tabs.captureVisibleTab() captures only the visible viewport; if you need a one-off manual capture, Chrome DevTools has a Capture full size screenshot command.
1. Choose the right Chrome approach
| Approach | Best for | Full-page behavior | Key limitation |
|---|---|---|---|
| ScreenshotNeo | Capturing a URL without managing a browser | Website screenshot API; supports full-page capture and lazy image loading | Requires an API key and network request |
| Puppeteer | Node.js automation controlling Chrome or Chromium | page.screenshot({fullPage:true}) |
You manage the browser, navigation, and page-specific rendering behavior |
| Chrome DevTools Protocol (CDP) | Clients that need low-level Chrome commands | Page.captureScreenshot with captureBeyondViewport: true |
You manage the protocol session and returned base64 image |
| Chrome extension API | Capturing the active tab from an extension | chrome.tabs.captureVisibleTab() captures the visible area only |
Full-page capture requires a separate scrolling and stitching design |
| DevTools command | One-off manual capture | DevTools offers Capture full size screenshot | Not a JavaScript automation method |
Use Puppeteer for a script you can rerun, CDP when you already have a protocol client, and the extension API when the user’s active tab is the input. The Chrome extension API is not available to ordinary webpage JavaScript.
2. Capture a full page with Puppeteer
Install Puppeteer in a Node.js project:
npm install puppeteer
Save this as screenshot.mjs, then run node screenshot.mjs https://example.com. Puppeteer manages a compatible browser for the script.
import puppeteer from 'puppeteer';
const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 60_000
});
await page.screenshot({
path: 'full-page.png',
fullPage: true,
type: 'png'
});
} finally {
await browser.close();
}
networkidle0 waits for a period with no network connections, but it does not guarantee that lazy-loaded content or application-specific rendering has finished. Pages with long polling or continuously active requests may never reach that condition; in that case choose a different navigation wait condition and wait for a page-specific selector.
Screenshot options you are likely to need
fullPage: capture the full document rather than only the viewport.path: write the screenshot to a file. Omit it to receive image bytes from the API.type: choosepng,jpeg, orwebpwhere supported. PNG is lossless; JPEG and WebP can reduce output size. If you choose JPEG, use a quality value from 0 to 100.quality: controls JPEG or WebP image quality; it is not meaningful for PNG.clip: capture a specified rectangle instead of the full page. Do not combine a clip withfullPage.omitBackground: omit the default white background when a transparent output is useful; format support and page backgrounds affect the result.encoding: request a base64 string instead of a buffer when you need to transport the result as text.captureBeyondViewport: controls whether capture can extend outside the viewport in relevant screenshot modes. For a normal Puppeteer full-page capture,fullPageis the direct setting.
Check the Puppeteer API documentation for the options supported by the version installed in your project: Puppeteer ScreenshotOptions.
Wait for content that appears after scrolling
Full-page dimensions do not force every site to load content that is triggered by scrolling. If the page uses lazy images or scroll-triggered components, scroll through the document and wait for the content to render before capturing. This helper advances in increments and then returns to the top:
async function loadScrollTriggeredContent(page, step = 700, pauseMs = 250) {
await page.evaluate(async ({ step, pauseMs }) => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const doc = document.documentElement;
const maxY = Math.max(0, doc.scrollHeight - window.innerHeight);
for (let y = 0; y <= maxY; y += step) {
window.scrollTo(0, y);
await sleep(pauseMs);
}
window.scrollTo(0, 0);
await sleep(pauseMs);
}, { step, pauseMs });
}
await loadScrollTriggeredContent(page);
await page.screenshot({ path: 'full-page.png', fullPage: true });
This is a starting point, not a universal guarantee. A page may change height while loading; take a second measurement or use a page-specific completion signal when needed. Scrolling can also trigger animations, analytics, infinite feeds, or new network requests.
Capture a single element instead
To capture one element, wait for it and pass its handle to Puppeteer’s screenshot method:
const card = await page.waitForSelector('.report-card', { visible: true });
if (!card) throw new Error('Report card was not found');
await card.screenshot({ path: 'report-card.png' });
3. Use Chrome DevTools Protocol directly
CDP is the lower-level option when your automation already has a Chrome protocol session. Enable the Page domain as required by your client and call Page.captureScreenshot. The following illustrates the protocol payload; client represents the session object supplied by your CDP library.
const { data } = await client.send('Page.captureScreenshot', {
format: 'png',
captureBeyondViewport: true
});
const image = Buffer.from(data, 'base64');
await import('node:fs/promises').then(fs => fs.writeFile('full-page.png', image));
The protocol returns image data as base64. The captureBeyondViewport option defaults to false in the protocol reference, so set it to true when the capture must extend beyond the viewport. CDP also supports format, quality, and clip parameters; a clip describes a rectangular region and is useful for targeted captures. CDP does not by itself solve lazy loading or page readiness.
Reference: Chrome DevTools Protocol: Page.captureScreenshot.
4. What a Chrome extension can capture
The Chrome Tabs API method chrome.tabs.captureVisibleTab() captures the visible area of the currently active tab. It returns a Promise containing a data URL. It is intended for an extension context, such as an extension service worker or extension page, rather than arbitrary page JavaScript or a content script.
A minimal Manifest V3 permission declaration for a user-invoked capture can use activeTab:
{
"manifest_version": 3,
"name": "Visible Tab Capture",
"version": "1.0.0",
"permissions": ["activeTab"],
"background": { "service_worker": "service-worker.js" },
"action": { "default_title": "Capture visible tab" }
}
Call the API from the extension service worker after the user invokes the extension action:
chrome.action.onClicked.addListener(async (tab) => {
try {
const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, {
format: 'png'
});
console.log(dataUrl);
} catch (error) {
console.error('Visible-tab capture failed:', error);
}
});
activeTab grants temporary access in response to a user action. The alternative all_urls permission grants broader host access. Chrome documents a maximum of two captureVisibleTab() calls per second, so a scrolling-and-stitching implementation must account for the rate limit, page movement, fixed headers, and gaps or overlaps. See the Chrome Tabs API reference.
5. Handle full-page capture edge cases
- Lazy images and scroll-triggered content: The screenshot can include only content that has rendered. Scroll through the page, wait for images or selectors, and inspect the output.
- Sticky and fixed headers: A browser-level full-page capture and a viewport-by-viewport stitch can treat fixed elements differently. Stitching may repeat fixed UI in every segment. Test the chosen approach on the page layout.
- Nested scroll areas: Full-page capture follows the document, not necessarily the contents hidden inside an independently scrolling panel. Scroll that container separately if its hidden content matters.
- Frames: Cross-origin frames have browser security boundaries. A page screenshot may show rendered frame pixels, but reading or waiting on frame internals requires handling the frame through browser automation and its origin constraints.
- Animations and video: Capture timing can land between animation states. Disable animations with test CSS or wait for a stable application state when repeatable output matters.
- Infinite scrolling: Scrolling may keep adding content. Define a stopping rule, such as a known item count or maximum scroll depth, or the page can grow throughout the capture workflow.
- Very tall documents: Large images consume memory and can exceed browser or image-processing limits. Consider capturing sections, reducing device scale factor, or using JPEG/WebP when lossless output is unnecessary.
- Responsive layout: Set the viewport before navigation and capture. Width changes can alter wrapping, breakpoints, and total document height.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Only the viewport appears | fullPage is missing, false, or the API captures only the visible tab |
Use Puppeteer fullPage: true or CDP captureBeyondViewport: true. The extension method is viewport-only. |
chrome is not defined |
Extension API was run in a webpage console or Node.js | Run it from an extension service worker or extension page with the required permissions. |
| Images or sections are blank | Lazy loading or app rendering had not completed | Scroll through the document, wait for relevant selectors or images, and capture after the page-specific ready signal. |
| Navigation times out | Network never becomes idle, often due to long-lived requests | Use a less restrictive navigation wait condition and wait for a known selector or readiness signal. |
| Fixed header repeats in strips | Separate viewport images were stitched while the header remained fixed | Prefer a true full-page browser capture or hide the fixed element during each segment and verify seams. |
| Nested panel content is missing | The panel has its own scroll container | Scroll the panel itself and capture its element, or capture panel segments separately. |
| Capture is unexpectedly huge | Long document, large viewport, or high device scale factor | Reduce viewport or scale, choose a compressed format, or capture in sections. |
| Extension call fails | Missing permission, invalid window context, or rate limit | Use a user action with activeTab or grant the needed host permission, pass the correct window ID, and stay within two calls per second. |
| Output file is empty or unreadable | Capture failed before writing, or base64 data was handled as ordinary text | Check the awaited result and error path; decode CDP base64 into bytes before writing. |
7. Performance, reliability, and cost
Local Puppeteer avoids a per-capture API charge, but uses your machine’s CPU, memory, browser installation, and network connection. Full-page images grow with document height, viewport width, and device scale factor. Reuse a browser process for batches where appropriate, while creating isolated pages and closing them when finished. Add timeouts and error handling, and record the target URL and capture settings so failures can be reproduced.
Navigation-idle conditions can be slow or unreachable on some applications. Selector-based readiness is often more predictable when the page exposes a clear completion marker. For reliability, handle navigation failures, retry only transient errors with a limit, and avoid retrying indefinitely on pages that consistently block automation or require authentication.
The Chrome extension API is subject to Chrome’s documented two-calls-per-second limit for visible-tab captures. A scrolling-and-stitching extension adds delays and layout failure modes. ScreenshotNeo’s service pricing is listed below; the local browser routes above have no ScreenshotNeo usage fee.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its GET endpoint captures a URL as an image or PDF, including full-page captures with lazy images loaded. See the API documentation for supported parameters and formats.
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(async fs => {
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
});
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed; response headers say the page verdict and whether the capture was billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. 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.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
9. FAQ
Can I run Puppeteer code in the browser console?
No. Puppeteer is a Node.js browser automation library. Run it in a Node.js script that launches or connects to a browser.
Does Chrome’s extension API have a full-page flag?
No. captureVisibleTab() captures the visible area. A full-page extension must implement its own scrolling and image composition or use another capture mechanism.
Should I choose PNG or JPEG?
Use PNG when exact pixels and sharp text matter. JPEG or WebP can make large photographic captures smaller, with a quality tradeoff.
Does a full-page screenshot include content in every iframe or hidden panel?
It captures the rendered page surface, but hidden or not-yet-loaded content is not made visible automatically. Handle frame and nested-scroll behavior explicitly when that content is required.


