ScreenshotNeo

BlogHow-to

How to Wait for a Web Font Before Taking a Puppeteer Screenshot

Wait for the page’s content, then await document.fonts.ready before capturing. Here’s the runnable Puppeteer pattern, options, and fixes for common timing issues.

By the ScreenshotNeo team4 October 20268 min read

To wait for web fonts before a Puppeteer screenshot, first wait for the relevant page content to exist, then await document.fonts.ready in the page context, and only then capture:

await page.goto(url, { waitUntil: 'networkidle2' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png' });

document.fonts.ready resolves when the current document has completed loading its fonts and the associated layout work. Network idle is a navigation condition; it is not a replacement for this font readiness signal. ([MDN: FontFaceSet.ready](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready), [Puppeteer: Page.evaluate](https://pptr.dev/api/puppeteer.page.evaluate), [Puppeteer screenshot guide](https://pptr.dev/guides/screenshots))

1. Complete runnable Puppeteer example

This CommonJS script opens a page, waits for navigation and a page-specific content selector, waits for fonts, and saves a full-page PNG. Install Puppeteer with npm install puppeteer, save this as capture.js, then run node capture.js https://example.com.

const puppeteer = require('puppeteer');

async function main() {
  const url = process.argv[2];
  if (!url) throw new Error('Usage: node capture.js <url>');

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });

    await page.goto(url, {
      waitUntil: 'networkidle2',
      timeout: 60000,
    });

    // Replace this selector with an element that signals your app is rendered.
    await page.waitForSelector('body');

    // Wait for fonts needed by the current document and its layout to settle.
    await page.evaluate(() => document.fonts.ready);

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

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

For an application that renders after navigation, replace body with a selector or condition that becomes true only after the content you need is present. The example’s navigation and selector waits address different conditions: navigation completion does not prove an SPA has rendered its final content.

2. Why wait for document.fonts.ready?

The browser exposes document.fonts as the document’s FontFaceSet. Its ready promise resolves after font loading and related layout operations have completed for the document’s current requirements. Await it inside page.evaluate, which runs code in the page and waits for a returned promise. ([MDN: FontFaceSet.ready](https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/ready), [Puppeteer: Page.evaluate](https://pptr.dev/api/puppeteer.page.evaluate))

This avoids guessing with an arbitrary sleep. A fixed delay may waste time when fonts load quickly and may still be too short when they load slowly. There is no universal delay that guarantees a particular site’s fonts have settled.

Order matters: wait for the content and styling that trigger the relevant font requests, then wait for document.fonts.ready. The promise describes the current document’s font work; it does not predict future content mutations. If an interaction later inserts text, changes font styling, or triggers another font request, wait for that state and await the font readiness promise again before capture.

3. Alternative wait with waitForFunction

For a single promise, page.evaluate(() => document.fonts.ready) is the clearest form. page.waitForFunction() is useful when readiness must be combined with a browser-side predicate; it waits until the supplied function returns a truthy value and supports an asynchronous function. ([Puppeteer: Page.waitForFunction](https://pptr.dev/api/puppeteer.page.waitforfunction))

await page.waitForFunction(async () => {
  await document.fonts.ready;
  return true;
});
await page.screenshot({ path: 'page.png' });

To combine an application condition with fonts, you can use a predicate such as:

await page.waitForFunction(async () => {
  const appReady = document.querySelector('[data-rendered="true"]');
  if (!appReady) return false;
  await document.fonts.ready;
  return true;
});

Choose a real application readiness signal for your page. A generic body selector only confirms that a body element exists.

4. Wait after interactions and capture an element

Use the same font wait for a full-page shot or an element screenshot. If a click opens a panel, changes theme, or causes content to render, perform that action first, wait for the resulting content, await fonts, and capture:

await page.click('[data-open-report]');
await page.waitForSelector('.report-panel');
await page.evaluate(() => document.fonts.ready);

const panel = await page.$('.report-panel');
if (!panel) throw new Error('Report panel did not appear');
await panel.screenshot({ path: 'report-panel.png' });

Puppeteer supports element screenshots through an element handle’s screenshot() method. ([Puppeteer screenshot guide](https://pptr.dev/guides/screenshots))

For lazy-loaded images or other non-font assets inside an element, font readiness alone is not enough. Add the relevant image or content readiness condition separately.

5. Explicitly request or check a particular font

For the normal case, where page CSS already applies the desired typography, wait on document.fonts.ready. The Font Loading API also provides document.fonts.load(font, text) to request a particular face for text, and document.fonts.check(font, text) to check whether the text can render without needing an unloaded font. ([MDN: CSS Font Loading API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API))

// Request a specific face for the text you intend to capture.
await page.evaluate(async () => {
  await document.fonts.load('16px "Example Sans"', 'Screenshot heading');
  await document.fonts.ready;
});

// Optional diagnostic: check whether the requested font is available for text.
const available = await page.evaluate(() =>
  document.fonts.check('16px "Example Sans"', 'Screenshot heading')
);
console.log({ available });

Use the font family and sample text that match the page. A successful check does not by itself prove that the intended font file loaded: fallback behavior and the exact text and face requested matter. Treat it as a focused check, not a replacement for ensuring the page rendered the intended styles.

6. Navigation and screenshot options that affect timing

Choice Use it for Keep in mind
waitUntil: 'networkidle2' A page that becomes mostly quiet after navigation It does not establish font readiness. Some pages keep network activity open.
waitUntil: 'domcontentloaded' Pages where you will wait on a specific app condition yourself The DOM being parsed does not mean content or fonts are ready.
waitUntil: 'load' Pages where the load event is an appropriate navigation boundary Follow with the app-specific and font waits when typography matters.
fullPage: true A screenshot of the full document Wait for any content that only appears after scrolling or other page actions.
Element handle screenshot() A crop of one element Wait for the element to exist and its fonts to settle first.

These are separate controls: navigation lifecycle, application rendering, font readiness, and screenshot area. Use the combination that matches the page rather than assuming one wait covers all four.

7. Troubleshooting

Symptom Likely cause Fix
Screenshot uses a fallback font The capture happened before the page requested or finished loading the intended font, or the font resource failed. Wait for app content and styles first, then await document.fonts.ready. Inspect the page’s font requests and CSS family names if fallback remains.
Network idle occurs but typography changes later Network idle and font readiness are different conditions; client-side rendering may also happen after navigation. Wait for the relevant app state, then explicitly await document.fonts.ready.
The font wait completes, then layout changes A later interaction or mutation introduced content or styling that needs another font. Perform the interaction first, wait for its resulting state, then await the promise again.
waitForFunction times out The predicate never becomes truthy, or the page-side condition is wrong. Check the selector/state in the predicate. Use direct page.evaluate(() => document.fonts.ready) if only the promise wait is needed.
Navigation times out on a busy site The chosen lifecycle condition may not occur because the page maintains connections or ongoing requests. Use a navigation boundary suitable for the site, then wait for a concrete application condition and fonts separately.
Element screenshot fails or captures the wrong region The selector did not resolve to the intended element, or the app had not rendered it. Wait for the selector, verify the handle exists, and capture after the font wait.
Font looks wrong despite the wait The requested family or weight may not match the CSS, the resource may be unavailable, or the font may not cover the captured characters. Check computed styles, font requests, weight/style declarations, and glyph coverage. Use document.fonts.load() for a specific face and sample text when appropriate.

8. Performance, reliability, and cost

Waiting for the promise synchronizes with actual font work instead of imposing a fixed delay. The extra wait varies with the page’s font state; it may resolve quickly when no additional fonts are needed or take longer while required fonts load. A separate selector or application-state wait remains necessary for content that appears asynchronously.

For reliable capture jobs, set an appropriate navigation timeout, handle failures, and close the browser in a finally block as in the example. Avoid capturing after a wait has failed: a timeout or rejected operation should be surfaced as a failed capture so the caller can retry or report it, rather than silently saving an image with uncertain typography. The browser code itself has no per-screenshot service charge; operational cost depends on the browser and compute environment you run.

9. Or skip the browser setup

If you need a screenshot without managing Puppeteer, [ScreenshotNeo](https://screenshotneo.com) is a website screenshot API and MCP server. One GET request returns an image or PDF. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for 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)
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

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 take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Get 1,000 free screenshots a month.

10. FAQ

Does document.fonts.ready wait for every font on the internet?

No. It is a readiness signal for the current document’s font set and requirements. It does not wait for unrelated fonts or future content that has not yet triggered a font request.

Should I use evaluate or waitForFunction?

Use evaluate for a one-time wait on the promise. Use waitForFunction when you need a predicate that combines font readiness with another browser-side condition.

Can I use the same wait for PDF or element capture?

Yes. Await font readiness after the relevant content is present and before invoking the capture method, whether you are saving a full-page screenshot, an element screenshot, or a PDF.

Is a fixed timeout ever useful?

A short delay can be part of a site-specific workaround, but it is not a reliable substitute for the browser’s font readiness signal and has no universal safe value.