ScreenshotNeo

BlogHow-to

How to Capture Website Thumbnails with Playwright When Pages Load Slowly

Capture reliable thumbnails from slow pages by waiting for the visual content you need, then choosing viewport, full-page, or element capture.

By the ScreenshotNeo team4 October 20268 min read

For a slow page, separate navigation from visual readiness: choose when navigation may continue, then wait for the specific heading, image, or component that must appear in the thumbnail. Capture only after that target is ready. A page lifecycle event does not guarantee that client-rendered content has appeared. Playwright documents the navigation wait conditions and locator waiting APIs.

Runnable JavaScript example

This example uses Playwright’s JavaScript API. Install Playwright and its Chromium browser, set TARGET_URL to the page to capture, and run it with Node.js:

npm install playwright
npx playwright install chromium
TARGET_URL="https://example.com" node thumbnail.mjs

Save the following as thumbnail.mjs. Replace the heading locator with a signal that actually indicates the desired thumbnail content is visible on your target site.

import { chromium } from 'playwright';

const url = process.env.TARGET_URL;
if (!url) throw new Error('Set TARGET_URL to the page to capture.');

const browser = await chromium.launch();
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
  page.setDefaultNavigationTimeout(30_000);
  page.setDefaultTimeout(15_000);

  // domcontentloaded is an early navigation milestone, not proof that
  // the page's asynchronously rendered thumbnail content is ready.
  const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
  if (!response?.ok()) {
    throw new Error(`Navigation did not return a successful response: ${response?.status() ?? 'no response'}`);
  }

  // Use a meaningful, site-specific readiness signal.
  const heading = page.getByRole('heading', { name: /expected page title/i });
  await heading.waitFor({ state: 'visible' });

  await page.screenshot({ path: 'thumbnail.png', type: 'png' });
} finally {
  await browser.close();
}

The example is a template, not a universal selector recipe: a site may not expose a heading with that text. Choose a locator for the hero image, title, or other content that must be present. If the URL redirects, consider validating the final URL or the page-specific content as well as the response status.

Choose the navigation milestone

page.goto() defaults to load. Choose a milestone based on what you need navigation to wait for, and then use a separate content signal for visual readiness when the page renders asynchronously. The Page API defines these options:

Milestone What it means When it can help
commit The response has been received and document loading has started. When you want to begin interacting or waiting early, while understanding that the document and its content may still be loading.
domcontentloaded The document’s DOM content has been loaded. When the initial document is enough to start waiting for a particular client-rendered target.
load The page’s load event has fired; this is the default for goto(). When waiting for the browser load event is appropriate, though it still may not mean application content is ready.
networkidle No network connections for at least 500 ms. Use only when network quiet is specifically relevant to your workflow. Playwright discourages it as a general test readiness strategy.

Do not treat any lifecycle milestone as proof that the exact visual content is present. Network activity can continue because of polling or other long-lived requests, and network quiet alone does not establish that the needed image or component rendered. Prefer a locator wait or a web-first assertion tied to that content.

Wait for the content that belongs in the thumbnail

Use a locator’s waitFor() for a clear condition such as visibility. When writing a test, a web-first assertion can express the same readiness requirement and retry until its timeout. Prefer these approaches to the older page.waitForSelector(), which is documented as discouraged. Choose a stable selector that represents the visual result, not an incidental loading detail.

// Wait for a hero image to appear:
await page.locator('main img.hero').waitFor({ state: 'visible' });

// Or wait for the page heading:
await page.getByRole('heading', { name: 'Product overview' }).waitFor({ state: 'visible' });

Visibility does not necessarily mean an image has finished decoding. If the thumbnail depends on an image, check that it loaded before capture:

const hero = page.locator('main img.hero');
await hero.waitFor({ state: 'visible' });
await hero.evaluate(async (img) => {
  if (!img.complete) {
    await new Promise((resolve, reject) => {
      img.addEventListener('load', resolve, { once: true });
      img.addEventListener('error', reject, { once: true });
    });
  }
  if (img.naturalWidth === 0) throw new Error('Hero image did not load.');
});

That image check is useful when the page can show an image element before its image data is ready. Adapt it for CSS background images, canvases, video posters, or other site-specific content; a locator for one element cannot establish readiness for every visual dependency.

Choose the screenshot scope and output

Playwright supports a viewport screenshot, a full-page screenshot, and a screenshot of a locator. Select the scope that matches the destination: a thumbnail is often a viewport composition, while a full-page image can include far more content than a card or preview needs. See the Playwright screenshots guide.

// Viewport (default):
await page.screenshot({ path: 'thumbnail.png' });

// Entire scrollable page:
await page.screenshot({ path: 'full-page.png', fullPage: true });

// One region only:
await page.locator('main article').screenshot({ path: 'article.png' });

// Capture bytes for downstream processing:
const pngBuffer = await page.screenshot({ type: 'png' });

Choose dimensions with page.setViewportSize({ width, height }) before navigation or capture. For output, screenshot options include a path or returned buffer, image type (png, jpeg, or webp where supported by the installed Playwright/browser combination), JPEG quality, full-page capture, and animations handling. Check the screenshot API for the exact options available in your installed version. There is no universal thumbnail size mandated by Playwright: follow the destination’s aspect ratio, dimensions, and file-size requirements.

Timeouts, failure policy, and retries

Navigation and locator waits can time out. Set timeouts to fit the expected page and your job’s overall deadline; the example sets a 30-second navigation timeout and a 15-second general timeout. If the readiness target never appears, let the operation fail with a useful error or produce an explicit fallback image. Do not silently capture a blank or half-rendered page as if it were ready.

For batch capture, decide what a failed page means to the caller: return a per-URL error, retry a transient failure within a bounded policy, or emit a known fallback. A retry should create a fresh page or context when appropriate and should not retry indefinitely. Keep the failed URL, stage (navigation, readiness, or screenshot), and error message so the cause can be diagnosed. Avoid retrying deterministic failures such as a selector that does not exist on that page.

Common problems and fixes

Symptom Likely cause Fix
goto() times out at load The page or one of its resources is slow, or the load event does not fire promptly. Choose an earlier navigation milestone when suitable, then wait for the actual thumbnail target. Set a considered navigation timeout.
networkidle never resolves The site keeps connections active or continues making requests. Wait for a locator or assertion that represents the content needed in the image.
Screenshot is blank or missing the hero Navigation completed before asynchronous rendering or image loading finished. Wait for the visible target and, for image-dependent captures, verify the image loaded before taking the screenshot.
Locator wait times out The selector or accessible name does not match, the target is hidden, or the page failed to render it. Inspect the page and locator, confirm the target exists in the relevant state, and report or handle genuine missing content.
Fixed delay works inconsistently Page timing varies, so the delay sometimes ends before content is ready and sometimes wastes time. Replace waitForTimeout() with a page-specific locator or assertion. Playwright marks fixed waits discouraged.
Full-page capture is unexpectedly large The document is taller than the intended thumbnail. Use the viewport screenshot or capture just the thumbnail element.
Screenshot operation times out The page may be unusually large, still changing, or resource constrained. Capture a smaller scope, wait for a stable target, and review the screenshot operation’s timeout and job deadline.

Performance, reliability, and cost

Waiting for a specific visible target often avoids the unnecessary delay of waiting for unrelated resources, but the right signal is page-dependent. Use the smallest capture scope that satisfies the destination, because full-page capture can require more browser work and produce larger output. Reuse a browser process for multiple captures when your application can manage contexts and cleanup safely; isolate pages or contexts where cookies, state, or concurrent work must not leak between jobs.

Reliability comes from explicit readiness conditions, bounded timeouts, cleanup in a finally block, and a clear policy for missing content. Fixed sleeps make every capture wait on a guess and still do not guarantee readiness. Playwright itself does not define a universal capture cost: account for browser compute, storage, retries, and the cost of running your own capture service.

Or skip the browser setup

If you want the same URL-to-image workflow without installing and maintaining a browser, ScreenshotNeo provides a website screenshot API and MCP server. Its API documentation covers the request options.

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()
with open("shot.webp", "wb") as f:
    f.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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents, including Claude and Cursor, take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.

FAQ

What should the readiness locator match?

Match the content whose presence makes the thumbnail useful: for example, the page title, hero image, or a particular preview component. Use a signal that applies to the actual pages you capture.

Should I wait for load or domcontentloaded?

Neither is universally correct. Select the navigation milestone that fits your workflow, then separately wait for the visual target if it renders asynchronously.

Can Playwright return the screenshot without saving a file?

Yes. Calling page.screenshot() without a path returns a buffer that your code can store or pass to an image-processing step.

Primary references