How to Set a Screenshot API to Wait for Page Fonts and Images
Wait for used fonts and the images your capture needs, including lazy-loaded images. Learn browser-side checks, provider options, and fixes for incomplete screenshots.
A page reaching its navigation load event does not guarantee that its fonts and images are ready to appear correctly in a screenshot. Wait for the browser’s used-font set with document.fonts.ready, then wait for the images in the capture area to finish. For lazy-loaded images, scroll the relevant content into view first so loading can begin. Check image success as well as completion: a broken image can still have img.complete === true.
The right implementation depends on the screenshot API. If it supports a pre-capture JavaScript hook, run the browser-side checks there. If you control the browser, run them between navigation and capture. If the API only accepts request parameters, use its documented wait and scrolling options; parameter names and script support are provider-specific. See the ScreenshotNeo API documentation for its request options.
1. Understand what “ready” means
Navigation events describe document loading milestones. They do not necessarily describe whether the specific visual content you care about has appeared.
| Signal | What it tells you | What it does not guarantee |
|---|---|---|
DOMContentLoaded |
The document was parsed. | That fonts, images, or app-rendered content are ready. |
load |
The document’s load event fired. | That later asynchronous work or the exact capture region is finished. |
networkidle |
Network activity met a provider’s idle heuristic. | That the desired fonts and images rendered. Analytics, polling, or other connections can also affect this signal. |
document.fonts.ready |
Loading and layout operations for fonts used by the document have completed. | That every declared font was used or loaded. |
img.complete |
An image request has reached a completed state. | That the image loaded successfully; check naturalWidth too. |
Playwright documents networkidle as discouraged for general readiness checks and recommends assessing readiness with assertions instead. A page-specific font and image check is more directly tied to the output you need. See the Playwright Page API, MDN: Document.fonts, and MDN: HTMLImageElement.complete.
2. Use a browser-side font and image check
For an API with a pre-screenshot JavaScript hook, adapt this page-context function to the provider’s documented hook syntax. It waits for fonts, takes an inventory of the images currently in the document, waits for each to load or fail, and returns the failed image URLs. The event handlers resolve on both outcomes so a failed request does not leave the capture waiting forever.
async () => {
await document.fonts.ready;
const images = [...document.images];
await Promise.all(
images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
})
);
const failedImages = images.filter((img) => img.naturalWidth === 0);
return {
imageCount: images.length,
failedImageUrls: failedImages.map((img) => img.currentSrc || img.src),
};
}
This is a browser-level pattern, not a universal screenshot API parameter. Put a provider-appropriate timeout around the hook. Decide whether failed images should fail the job or whether a partial screenshot is acceptable. If the page inserts images after this function takes its inventory, wait for a page-specific condition first, then collect the images.
Include only the images that matter
document.images covers HTML <img> elements, including responsive image sources selected by the browser. It does not cover every image-like visual resource: a CSS background image, a canvas drawing, or an image inside a cross-origin iframe may need a page-specific check or separate handling. For an element screenshot, scope the check to that element rather than waiting on unrelated images throughout the page.
For example, replace the inventory line with a selector scoped to your capture region:
const region = document.querySelector('#report');
if (!region) throw new Error('Capture region #report was not found');
const images = [...region.querySelectorAll('img')];
3. Handle lazy-loaded and dynamic content
A wait can only observe an image request that has started or an image element that is already present. Lazy-loaded images may not be requested until they approach the viewport. For a full-page capture, scroll through the intended capture area before running the final image check. Keep the scroll bounded on pages that grow as you scroll, such as feeds with infinite loading.
A simple browser-side scroll pass can trigger many native lazy-loading behaviors. Adapt the maximum height and step size to the page and provider’s capture limits:
async () => {
const maxHeight = Math.min(document.documentElement.scrollHeight, 12000);
const step = Math.max(400, Math.floor(window.innerHeight * 0.8));
for (let y = 0; y < maxHeight; y += step) {
window.scrollTo(0, y);
await new Promise((resolve) => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
await document.fonts.ready;
const images = [...document.images];
await Promise.all(images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
return images
.filter((img) => img.naturalWidth === 0)
.map((img) => img.currentSrc || img.src);
}
The short pause in this example gives scroll-triggered work a chance to start; it is a heuristic, not a guarantee. If the site has a known “content loaded” selector or application state, wait for that condition instead. Do not scroll an unbounded page indefinitely: choose a maximum capture region, then report or accept content beyond it as outside the job’s scope.
4. Playwright: complete Node.js example
When you control the browser, use navigation as a baseline, then wait explicitly for the visual resources before calling screenshot(). Install Playwright in your Node.js project and make sure the browser binaries are installed according to the Playwright installation guide.
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto('https://example.com', {
waitUntil: 'load',
timeout: 30000,
});
// Optional: trigger lazy loading for a bounded full-page capture.
await page.evaluate(async () => {
const maxHeight = Math.min(document.documentElement.scrollHeight, 12000);
const step = Math.max(400, Math.floor(window.innerHeight * 0.8));
for (let y = 0; y < maxHeight; y += step) {
window.scrollTo(0, y);
await new Promise((resolve) => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
});
const result = await page.evaluate(async () => {
await document.fonts.ready;
const images = [...document.images];
await Promise.all(images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
return {
imageCount: images.length,
failedImageUrls: images
.filter((img) => img.naturalWidth === 0)
.map((img) => img.currentSrc || img.src),
};
});
if (result.failedImageUrls.length) {
throw new Error(`Failed images: ${result.failedImageUrls.join(', ')}`);
}
await page.screenshot({ path: 'capture.png', fullPage: true });
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
For an application that renders after navigation, wait for its meaningful selector before the font and image check. Playwright’s waitForFunction can wait for a page-context predicate to become truthy. Set timeouts so an absent selector or stuck resource produces a diagnosable failure rather than an indefinitely waiting job. See the Playwright Page API.
await page.waitForFunction(
() => document.querySelector('[data-report-ready="true"]'),
{ timeout: 15000 }
);
5. Python: complete Playwright example
The same browser-side readiness check works when driving Playwright from Python. Install the Playwright package and its browser as described in the Playwright for Python documentation.
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(viewport={"width": 1440, "height": 900})
try:
await page.goto("https://example.com", wait_until="load", timeout=30000)
result = await page.evaluate("""async () => {
await document.fonts.ready;
const images = [...document.images];
await Promise.all(images.map((img) => {
if (img.complete) return Promise.resolve();
return new Promise((resolve) => {
img.addEventListener('load', resolve, { once: true });
img.addEventListener('error', resolve, { once: true });
});
}));
return images
.filter((img) => img.naturalWidth === 0)
.map((img) => img.currentSrc || img.src);
}""")
if result:
raise RuntimeError(f"Failed images: {', '.join(result)}")
await page.screenshot(path="capture.png", full_page=True)
finally:
await browser.close()
asyncio.run(main())
6. cURL and HTTP screenshot APIs
cURL can call an HTTP screenshot API, but it cannot itself wait inside the page for document.fonts.ready or inspect image elements. Those checks must run in a browser controlled by the API or by your own automation code. For an HTTP provider, use its documented navigation wait, delay, JavaScript hook, scrolling, or async-job options. Do not assume that one provider’s scripts, wait_until, or image-wait parameter exists on another provider.
The generic cURL shape below sends a URL and a provider-specific readiness option. Replace the endpoint, authentication, and option names with the exact values in your provider’s documentation:
curl -G 'https://provider.example/shot' \
-H 'Authorization: Bearer YOUR_API_KEY' \
--data-urlencode 'url=https://example.com' \
--data-urlencode 'wait_until=load' \
-o capture.png
A fixed delay may help when the provider has no browser-side hook, but it is a coarse fallback: it can wait longer than needed and still miss slower resources. If the provider supports asynchronous jobs, a long capture can be submitted and polled according to its documented job flow; confirm its timeout and callback semantics before relying on that approach.
7. Choosing provider wait controls
Compare controls by what they wait for and what they report, not only by their option names.
| Control | Useful for | Limit |
|---|---|---|
Navigation event (load, domcontentloaded) |
A baseline before page checks. | Does not prove the target visuals are ready. |
| Browser-side predicate | Waiting for used fonts, selected images, or application state. | Requires provider script support or a browser session; needs a timeout and failure policy. |
| Network idle | A rough signal on pages whose network activity settles. | Can be disrupted by persistent traffic and is not proof of visual readiness. |
| Fixed delay | A simple fallback when finer controls are unavailable. | May be wasteful or too short for variable page conditions. |
| Scroll before capture | Triggering viewport-based lazy loads in the intended capture region. | Must be bounded for very long or continuously growing pages. |
| Selector or app-state wait | Pages with a reliable marker for rendered content. | The marker must actually correspond to the content and resources needed in the image. |
Provider behavior changes over time. Check the provider’s current documentation for script execution order, request limits, timeout behavior, full-page scroll controls, and whether a failed resource causes a failed job or a partial image. Browserless documents screenshot options and image-wait controls in its official documentation; ScreenshotOne documents navigation, delay, and full-page scrolling options in its official options documentation. Verify the exact endpoint and interface you use.
8. Troubleshooting incomplete screenshots
| Symptom | Likely cause | Fix |
|---|---|---|
| Fallback font appears | The screenshot happened before the used web font finished loading, or the font request failed. | Await document.fonts.ready after the relevant content is present. If the page still uses a fallback, inspect the page’s font request and font-family selection. |
| Image is absent but no wait timed out | The image is lazy-loaded and has not entered the loading region, or it is inserted after the inventory was taken. | Scroll the capture area first; wait for the app’s content marker; then collect the image list. |
| The wait completes but an image is broken | complete also covers failed requests. |
Check naturalWidth; log the failing currentSrc or src and choose whether to fail or accept a partial capture. |
| Capture waits indefinitely | A promise listens only for load, a resource never settles, or the provider has no effective script timeout. |
Resolve on both load and error, set a bounded timeout, and surface the resource or condition that exceeded it. |
| Images appear in the viewport capture but not the full-page image | Full-page capture may not trigger lazy loading by itself. | Use documented full-page scrolling or a bounded scroll pass before the readiness check. |
| CSS illustration or background is missing | The check only inventories img elements. |
Wait for the relevant element or app state; if needed, inspect its computed style or use page-specific readiness logic. |
| Network-idle wait times out or never occurs | The page keeps requests active, such as polling or streaming. | Wait for a relevant selector or explicit font/image condition instead of treating global network silence as required. |
| Screenshot API rejects the wait parameter | Parameter names and supported hooks differ by provider or endpoint. | Use that endpoint’s current docs; do not copy another provider’s option name. |
9. Performance, reliability, and cost
- Wait narrowly. For a component screenshot, check that component’s images instead of every image on the page. This avoids waiting on unrelated or offscreen resources.
- Prefer conditions to long sleeps. A font promise, image completion check, or app-specific predicate finishes when its condition is met; a fixed delay always spends the full delay and may still be insufficient.
- Bound the work. Set navigation and readiness timeouts, limit how far a full-page scroll can go, and decide what to do with failed images.
- Make failures observable. Record the failed image URLs or timed-out condition so retries and debugging can target the cause. Retrying a permanently broken asset will not fix it.
- Expect capture time to vary. Waiting for additional resources adds work to the request. Use the smallest capture region and readiness condition that produces the required output.
- Check billing semantics. Hosted screenshot providers define their own billing rules for timeouts, retries, and cache hits. Read those rules before increasing wait limits or retrying jobs.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its API accepts a URL in one GET request and returns an image or PDF. Use the documented options to choose capture behavior; the generic browser-side code above is not a ScreenshotNeo parameter or promise that a custom JavaScript hook is available. See the ScreenshotNeo API docs for its request parameters and supported wait controls.
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}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers indicate the page verdict and billing status. Its MCP server provides screenshot and PDF tools for AI agents. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
11. FAQ
Does document.fonts.ready load every font declared in CSS?
It resolves after fonts used by the document and their related layout work are ready. A declared font that was not used is not necessarily fetched.
Should I always wait for every image on the page?
No. Wait for the images relevant to the capture. A page can contain offscreen, decorative, or continually added images that do not belong in the requested screenshot.
Is networkidle enough?
It can be a useful heuristic, but it does not prove a particular font or image is ready. Pair navigation with a direct page condition when the exact visual output matters.
Can cURL wait for browser fonts?
No. cURL makes the HTTP request. The screenshot provider must run the browser-side check, or your own browser automation must do so before taking the screenshot.


