Puppeteer Screenshot: How to Wait for a Web Font to Load
Wait for document.fonts.ready before capturing so Puppeteer screenshots use settled web fonts. Learn when to load a specific face and how to troubleshoot fallbacks.
Direct answer: after navigation and any page-specific rendering setup, await document.fonts.ready in the page context, then take the screenshot:
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'screenshot.png' });
Puppeteer waits for a promise returned by page.evaluate(). The browser’s document.fonts.ready promise resolves when font loading and layout work are complete and no further font loads are needed. It does not prove that a particular font loaded successfully or that the page selected it for the text you are capturing. Puppeteer: Page.evaluate() · MDN: FontFaceSet.ready
Wait for fonts before taking a screenshot
For a typical page whose CSS and content use a web font, navigate first, wait for the fonts, and capture afterward:
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Replace the URL with the page you need to capture. networkidle2 is one possible navigation condition; it is not a replacement for explicitly waiting on the font set. Puppeteer’s screenshot guide shows the navigation-then-screenshot flow, while the font wait is a separate browser synchronization step. Puppeteer screenshot guide
Choose a navigation condition that fits the page
loadwaits for the load event and is a useful default when page resources finish normally.domcontentloadedcan return earlier, but page scripts may still be adding content or styles. Wait for your app’s rendering condition before waiting for fonts.networkidle2can suit pages that settle after network activity. Persistent connections or background requests may prevent network idle from being a useful signal.
Whichever condition you choose, make sure the page has applied the styles and content that use the desired font before reading document.fonts.ready.
Load a specific font and character set
If the desired face is not yet used by the page, explicitly request it with document.fonts.load(fontShorthand, text), then wait for the font set and capture:
await page.evaluate(async () => {
await document.fonts.load('16px "Example Sans"', 'Sample text 123');
await document.fonts.ready;
});
await page.screenshot({ path: 'page.png' });
Change Example Sans to the CSS family name and use representative text containing the characters that will appear in the capture. The text argument filters matching faces, including faces with Unicode ranges; it does not check whether a loaded font contains every requested glyph. FontFaceSet.load() returns a promise that fulfills with matching loaded faces or rejects if loading fails. MDN: FontFaceSet.load()
Complete runnable examples
cURL: run a Puppeteer script
Puppeteer is a Node.js library, so cURL does not provide a native way to wait on a browser’s document.fonts. Save the JavaScript example below as capture.mjs, install Puppeteer, and use cURL to invoke the script with the page URL:
npm install puppeteer
node capture.mjs https://example.com
# Or invoke the script through cURL:
curl --get 'file:///path/to/capture.mjs' --data-urlencode 'url=https://example.com'
The cURL form above is not a way to execute a local JavaScript file; it illustrates why a command-line HTTP client cannot control a local Puppeteer browser. To run the capture from a shell, use node capture.mjs. If you need an HTTP screenshot endpoint callable with cURL, see the ScreenshotNeo section below.
Node.js: Puppeteer capture script
Save this as capture.mjs. Run it with node capture.mjs https://example.com. It waits for navigation, then for fonts, and writes a full-page PNG.
import puppeteer from 'puppeteer';
const url = process.argv[2];
if (!url) throw new Error('Usage: node capture.mjs <url>');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
await browser.close();
}
Install Puppeteer in the project first with npm install puppeteer. Its documented screenshot options include output path, full-page capture, clipping and image type; waitForFonts is not documented as a screenshot option. Puppeteer: ScreenshotOptions
Python: call a local Puppeteer service
Puppeteer is a Node.js library, so Python cannot directly call its Page API. If your application already exposes a local HTTP endpoint that runs the Puppeteer sequence above and returns image bytes, Python can call that endpoint. This example assumes an endpoint at http://localhost:3000/screenshot that accepts a url query parameter and returns a PNG:
import requests
response = requests.get(
'http://localhost:3000/screenshot',
params={'url': 'https://example.com'},
timeout=90,
)
response.raise_for_status()
with open('screenshot.png', 'wb') as image:
image.write(response.content)
The endpoint is application code you provide; it is not part of Puppeteer. Its browser handler must navigate, await document.fonts.ready, then return the screenshot bytes. If you do not want to run a browser service yourself, use the one-call API below.
Troubleshooting font rendering
| Symptom | Likely cause | What to check or change |
|---|---|---|
| Screenshot shows a fallback font | The intended font failed to load, is not selected for that text, or was not used before readiness was checked. | Inspect the element’s computed font-family, verify the page’s font resource and CSS, and explicitly call document.fonts.load() for the desired family and representative text. |
| Wait completes but some characters look different | The font may not include those glyphs, or another face may cover them. | Check the rendered characters and fallback behavior. The text passed to load() selects matching faces but does not validate individual glyph coverage. |
document.fonts is undefined |
The evaluation ran outside the intended document context or before a page was available. | Run it using page.evaluate() after navigation has created the document. |
document.fonts.load() rejects |
A matching font resource could not be loaded. | Check the font URL, server response and browser access to the resource; handle the rejection if your capture pipeline should continue with a fallback. |
| Navigation never reaches network idle | The site may keep requests open or perform background traffic. | Choose a suitable navigation condition, wait for the app’s own rendered-state signal, then still await document.fonts.ready. |
Screenshot API rejects waitForFonts |
waitForFonts is documented for Puppeteer PDF options, not screenshot options. |
For screenshots, explicitly await page.evaluate(() => document.fonts.ready). For PDF generation, see the distinction below. |
What font readiness does and does not guarantee
document.fonts.ready resolves after fonts used by the document have finished loading and layout operations have completed, with no further font loads needed. Some declared fonts may remain unloaded if the page does not use them, including optional fonts that did not load in time. The promise therefore signals that the current document’s font work has settled; it does not certify that the page uses a particular family, that every declared face loaded, or that a particular glyph exists. MDN: FontFaceSet.ready
Screenshot and PDF options differ
Puppeteer’s PDF options include waitForFonts, which defaults to true and waits for document.fonts.ready. The documentation notes that bringing a background page to the front may be necessary. This option is for PDF generation; do not pass it to page.screenshot() based on the screenshot API. Puppeteer: PDFOptions
Reliability, performance and cost
- Reliability: wait after the page has applied the relevant CSS and content. If you need a specific face, call
load()and handle a rejected promise according to your fallback policy. - Performance: font waiting adds only the time needed for the relevant font loading and layout to settle. Avoid adding an arbitrary fixed delay as a substitute; it may be too short on a slow page and unnecessarily long on a fast one.
- Full-page captures: lazy-loaded content may change what fonts are used. Trigger the page behavior needed to reveal that content before the final font wait.
- Cost: a self-hosted Puppeteer workflow has no per-shot API fee, but your application bears browser runtime, infrastructure and maintenance costs. A hosted screenshot API trades that setup for a service charge; compare its pricing and billing rules against your capture volume.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its one-call API returns an image or PDF without requiring you to install and operate Puppeteer. Cookie and consent banners, newsletter popups and chat widgets are removed before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads and cache hits cost nothing, and response headers report the page verdict and billing status. AI agents can use its MCP tools to take screenshots, get page information and capture PDFs.
For a normal capture, make a GET request with your API key and target URL. See the ScreenshotNeo API documentation for the available 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 Bun.write('shot.webp', res);
The Python and cURL examples save the response body as an image. The Node.js example uses Bun’s file writer; in Node.js, save the response with await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))) after checking the status.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Does networkidle2 guarantee web fonts are ready?
No. Use it as a navigation condition if it suits the page, then explicitly await document.fonts.ready.
Should I always call document.fonts.load()?
No. For fonts already used by the rendered page, waiting for document.fonts.ready is the usual approach. Call load() when you need to trigger a specific face and text before capture.
Can I use waitForFonts with a screenshot?
The reviewed Puppeteer documentation places that option on PDF generation. For screenshots, await the browser’s font readiness promise through page.evaluate().


