ScreenshotNeo

BlogHow-to

Take a Screenshot of a Webpage with Go and chromedp After Waiting for Fonts

Wait for web fonts and layout to settle before capturing a webpage with Go and chromedp. Choose full-page, viewport, or element capture and handle late-loading content.

By the ScreenshotNeo team4 October 20267 min read

To capture a webpage after its web fonts have loaded, navigate with chromedp, wait for document.fonts.ready, then take the screenshot. That promise resolves after fonts used by the document have finished loading and related layout work has completed. It does not guarantee every declared font loaded, and it does not wait for application data that arrives later. MDN documents the browser font readiness API.

1. Install chromedp and prepare Chrome

Start with a Go module and the chromedp package:

mkdir font-shot
cd font-shot
go mod init example.com/font-shot
go get github.com/chromedp/chromedp

Install Chrome or Chromium in the environment where the program runs. chromedp drives a browser through the Chrome DevTools Protocol; it runs Chrome headless by default. See the chromedp repository and its maintainer screenshot example.

2. Capture after fonts are ready

This complete program captures the entire rendered page as PNG and writes it to page.png. Replace the example URL with the page you want to capture.

package main

import (
    "context"
    "log"
    "os"
    "time"

    "github.com/chromedp/chromedp"
)

func main() {
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    ctx, cancelTimeout := context.WithTimeout(ctx, 45*time.Second)
    defer cancelTimeout()

    var image []byte
    err := chromedp.Run(ctx,
        chromedp.Navigate("https://example.com"),
        chromedp.Evaluate(`document.fonts.ready.then(() => true)`, nil),
        chromedp.FullScreenshot(&image, 100),
    )
    if err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("page.png", image, 0o644); err != nil {
        log.Fatal(err)
    }
}

chromedp.Evaluate runs the JavaScript readiness expression in the page. FullScreenshot captures beyond the viewport; at quality 100, chromedp selects PNG. The 45-second timeout is an adjustable example, not a benchmark or universal recommendation. Use a deadline that fits the pages and execution environment you control.

3. Choose what to capture

Need Action Output notes
Entire page chromedp.FullScreenshot(&image, 100) Quality 100 selects PNG; another quality from 0 through 99 selects JPEG.
Current viewport chromedp.CaptureScreenshot(&image) Captures the visible browser viewport.
One DOM element chromedp.Screenshot("main article", &image, chromedp.NodeVisible) Captures the selected element; choose a selector present on the target page.

These actions and their behavior are documented in chromedp’s screenshot implementation. The maintainer’s screenshot example demonstrates writing the returned byte slice to a file.

Viewport example

var image []byte
err := chromedp.Run(ctx,
    chromedp.Navigate("https://example.com"),
    chromedp.Evaluate(`document.fonts.ready.then(() => true)`, nil),
    chromedp.CaptureScreenshot(&image),
)
if err != nil {
    return err
}
return os.WriteFile("viewport.png", image, 0o644)

Element example

var image []byte
err := chromedp.Run(ctx,
    chromedp.Navigate("https://example.com"),
    chromedp.Evaluate(`document.fonts.ready.then(() => true)`, nil),
    chromedp.Screenshot("main article", &image, chromedp.NodeVisible),
)
if err != nil {
    return err
}
return os.WriteFile("article.png", image, 0o644)

In both snippets, put the actions inside a function returning error and import os. A missing or hidden element can prevent a useful capture; confirm the selector and visibility condition for the page.

4. Wait for the page state you actually need

What document.fonts.ready means

The browser exposes a FontFaceSet as document.fonts. Its ready promise fulfills when loading and layout operations for used fonts have completed. The set of used fonts can differ from declared fonts. For example, an unused face or a face declared with font-display: optional may remain unloaded. Read MDN’s description of Document.fonts and the FontFaceSet API.

Wait for a specific font when necessary

If a particular family and text must be present, use document.fonts.load() with the CSS font shorthand and representative text. This targets a font request; it is not a replacement for waiting for application content or checking the result.

chromedp.Evaluate(`document.fonts.load('16px "Example Font"', 'Screenshot text').then(() => true)`, nil)

Use the family name and weight/style that match the page’s CSS. A resolved promise alone does not prove that the intended font file was successfully used; inspect the page’s font declarations and browser behavior if the result still looks like a fallback.

Wait for asynchronous content separately

Font readiness is not a general network-idle or application-ready signal. If a page fetches data, inserts content, or changes typography after an interaction, wait for the relevant selector or state too. chromedp provides wait actions such as WaitVisible; select a condition that corresponds to the content your image must contain.

err := chromedp.Run(ctx,
    chromedp.Navigate("https://example.com"),
    chromedp.WaitVisible("main article", chromedp.ByQuery),
    chromedp.Evaluate(`document.fonts.ready.then(() => true)`, nil),
    chromedp.FullScreenshot(&image, 100),
)

Waiting for the content selector before fonts is useful when that content introduces text using additional faces. If the application changes the page again after fonts settle, wait for the application-specific final state and ensure its fonts are ready before capture.

5. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. Its one-call screenshot endpoint handles the browser capture for you. See the API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.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://example.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 Node.js example uses the built-in fetch and Bun’s file writer; in Node, save the response bytes with writeFile from node:fs/promises. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. 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. Create a free ScreenshotNeo account.

6. Handle browser emulation and capture details

FullScreenshot changes capture behavior to obtain the full page and overrides device emulation settings according to the chromedp example notes. If your workflow relies on emulated viewport or device metrics, account for this behavior; the example points to device.Reset for resetting emulation and viewport settings. Consult the maintainer example and the API for the version pinned in your module.

Choose PNG when you need lossless output. Choose JPEG by passing a quality from 0 to 99 to FullScreenshot when a compressed photographic image is acceptable. CaptureScreenshot returns the current viewport capture. Check the bytes or open the resulting file when integrating with downstream tools that require a particular format or dimensions.

7. Troubleshoot common failures

Symptom Likely cause Fix
Screenshot uses a fallback font The requested face failed, was not used, or was not the family/weight being awaited. Check font network requests and CSS family, weight, and style. Use document.fonts.load() for a known face and text, then verify the rendered page state.
Screenshot misses content even though fonts are ready Application data or a later UI update has not completed. Wait for a page-specific selector or state in addition to document.fonts.ready.
Navigation or capture exceeds the deadline The page, browser startup, or font resource took longer than the context allows. Inspect browser and network errors, then adjust the timeout to the expected workload. Keep a deadline so work cannot hang indefinitely.
Context is canceled or browser disconnects The Chrome process exited or the DevTools connection was lost; chromedp contexts can be canceled on browser disconnection. Check that Chrome/Chromium is installed and can start in the runtime, and keep the chromedp context alive until the capture finishes. See the chromedp README.
Element capture fails or captures the wrong region The selector is absent, ambiguous, or not visible. Use a selector unique to the target, wait for it to become visible, and confirm the page URL and DOM state.
Full-page image differs from emulated viewport expectations FullScreenshot overrides device emulation settings. Review the screenshot example’s emulation notes and reset or reapply device metrics as needed.
Output file is empty or not created An earlier action failed, or the write error was ignored. Check the error returned by chromedp.Run before writing, and propagate os.WriteFile errors.

8. Performance, reliability, and cost

Waiting on the browser’s readiness promise avoids choosing an arbitrary sleep duration for font loading. A timeout still matters: a page may have slow or stalled resources, so bound the entire navigation-and-capture operation with a context deadline. If the page’s content is asynchronous, use explicit readiness conditions to avoid capturing too early; adding unrelated waits can increase capture time without improving correctness.

Full-page captures can produce larger images than viewport or element captures. Select only the scope required by your downstream task, and choose PNG or JPEG based on fidelity and file-size needs. chromedp is a self-managed browser workflow: your runtime must provide Chrome/Chromium and enough resources for it. No performance benchmark or fixed cost is implied here; browser hosting and execution costs depend on your infrastructure.

For a hosted option, ScreenshotNeo bills only clean shots: bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify page verdict and billing through headers. Plans are Free (1,000 per month), Starter ($5 for 3,000), Growth ($15 for 15,000), Pro ($39 for 60,000), Scale ($99 for 250,000), and Business ($249 for 1,000,000); yearly billing gives two months free. Every feature is on every plan. Check the docs for API behavior.

FAQ

Does document.fonts.ready wait for every font declared in CSS?

No. It resolves for font loading and layout work for fonts used by the document; unused and some optional faces may remain unloaded.

Can I use this with a page behind an interaction?

Yes. Perform the interaction with chromedp, wait for the resulting application state, then wait for the fonts needed by the updated content before capture.

Does chromedp require a visible desktop?

No. Chrome runs headless by default, though Chrome or Chromium still needs to be available to the process.

Which capture action should I use for a long article?

Use FullScreenshot for the entire rendered page. Use viewport or element capture when you need only the visible area or one component.