ScreenshotNeo

BlogHow-to

How to Capture a Website Screenshot After JavaScript Loads in Playwright

Wait for the page state your screenshot needs, then capture it with Playwright. See runnable examples, timing strategies, fixes, and visual regression guidance.

By the ScreenshotNeo team4 October 20267 min read

To capture a page after JavaScript has rendered the content you need, navigate with page.goto(), wait for an observable application state with a retrying Playwright assertion, then call page.screenshot(). A browser navigation event such as load is not always the same as the point when a client-rendered component or API-backed result is ready.

In a Playwright Test project, this is a complete example:

import { test, expect } from '@playwright/test';

test('captures the rendered dashboard', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
});

Replace the example URL and heading with the page and a visible element that genuinely indicates the content you want in the image. The assertion retries while waiting for the heading to become visible, up to the assertion timeout.

1. Choose the right readiness signal

page.goto() waits for the load event by default. That is a navigation lifecycle milestone, not a guarantee that every later JavaScript render, data request, or asynchronous component has finished. For a screenshot, decide what “ready” means for the particular page and wait for that outcome.

Wait strategy What it tells you When to use it
Default page.goto(url) The page reached the load event. Ordinary navigation is enough, or you will follow it with a page-specific assertion.
waitUntil: 'domcontentloaded' The initial document was parsed; it does not wait for all resources or application rendering. You explicitly need an early navigation milestone and will wait for the relevant content separately.
waitUntil: 'commit' The navigation response was received and loading began. You need to begin interacting as early as possible, with subsequent waits for actual page state.
waitUntil: 'networkidle' No network connections for at least 500 ms. Use only when that quiet period is specifically meaningful to your workflow. Playwright discourages it as a testing readiness check.
Web-first assertion A specific locator became visible, acquired expected text, or met another asserted condition. Usually the clearest choice for content-driven screenshots.

For example, waiting for a dashboard heading is more specific than waiting for network traffic to stop. A page can keep connections open or make background requests even when the visible content needed for the capture is ready. Conversely, an idle network does not prove that the expected UI state appeared.

2. Wait for the content with a web-first assertion

Playwright locators and web-first assertions retry until the condition passes or the assertion timeout expires. This makes them suitable for asynchronous rendering. Prefer an assertion describing the desired state over a one-time check such as isVisible(), which reports immediately.

import { test, expect } from '@playwright/test';

test('captures an asynchronously rendered report', async ({ page }) => {
  await page.goto('https://example.com/reports/monthly');

  const report = page.getByRole('heading', { name: 'Monthly report' });
  await expect(report).toBeVisible();

  await expect(page.getByTestId('report-status')).toHaveText('Ready');
  await page.screenshot({ path: 'monthly-report.png', fullPage: true });
});

Use the signal that matches the page, such as a heading, a status label, a result count, or a particular element becoming visible. If the page can show a placeholder before real content arrives, assert on the completed state or the real content rather than the placeholder.

Web-first assertions have a five-second default timeout in the documented configuration; configure the assertion timeout when the page legitimately takes longer. Keep the timeout appropriate to the application and environment. A longer timeout gives a slow page more time, but it does not make a missing state appear.

3. Capture the page or a stable visual baseline

For a one-off image, use page.screenshot(). Set fullPage: true when you need the full scrollable page; omit it for the current viewport. Check the Page API documentation for all supported screenshot options and defaults for the Playwright version installed in your project.

await page.screenshot({
  path: 'screenshot.png',
  fullPage: true
});

For visual regression in Playwright Test, use toHaveScreenshot(). It waits for two consecutive screenshots to match before comparing against the baseline:

import { test, expect } from '@playwright/test';

test('dashboard matches its visual baseline', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
  await expect(page).toHaveScreenshot('dashboard.png', { fullPage: true });
});

Keep the baseline and comparison environment consistent. Operating system, browser version, browser settings, hardware, power source, and headless mode can all affect rendered pixels. A difference can come from the environment as well as a product change.

4. A complete runnable setup

For a new Node.js project, install Playwright Test and its browser, then create a test file:

npm init playwright@latest
npx playwright test

Save the first example as tests/screenshot.spec.ts (or use JavaScript in a .js test file). Replace the example page and readiness locator with values from your site. Run the test with npx playwright test; the screenshot is written to the requested path relative to the test process working directory.

In an existing Playwright Test project, use its provided page fixture and import test and expect from @playwright/test. For visual comparisons, Playwright Test manages screenshot baselines through its screenshot assertion workflow.

5. When a load-state wait is actually needed

page.goto() accepts a waitUntil option. Use it to control which navigation milestone the call waits for, then assert the page state that matters:

await page.goto('https://example.com/app', { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('app-ready')).toBeVisible();
await page.screenshot({ path: 'app.png' });

The available documented milestones are commit, domcontentloaded, load, and networkidle. Most of the time, a separate waitForLoadState() call is unnecessary because Playwright auto-waits before actions. That auto-waiting does not mean a screenshot knows which application-specific asynchronous result you require; make that condition explicit.

6. Troubleshooting

Symptom Likely cause Fix
The screenshot shows a spinner or empty data area. The navigation milestone completed before the client-side render or data result. Assert on the expected content or a real ready status before capturing.
The assertion times out. The locator is wrong, the expected state never occurs, or the assertion timeout is too short for a legitimately slow page. Check the locator and page state, ensure the test reaches the expected page, and adjust the assertion timeout only when the real workflow needs more time.
networkidle never arrives. The page continues background requests or maintains connections. Wait for the content or state needed in the screenshot instead of relying on network quiet.
The screenshot differs between runs or machines. Rendering environment or browser configuration differs, or the page itself has changing content. Use a consistent OS, browser version, settings, hardware context, and headless mode; wait for stable application content before comparison.
The screenshot is only the visible viewport. fullPage was not enabled. Use page.screenshot({ path: 'screenshot.png', fullPage: true }) when the full page is required.
A visibility check passes too early or fails immediately. A one-time check such as isVisible() does not retry while asynchronous rendering proceeds. Use an awaited web-first assertion such as await expect(locator).toBeVisible().

7. Performance, reliability, and cost

Choose the earliest meaningful signal that proves the screenshot content is ready. Waiting for a broad lifecycle condition or a fixed pause can add latency without proving the relevant state. A locator assertion waits for the specific outcome and fails with a timeout if that outcome does not happen.

For repeated captures or visual regression, keep the browser and rendering environment consistent so comparisons remain interpretable. Treat external sites and live data as variable inputs: their content and timing can change between runs. No general wait strategy guarantees that an external page will always load successfully.

With Playwright, your own infrastructure runs the browser and incurs its compute and maintenance costs. For a hosted screenshot API alternative, ScreenshotNeo charges only for clean shots: bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing. Its response includes X-Page-Verdict and X-Billed headers indicating the result and billing status. Free includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Every feature is available on every plan.

Or skip the browser setup

ScreenshotNeo takes a screenshot with one GET request. See the ScreenshotNeo API documentation for options and response details.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, and failed loads are never billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. You get 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000.

Sign up free for ScreenshotNeo and make your first capture.

Frequently asked questions

Does page.screenshot() wait for my JavaScript application to finish?

Do not assume it knows when your application is ready. Wait for a locator or state that represents the content you need, then capture.

Should I always use networkidle?

No. It means there were no network connections for at least 500 ms, and Playwright discourages it as a general testing readiness signal. Prefer an assertion on the expected page content.

What should I use for a visual regression test?

Use Playwright Test’s expect(page).toHaveScreenshot() after waiting for the page-specific content. Keep the environment consistent between baseline generation and comparison.

Do I need a fixed sleep before every screenshot?

No. A retrying assertion on the desired state is generally clearer and more reliable than an arbitrary delay. Use a delay only when a real requirement cannot be represented by an observable state.

References