Fix Website Screenshots That Capture a Partially Loaded Web App
A page reaching `load` does not mean its app is ready. Wait for the specific content you need, then capture it with Playwright or Puppeteer.
A browser reaching load does not prove that a web app has finished rendering the content you want to capture. The reliable fix is to wait for page-specific evidence—a visible component, expected text or data, or a relevant response followed by confirmation in the UI—and then take the screenshot.
In Playwright, use a locator assertion for the content that matters. In Puppeteer, wait for a selector or another app-specific condition before calling page.screenshot(). A fixed sleep and generic network quiet can both miss the real readiness condition.
Why screenshots capture a partially loaded app
Navigation states describe browser document milestones. Playwright exposes commit, domcontentloaded, load, and networkidle. These do not automatically tell you whether a single-page app has fetched its data, rendered a component, or completed client-side hydration. A page can have loaded its HTML and still be waiting on application work. [Playwright Page API]
Hydration is another source of confusing captures: the page may look visible while client-side code is still attaching behavior or updating content. Playwright’s navigation guide describes cases where clicks are ignored or values reset when hydration is incomplete. [Playwright navigation guide]
Other possibilities include a late API response, lazy-loaded images, animation, a failed request, or a capture that happens before a particular element appears. Do not assume which one is responsible. Observe the page at the moment the screenshot is saved and identify what is missing.
Choose a readiness condition that proves the target is present
| Wait strategy | What it tells you | When to use it |
|---|---|---|
domcontentloaded or load |
A document lifecycle milestone occurred. | Useful for navigation sequencing, but insufficient on its own when the app renders data afterward. |
networkidle |
In Playwright, no network connections for at least 500 ms. | Not a general app-readiness test. Playwright explicitly discourages using it for tests and recommends web assertions instead. [Playwright Page API] |
| App-specific locator assertion | The rendered element or expected value is present. | Usually the best fit: wait for the heading, data, or component the screenshot must show. |
| Relevant response, then UI assertion | A particular request completed, followed by confirmation the app rendered its result. | Use when a known response carries required data; the response alone does not prove rendering completed. |
| Fixed delay | Only that a chosen amount of time passed. | Useful for diagnosis in a pinch, but can waste time on fast pages and still be too short on slow ones. |
Playwright’s guidance is direct: “networkidle” is discouraged as a testing readiness signal; rely on web assertions instead. The documented 500 ms is the definition of that network state, not evidence that an app is ready after half a second. [Playwright Page API]
Fix it with Playwright
The example below navigates, waits for the page’s meaningful content, and saves a screenshot. Replace the URL and locator with a real route and a stable element on your page.
import { test, expect } from '@playwright/test';
test('captures the rendered dashboard', async ({ page }) => {
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
});
// Wait for application content, not merely document navigation.
await expect(page.getByRole('heading', { name: 'Dashboard' }))
.toBeVisible();
await expect(page.getByTestId('revenue-total'))
.toHaveText('$12,450');
await page.screenshot({ path: 'dashboard.png', fullPage: true });
});
Use a selector or assertion tied to the exact content in the image. A heading can prove the shell is present, but if the screenshot depends on a populated table or chart, also assert that data. Playwright’s web-first assertions retry while checking the condition, so they can finish as soon as it is true rather than sleeping for a fixed duration. See the PageAssertions API.
When a specific data request matters
If a particular API response is a reliable proxy for the required data, register the response wait before navigating or triggering the action. Then assert the rendered value before capturing. This handles both timing and the distinction between data arriving and data appearing onscreen.
import { test, expect } from '@playwright/test';
test('waits for dashboard data before capture', async ({ page }) => {
const dataResponse = page.waitForResponse(response =>
response.url().includes('/api/dashboard') &&
response.request().method() === 'GET' &&
response.ok()
);
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
});
await dataResponse;
await expect(page.getByTestId('revenue-total')).toHaveText('$12,450');
await page.screenshot({ path: 'dashboard.png' });
});
Match the actual endpoint and expected content for your app. If the request can succeed with an empty or partial result, the response is not enough; keep the UI assertion.
Visual regression screenshots
For a baseline comparison in Playwright Test, use toHaveScreenshot(). Its screenshot assertion waits for two consecutive screenshots to match before comparing with the baseline. This helps stabilize visual comparison after the app is ready; it does not replace an assertion that the intended content exists. [Playwright PageAssertions API]
import { test, expect } from '@playwright/test';
test('dashboard matches its visual baseline', async ({ page }) => {
await page.goto('https://example.com/dashboard');
await expect(page.getByTestId('revenue-total')).toHaveText('$12,450');
await expect(page).toHaveScreenshot('dashboard.png', { fullPage: true });
});
Fix it with Puppeteer
Puppeteer’s Page.screenshot() captures the current page. Wait for an app-specific selector before calling it. The selector below is illustrative; choose one that appears only when the content you need is rendered.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
});
await page.waitForSelector('[data-testid="dashboard-ready"]', {
visible: true,
timeout: 15000,
});
await page.waitForFunction(() =>
document.querySelector('[data-testid="revenue-total"]')?.textContent?.trim() === '$12,450'
);
await page.screenshot({ path: 'dashboard.png', fullPage: true });
} finally {
await browser.close();
}
Puppeteer’s screenshot guide demonstrates screenshot capture and includes a networkidle2 navigation example. Treat navigation options as sequencing tools, not proof that your app’s specific data is ready. [Puppeteer screenshot guide]
Or skip the browser setup
If you need a screenshot without maintaining browser automation, ScreenshotNeo is a website screenshot API and MCP server. Its one-call API returns an image or PDF; see the 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)
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}`);
- Cookie banners are accepted and removed before capture; ScreenshotNeo also removes known consent platforms, newsletter popups, and chat widgets. Each step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing. Response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdffor AI agents and 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 on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
Troubleshooting partially loaded captures
| Symptom | Likely cause to investigate | Next step |
|---|---|---|
| HTML or page shell appears, but data is absent | Application data is requested or rendered after the navigation milestone. | Wait for the relevant rendered value or component. If useful, wait for its API response too, then verify the UI. |
| Element exists but is invisible or empty | The app may have inserted a placeholder before completing render, or the locator may target the wrong element. | Assert visibility and expected text/value, and verify the selector against the live page. |
| Clicks are ignored or values change after capture | Hydration may still be in progress. | Wait for a page-specific signal that reflects completed client rendering, then check the content or interaction relevant to the capture. |
| Wait for response times out | The endpoint, method, timing, or status predicate may not match the real request; the request may also fail. | Inspect the page’s actual requests and response status. Register the wait before the action that triggers it. |
| Fixed timeout sometimes works, sometimes fails | Render time varies with load and environment. | Replace the duration with an assertion on the target content; retain a timeout as a failure bound. |
| Content is ready but screenshots still differ | Animation, fonts, images, viewport, browser, or platform may affect pixels. | Check these factors in the target environment. For full-page captures, investigate lazy-loaded content that may not have been brought into view. |
| Screenshot ends before a lower-page image appears | The image may load lazily only when scrolled near the viewport. | Scroll the relevant region into view and wait for the image to load before capturing; verify the resulting screenshot. |
Performance, reliability, and cost
A condition-based wait can return as soon as the target is ready. A fixed delay always spends its full duration and remains unreliable when conditions change. Network quiet may never happen on pages with polling, analytics, or long-lived connections, and can happen before the particular UI has rendered; Playwright cautions against treating it as test readiness. [Playwright Page API]
Set a bounded timeout so a missing condition produces a useful failure instead of an indefinitely stalled job. Keep the condition specific enough to avoid false readiness, but stable across normal content changes. In screenshot pipelines, record which assertion failed and capture the relevant page state or logs so intermittent cases can be diagnosed. A stable screenshot assertion helps visual comparison but adds no semantic guarantee about which content should be present.
Browser automation carries the work of launching and maintaining a browser environment. For repeated captures, the right cost comparison depends on volume, infrastructure, and how much page-specific behavior you need. ScreenshotNeo’s listed plans are Free for 1,000 shots per month, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Its billing rules exclude bot checks, blank pages, timeouts, failed loads, and cache hits. These are product plan facts, not a performance benchmark.
FAQ
Does waitUntil: 'load' mean the page is ready for a screenshot?
No. It marks a document lifecycle event. App data and client rendering can continue afterward, so wait for the content the screenshot needs.
Should I use networkidle?
Not as a universal readiness test. Playwright defines it as at least 500 ms without network connections and discourages it for tests; use an app-specific assertion. [Playwright Page API]
What should the readiness locator target?
Prefer a visible, stable element or expected value that directly represents the content in the screenshot, such as the populated total or a rendered results row.
Can a screenshot assertion replace a content assertion?
No. A visual assertion can wait for consecutive images to stabilize for comparison, but you should separately verify the intended app content is present. [Playwright PageAssertions API]


