What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Short answer: Playwright’s normal page.goto() waits for the page’s load event, which includes dependent stylesheets and scripts. That is only the starting point. For JavaScript-rendered interfaces, wait for a page-specific “ready” condition; when typography matters, await document.fonts.ready immediately before the screenshot.

This sequence avoids the three common failures developers see: an unstyled capture, a page that shows a loading shell instead of its data, and text rendered in a fallback font. The correct wait is defined by what the page must visibly contain—not by an arbitrary delay or by assuming that every network request has ended.

The reliable Playwright sequence

Use navigation, an application-state assertion, and a font wait as separate steps:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 1
});

await page.goto('https://example.com/dashboard');

// Replace this with a condition that proves this application is ready.
await page.locator('[data-page-ready="true"]').waitFor();

await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'dashboard.png', fullPage: true });

await browser.close();

The selector is illustrative. Many sites do not expose data-page-ready. Use a result table, chart, heading, or completion marker that appears only after the data needed in the image has rendered. A locator wait also fails clearly when the application never reaches the expected state, instead of silently producing an incomplete image.

What page.goto() actually waits for

Playwright navigation defaults to waitUntil: 'load'. The browser fires load after dependent resources such as linked stylesheets, scripts, frames, and images have loaded. Therefore, a normal navigation is generally sufficient to start loading external CSS and JavaScript:

await page.goto(url, { waitUntil: 'load' });

Do not confuse that with “the application is finished.” Modern pages can execute JavaScript after load, fetch API data, hydrate server-rendered markup, insert components, or lazy-load images. Playwright’s navigation guidance explicitly notes that substantial activity can continue after the event.

domcontentloaded is earlier

domcontentloaded means the HTML document has been parsed. It is useful when you intentionally want an early snapshot, but it does not prove that external stylesheets, font files, images, or application-generated content are ready:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto(url, { waitUntil: 'domcontentloaded' });
// Only use this when an early, partially loaded capture is intentional.

Why networkidle is not a universal fix

Playwright defines networkidle as no network connections for at least 500 ms, but its Page API labels the state DISCOURAGED for testing and recommends web assertions instead. Analytics beacons, polling, advertisements, WebSockets, and lazy requests can keep a page active indefinitely—or stop briefly before the specific content you need is painted. Treat the 500 ms interval as an API definition, not as proof of visual completeness.

Wait for the rendered application state

The best readiness condition is observable and specific to the page. Select a stable element whose presence or state means the screenshot’s content is available.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Useful readiness assertions

// A results panel exists and is visible
await expect(page.locator('[data-testid="results"]')).toBeVisible();

// A loading indicator has disappeared
await expect(page.locator('[aria-busy="true"]')).toHaveCount(0);

// A known result has arrived
await expect(page.getByRole('heading', { name: 'Quarterly report' })).toBeVisible();

Import expect from @playwright/test for the assertion form. With the standalone library, locator.waitFor({ state: 'visible' }) is the equivalent. Prefer a marker controlled by the application over a random class name that a redesign may remove.

When the page has no readiness marker

Choose a meaningful DOM change: a non-empty table body, a particular navigation item, or a chart’s SVG. If the site exposes an API that determines completion, wait for the resulting UI rather than the request alone:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.waitForResponse(response =>
  response.url().includes('/api/report') && response.ok()
);
await expect(page.locator('#report-table tbody tr')).toHaveCount(10);

A response can succeed while rendering still fails, so retain the DOM assertion. If you control the application, add a stable completion attribute or test id specifically for automation.

Fixed delays: diagnostic, not a contract

A bounded delay can help diagnose a race or accommodate an animation:

await page.waitForTimeout(1000);

It is not a reliable general solution. A fast run wastes time; a slow run still captures too early. Replace it with a state assertion once you know what the page is waiting for.

External CSS: diagnose an unstyled screenshot

Because load includes linked stylesheets, an unstyled image usually indicates a failed request, a stylesheet that is injected later, blocked cross-origin access, or a capture taken before a client-side theme is applied.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Inspect requests and browser errors

page.on('requestfailed', request => {
  console.log('failed:', request.url(), request.failure()?.errorText);
});
page.on('console', message => console.log('console:', message.type(), message.text()));
page.on('pageerror', error => console.error('page error:', error));

await page.goto(url);

Open the CSS URL directly in a browser or fetch it from the same environment. Check DNS, TLS, authentication, content-security policy, redirects, and whether a proxy or firewall blocks the asset. A stylesheet can return HTTP 200 and still be unusable if its body is an HTML error page or malformed CSS.

Wait for a style-dependent signal

If a framework applies a class after hydration, wait for it:

await page.locator('html.theme-ready').waitFor();
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'styled.png' });

External JavaScript and dynamic content

Scripts referenced by the document are part of the normal load dependency chain, but their asynchronous work is not. Single-page applications commonly fetch data after startup. Capture only after the visible result exists and loading UI has gone.

await page.goto('https://example.com/app');
await page.locator('[role="progressbar"]').waitFor({ state: 'hidden' });
await page.locator('.invoice-list .invoice-row').first().waitFor({ state: 'visible' });
await page.screenshot({ path: 'invoices.png', fullPage: true });

If a script needs interaction, perform it before the readiness assertion:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
await page.getByRole('button', { name: 'Load report' }).click();
await page.getByRole('heading', { name: 'Report results' }).waitFor();

For infinite scrolling, explicitly scroll and wait for the next batch, or capture only the settled viewport. A full-page shot cannot include items the application has not yet rendered.

Web fonts: wait for the font promise

Use document.fonts.ready when the screenshot depends on web-font metrics:

await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'with-fonts.png' });

The promise resolves when loading and layout operations for the document’s used fonts have settled. It does not guarantee that every font declared in CSS was used or loaded. CSS can specify optional fonts, and a browser may legitimately retain a fallback for an unused face.

Understand the two external font requests

With a provider such as Google Fonts, the browser first downloads the provider’s CSS, then downloads a suitable font file format named by that CSS. A failure in either stage produces fallback typography. Inspect both requests, and verify that the requested weight and style actually exist. If your page uses weights 400 and 700, loading only 400 can make bold text appear synthetic or substituted.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prove which font is applied

const family = await page.locator('h1').evaluate(element =>
  getComputedStyle(element).fontFamily
);
console.log(family);

This reports the CSS family list, not a guarantee that the first face was downloaded. Compare a screenshot with the font request blocked, and check the browser’s computed style and console messages for a stronger diagnosis. Ensure the capture waits after any class or content change that alters font usage.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep screenshots comparable

Fix the viewport and scale settings across runs. Playwright distinguishes CSS-pixel rendering from device-pixel output through deviceScaleFactor and screenshot scale options. A changed viewport can trigger responsive breakpoints; a changed scale changes image dimensions and text rasterization. Record these settings alongside the URL and readiness condition when visual diffs matter.

const context = await browser.newContext({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 1
});

Use the same color scheme, locale, timezone, permissions, and authentication state for repeatable captures. If those values are part of the page’s output, set them explicitly in the browser context.

Complete reusable helper

import { chromium } from 'playwright';

export async function capture(url, readySelector, path) {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage({
      viewport: { width: 1440, height: 900 },
      deviceScaleFactor: 1
    });

    page.on('requestfailed', request =>
      console.warn('Request failed:', request.url(), request.failure()?.errorText)
    );

    await page.goto(url, { waitUntil: 'load', timeout: 60_000 });
    await page.locator(readySelector).waitFor({ state: 'visible', timeout: 30_000 });
    await page.evaluate(() => document.fonts.ready);
    await page.screenshot({ path, fullPage: true });
  } finally {
    await browser.close();
  }
}

await capture(
  'https://example.com/dashboard',
  '[data-testid="dashboard-ready"]',
  'dashboard.png'
);

Give navigation and readiness separate timeouts so a slow server and a missing application marker are distinguishable. Always close the browser in a finally block.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Troubleshooting checklist

  • Blank or partially blank page: check navigation errors, failed requests, redirects, authentication, and whether the page needs a longer application-specific wait.
  • HTML appears without styling: inspect stylesheet request failures, CSP and proxy rules; wait for the framework’s theme or hydration marker.
  • Fallback font: inspect both the provider CSS and font-file requests, verify CORS and requested weights, then await document.fonts.ready.
  • Data missing but navigation succeeded: wait for the rendered result, not merely load or an API response.
  • Timeout on networkidle: remove it and assert the page state you need; background polling may never stop.
  • Flaky visual diffs: fix viewport, device scale, locale, timezone, color scheme, authentication, animations, and readiness selectors.
  • Full-page image cuts off lazy content: scroll deliberately or use a page-specific mechanism that forces lazy items to render before capture.

Or skip the browser setup

ScreenshotNeo provides a single-call website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.

For a direct capture, see the ScreenshotNeo 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}`);

It also offers full-page and selector captures, device presets, retina scale, custom CSS and JavaScript, waits for selectors or network idle, request blocking, cookies and headers, PDFs, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and an MCP server with take_screenshot, get_page_info, and capture_pdf for AI clients. The Free plan includes 1,000 screenshots each month without a card; paid plans start at $5 for 3,000 shots. Sign up for the free plan.

Frequently Asked Questions

Does waiting for document.fonts.ready load every font in the stylesheet?

No. It covers loading and layout for fonts used by the document. Optional or unused faces may not load.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Should I use a longer timeout instead of a readiness selector?

No. Increase timeouts to accommodate a known slow environment, but still assert the rendered state required by the screenshot.

Does a successful API response mean the screenshot is ready?

Not necessarily. The response may arrive before the browser has rendered the resulting content, so pair request waits with a DOM assertion.

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.