How to Make Chrome Wait for Web Fonts Before Taking a Full-Page Screenshot
Wait for document.fonts.ready before capturing a full-page screenshot in Chrome. Here’s runnable Playwright and CDP code, plus fixes for fonts that still look wrong.
In Chrome automation, wait for the page’s used web fonts and their related layout work to settle before capturing the page. With Playwright, evaluate document.fonts.ready in the page, then take the full-page screenshot:
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
document.fonts.ready resolves when fonts currently needed by the document have finished loading and font-related layout work is complete. It does not promise that every face declared in CSS has downloaded: unused faces and optional faces may not load. MDN describes the promise as resolving after fonts and layout operations are complete and no further font loads are needed (MDN FontFaceSet.ready).
1. Capture a full page after fonts settle with Playwright
Install Playwright and its Chromium browser if they are not already available:
npm install playwright
npx playwright install chromium
Save this as screenshot.mjs and run it with node screenshot.mjs https://example.com. The example waits for DOM content, then for fonts, and writes a full-page PNG. Replace the generic navigation wait with a page-specific locator when the site renders its content asynchronously.
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot.mjs <url>');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
// Replace this with a locator for the content your page renders asynchronously.
await page.locator('body').waitFor({ state: 'visible' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
The two relevant Playwright pieces are the page-context evaluation and fullPage: true; the latter captures the full scrollable page rather than just the current viewport (Playwright screenshot documentation).
2. Choose the right wait order for dynamic and lazy content
A font readiness wait cannot cover content that has not yet been inserted or rendered. A single-page app may update after navigation, and content below the fold may only appear after scrolling. Wait for the content that matters first, trigger relevant lazy content if needed, then wait for fonts and capture.
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.getByRole('heading', { name: 'Latest reports' }).waitFor();
// If the page loads content on scroll, visit the relevant area before capture.
await page.evaluate(async () => {
const step = Math.max(300, window.innerHeight);
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
// Scrolling may reveal content that uses additional font faces.
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: 'page.png', fullPage: true });
The scroll loop is an example, not a universal lazy-loading solution. Adapt it to the site: some pages require waiting for a specific item, clicking “load more,” or satisfying an application-specific condition. Once the content is present, await document.fonts.ready again so newly used faces can settle.
3. Add a bounded font wait for production jobs
For unattended capture jobs, a project-specific timeout can keep one stalled page from holding a worker indefinitely. The timeout is an operational limit you choose; it is not a Chrome guarantee. Decide how your application should handle the timeout: fail the capture, record a warning and continue, or retry under a bounded policy.
const fontWait = page.evaluate(() => document.fonts.ready);
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Timed out waiting for web fonts')), 15_000);
});
await Promise.race([fontWait, timeout]);
await page.screenshot({ path: 'page.png', fullPage: true });
Choose the limit from your job’s time budget and the sites you capture. If a timeout occurs, capture diagnostics before retrying: URL, elapsed time, console messages, failed requests, and the font faces’ statuses. Avoid silently treating a timed-out font wait as proof that the intended font rendered.
4. Use Chrome DevTools Protocol directly
If your automation already speaks CDP, evaluate the promise in the page’s execution context and then call Page.captureScreenshot. The protocol’s captureBeyondViewport option is documented as experimental and defaults to false, so confirm its support and behavior in your Chromium version (Chrome DevTools Protocol Page.captureScreenshot).
// Pseudocode for a CDP client after attaching to the target page.
await cdp.send('Runtime.evaluate', {
expression: 'document.fonts.ready',
awaitPromise: true,
returnByValue: true
});
const result = await cdp.send('Page.captureScreenshot', {
format: 'png',
captureBeyondViewport: true
});
// Decode result.data from base64 and write it to page.png.
CDP clients differ in how they attach to a target and save returned base64 data, so the snippet shows the protocol calls rather than a library-specific launcher. The ordering is the same as Playwright: execute the wait in the page, then capture.
5. What the font wait does—and does not—guarantee
- It waits for currently needed fonts and related layout work. The browser resolves the page’s
FontFaceSet.readypromise when loading and layout operations for fonts used by the document are complete. - It does not download every declared font. A face that is unused, optional, or not needed by the rendered content may remain unloaded.
- It does not wait for your app’s future content. First wait for application rendering, then wait for fonts.
- It does not disable animations. For repeatable visual regression captures, handle animations separately using the screenshot library’s animation options. Font readiness and animation control solve different problems.
- It does not guarantee the desired font succeeded. A failed font request can leave the page using a fallback. Inspect font loading status and errors when the result looks wrong.
The CSS Font Loading API exposes font face statuses and loading error events. For troubleshooting, inspect document.fonts.status and the faces in document.fonts from the page context.
6. Troubleshoot screenshots with the wrong font
| Symptom | Likely cause | What to do |
|---|---|---|
| Text uses a fallback font | The intended font request failed, was blocked, or returned an unusable font. | Inspect browser network failures and console errors for font requests; verify the URL, access policy, and response. Check the face’s status and any loading error event. |
| The wait finishes but a section looks different | The section appeared after the wait and introduced a newly used face. | Wait for that content first, then await document.fonts.ready again before capture. |
| Only the first viewport is correct | Below-the-fold content or fonts were activated by scrolling, or only a viewport capture was requested. | Trigger the page’s lazy content as needed, repeat the font wait, and use fullPage: true in Playwright. |
| The screenshot occasionally changes between runs | Content, fonts, or animations are still changing at capture time. | Wait for a meaningful content condition, then fonts; control animations separately if the capture is for visual comparison. |
| The font wait hangs or exceeds the job limit | A font load or page operation is stalled, or the site is unusually slow. | Use a project-specific bounded wait, collect request and console diagnostics, and decide whether to fail, retry, or continue with a recorded warning. |
| CDP screenshot includes only the viewport | captureBeyondViewport was omitted, unsupported, or behaves differently in the Chromium version. |
Check the protocol version and option support. The protocol documents the option as experimental; validate it in the browser you deploy. |
7. Reliability, performance, and cost considerations
document.fonts.ready is more closely tied to font and layout state than an arbitrary fixed sleep, so it can avoid both capturing too early and waiting a needless fixed interval. It is not a universal page-ready signal: page-specific rendering, lazy loading, network behavior, and animations need their own handling.
Full-page capture can require more work and produce a larger image than a viewport capture, especially for long pages. Set a deliberate viewport, output format, and worker time budget for your own workload; there is no universal duration that applies to all sites. If font loading fails, decide explicitly whether your use case accepts a fallback-font image or should mark the capture as failed. Retrying without recording the underlying failure can repeat the same problem.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Make a GET request with the target URL; see the API documentation for options and configuration.
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()
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);
ScreenshotNeo removes cookie banners, newsletter 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, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month—no card required.
9. Frequently asked questions
Does document.fonts.ready wait for fonts in iframes?
The promise belongs to a document’s font set. If your capture depends on content in a separate frame, check readiness in that frame’s document as well, subject to the page’s access and automation constraints.
Should I use networkidle instead?
Network idleness and font readiness are different signals. A page can have no active network requests while app content or layout is still changing, and a busy page may never become network-idle. Wait for the content you need and then await the font set.
Can I wait for every font declared in the stylesheet?
The ready promise concerns fonts currently needed by the document, not every declared face. If a particular section or text must use a face, ensure that content is rendered and that the face is actually selected, then inspect its load result.


