Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Use Puppeteer’s page.screenshot({ fullPage: true }) after your SPA has reached a known rendered state. A reliable capture fixes the viewport, waits for navigation and an app-owned readiness signal, loads content that appears only after scrolling, then saves the image and closes Chromium even when a job fails.
Complete Puppeteer example
Install Puppeteer in a Node.js project, then run this script. Replace the URL and readiness selector with values from your application.
npm install puppeteer
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.goto('https://example.com/app', {
waitUntil: 'networkidle2',
timeout: 60000
});
await page.waitForSelector('[data-app-ready="true"]', {
visible: true,
timeout: 30000
});
await page.screenshot({
path: 'spa-full-page.png',
fullPage: true
});
} finally {
await browser.close();
}
fullPage: true tells Puppeteer to capture the complete document rather than only the current viewport. The viewport is set explicitly because responsive breakpoints can change the page’s layout, number of columns and even which components render. A fixed deviceScaleFactor makes output dimensions and text rasterization more repeatable.
The selector in the example is deliberately application-specific. Add an attribute such as data-app-ready="true" when your app has fetched its data and rendered the state you want captured. If your app cannot expose a selector, use a JavaScript predicate instead.
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
Why SPAs often produce incomplete screenshots
Navigation completion is not UI completion
Single-page apps commonly load a shell first and fetch route data afterward. waitUntil: 'networkidle2' waits for a period with no more than two active connections, but analytics, WebSockets, polling and other background traffic can keep a page busy. Conversely, a brief quiet period can occur before the meaningful component renders. Treat network idle as a useful lower bound, not proof that the screen is ready.
Lazy content is not in the document yet
Images, cards and sections may be inserted only when they approach the viewport. A full-page capture cannot include elements that the app has not created. Trigger those sections deliberately, wait for their content, and then capture.
Animations can change pixels during capture
Transitions, carousels and blinking carets make visual comparisons unreliable. In test builds, disable motion with a stylesheet or an app setting; otherwise wait until the relevant animation has ended. The exact CSS or flag is application-specific.
Choose the right readiness wait
Use a stable selector for an explicit app state
A marker rendered only after data and critical components are ready is usually the most maintainable approach.
await page.waitForSelector('#dashboard[data-state="loaded"]', {
visible: true,
timeout: 30000
});
Use waitForFunction for a state flag
If the application sets a global flag, wait for that predicate rather than guessing a delay.
Rank #2
await page.waitForFunction(
() => window.__APP_READY__ === true,
{ timeout: 30000 }
);
Use a delay only for a known, bounded transition
page.waitForTimeout() can cover a short animation, but it does not verify that data arrived. Prefer a selector or predicate whenever possible, and keep any delay small and documented.
When network-idle is appropriate
networkidle0 requires zero active connections and can be useful for a static route with no polling. networkidle2 is less strict and often better for ordinary pages. For an SPA, combine one of them with an app-owned readiness check.
Loading lazy sections before capture
Scroll in controlled increments so intersection observers request deferred content. Wait between increments and, when possible, wait for a marker indicating that the newly exposed section is populated.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
async function loadLazyContent(page) {
await page.evaluate(async () => {
await new Promise(resolve => {
let y = 0;
const step = Math.max(window.innerHeight * 0.8, 400);
const timer = setInterval(() => {
window.scrollBy(0, step);
y += step;
if (y >= document.body.scrollHeight) {
clearInterval(timer);
resolve();
}
}, 150);
});
});
await page.waitForFunction(
() => [...document.images].every(img => img.complete),
{ timeout: 30000 }
);
await page.evaluate(() => window.scrollTo(0, 0));
}
await loadLazyContent(page);
await page.screenshot({ path: 'spa-full-page.png', fullPage: true });
The image-complete check confirms that browser image requests finished; it does not prove that every framework component has completed. Add a selector or predicate for sections whose data arrives through JavaScript. Some virtualized lists intentionally remove off-screen rows, in which case a single full-document screenshot cannot contain every row; render a non-virtualized print route or capture segments instead.
Viewport, scale and deterministic output
- Viewport width: choose the width your users or visual tests target. A different width can activate a mobile layout.
- Viewport height: it affects what loads initially and which intersection observers fire.
- Device scale factor: use
1for predictable CSS-pixel output or a higher value when you need retina-like detail. - Fonts: wait for web fonts before capture when text metrics matter:
await page.evaluate(() => document.fonts.ready). - Color scheme and locale: configure them explicitly if the app changes for dark mode, timezone or language.
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'light' }
]);
await page.evaluate(() => document.fonts.ready);
Use a consistent Chromium version in CI and local development when pixel-level diffs are important. Save screenshots with a unique name per job so parallel runs do not overwrite one another.
Full page, viewport, element or PDF?
| Goal | Puppeteer method | Result |
|---|---|---|
| Visible screen only | page.screenshot() |
Current viewport |
| Entire document | page.screenshot({ fullPage: true }) |
Raster image of the document |
| One rendered component | elementHandle.screenshot() |
Raster image of that element |
| Printable pages | page.pdf() |
PDF using print CSS by default |
Choose page.pdf() only when a PDF is the required output. Print CSS, page breaks and margins make it a different rendering path from a PNG, JPEG or WebP screenshot.
Production reliability and failure handling
Use layered timeouts
Set a navigation timeout long enough for your environment, selector timeouts that reflect backend latency, and an overall job deadline in your worker. A single unbounded wait can exhaust a queue when a site is down.
Always close the browser
Put capture code in a try/finally block. Closing the browser releases Chromium processes and temporary resources after both successful and failed jobs.
Record what happened
Log the URL, viewport, browser version, elapsed time and the readiness step that failed. Keep the HTML or a diagnostic screenshot when permitted by your data policy. These details distinguish an app regression from a network failure.
Control external dependencies
Third-party ads, trackers and chat scripts can delay readiness or alter layout. In a controlled test environment, mock APIs or block nonessential requests. Do not hide required application requests merely to make a screenshot pass.
Rank #4
Troubleshooting common failures
The screenshot stops at the first viewport
Check that the options object contains fullPage: true and that the document, rather than a fixed-height scrolling container, owns the content. If the app scrolls inside a div, capture that element or temporarily expand it for a print route.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteCards or images below the fold are missing
The app is probably lazy-loading them. Run the controlled scroll routine, wait for section-specific markers and verify that virtualization is not discarding off-screen nodes.
waitForSelector times out
Confirm the selector in the browser’s final DOM, including route changes and shadow DOM boundaries. Increase the timeout only after checking that the marker is actually emitted on error states. For shadow roots, expose a test hook or query inside the component from page context.
networkidle2 never resolves
Long polling, WebSockets or analytics may keep connections open. Navigate with waitUntil: 'domcontentloaded' and then wait for your app’s selector or predicate. Keep a separate overall timeout.
The page is blank or shows a bot challenge
Verify the URL, authentication and required headers, then inspect the response and console logs. A challenge may require an approved test environment; do not attempt to bypass access controls.
Recommended Free Tools
Best Value
- Used Book in Good Condition
Text shifts between runs
Wait for document.fonts.ready, use a fixed viewport and browser version, freeze time or data where your test permits, and disable animations. Differences in remote content can still produce legitimate pixel changes.
Chromium fails to launch in CI
Install the browser revision that your Puppeteer version expects, ensure the runner has required system libraries, and avoid sharing one browser process across unrelated jobs unless you manage isolation and cleanup.
Or skip the browser setup
ScreenshotNeo provides a hosted screenshot API when you do not want to maintain Chromium. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers report the page verdict and billing result.
One GET request returns PNG, JPEG, WebP or PDF. The API supports full-page and element captures, device presets or custom viewports, retina scale, custom CSS and JavaScript, clicks, selector or network-idle waits, lazy-image loading, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, selectable-TTL caching, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage data and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info and capture_pdf to Claude, Cursor and other MCP clients.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →See the ScreenshotNeo documentation for the current parameters. This call saves a WebP response:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/app -o shot.webp
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, and yearly billing gives two months free. Create a free ScreenshotNeo account.
Equivalent calls in Python and Node.js
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/app"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/app' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Frequently Asked Questions
Can Puppeteer capture a page inside an iframe?
Yes, obtain the target frame with page.frames() and query its DOM. Cross-origin restrictions still apply to what browser JavaScript can inspect.
What does fullPage do with a very tall document?
Puppeteer asks Chromium to render the document’s full layout. Extremely tall pages can consume substantial memory; split captures or provide a print-specific route when necessary.
Should I reuse one browser for many screenshots?
A shared browser can reduce launch overhead, but isolate pages, clear state and enforce per-job timeouts. Launch separate browser processes when stronger fault isolation is more important than throughput.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




