Capture a Website Screenshot in Go with chromedp and Wait for Images to Load
Use chromedp to capture an element, viewport, or full page after current HTML images finish loading, with practical handling for lazy-loaded and failed images.
Navigate with chromedp, wait on a JavaScript predicate that checks the page’s current <img> elements, then capture the viewport, an element, or the full page. For an image to count as successfully loaded, check both img.complete and img.naturalWidth > 0. The predicate below is a practical baseline; it does not automatically trigger lazy loading or account for CSS background images, canvas, or later application updates.
1. Install chromedp and run a complete example
Start with a Go module and add chromedp:
go mod init example.com/chromedp-shot
go get github.com/chromedp/chromedp
Save this as main.go. It navigates to a URL, polls until all current DOM images are loaded successfully, and saves a full-page PNG. Change targetURL to the page you control or are authorized to capture.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
const imagesLoaded = `Array.from(document.images).every(img => img.complete && img.naturalWidth > 0)`
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, "screenshot:", err)
os.Exit(1)
}
}
func run() error {
targetURL := "https://example.com"
// A timeout prevents navigation or a wait from hanging forever.
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.Poll(imagesLoaded, nil),
chromedp.FullScreenshot(&image, 100),
)
if err != nil {
return err
}
return os.WriteFile("page.png", image, 0644)
}
The actions run in order: navigation, polling, then capture. Errors are returned before the file is written, so a failed wait does not silently produce an output file that looks successful. FullScreenshot with quality 100 produces PNG; lower quality values produce JPEG.
For production use, pin and review your module dependencies through your normal Go dependency process. chromedp controls a browser that supports the Chrome DevTools Protocol; the browser executable and runtime environment must also be available where the program runs.
2. Choose what to capture
Keep the wait before the capture action. The capture target determines which chromedp action to use:
| Target | Action | Notes |
|---|---|---|
| Selected element | chromedp.Screenshot(selector, &image, ...) |
Captures the selected element. Use a selector that uniquely identifies the intended content. |
| Current viewport | chromedp.CaptureScreenshot(&image) |
Captures the visible browser viewport. Set the viewport or device emulation before navigation when those dimensions matter. |
| Full page | chromedp.FullScreenshot(&image, 100) |
Captures beyond the viewport. Quality 100 selects PNG; lower supported values select JPEG. This action overrides device emulation settings. |
For an element capture, the essential sequence is:
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate(targetURL),
chromedp.Poll(imagesLoaded, nil),
chromedp.Screenshot("main article", &image),
)
if err != nil {
return err
}
if err := os.WriteFile("article.png", image, 0644); err != nil {
return err
}
Use a selector appropriate to the target page; main article is only an example. For lower-level viewport capture or a custom clipped area, the Chrome DevTools Protocol capture API exposes format, quality, clipping, surface capture, and capture-beyond-viewport parameters. chromedp.CaptureScreenshot captures the current viewport.
3. What the image wait checks—and what it misses
chromedp.Poll waits for a JavaScript predicate. The predicate in the runnable example checks every image currently in document.images:
img.completeindicates the image has completed loading or failed.img.naturalWidth > 0distinguishes an image with usable intrinsic dimensions from a broken or empty image.Array.from(document.images).every(...)requires every current DOM image to pass. With no images,everyreturns true.
Checking complete alone is not enough: it can also be true when an image has no source or when loading failed. The positive-width condition prevents a broken image from being treated as a successful load.
This is a snapshot of the DOM images at the time the predicate runs, not a promise that the entire page has settled. A page can insert more images after the predicate succeeds, and a failed image keeps the predicate false until the polling action times out. Pick the failure behavior that fits your use case: fail the capture when every image is required, or define an application-specific readiness predicate if broken images are acceptable and should not hold up capture.
Lazy-loaded images
Images with lazy loading may not begin loading until they approach the viewport. A full-page screenshot does not guarantee that scrolling has triggered every image. If below-the-fold images matter, scroll through the page to trigger the site’s intended loading behavior, then wait for the resulting images before capturing. Pages vary in how they load content, so use a page-specific signal where possible. The basic predicate alone cannot know that an image is still waiting for a scroll event.
Images outside document.images
The predicate does not cover CSS background images, images drawn into canvas, or images managed outside the DOM image elements. It also does not wait for fonts, video frames, animations, or application work that begins after image loading. For these cases, trigger the page’s intended behavior and wait for a site-specific JavaScript condition that represents the content you need. There is no single generic chromedp wait that covers every such pattern.
Element readiness is a different condition
chromedp.WaitReady and chromedp.WaitVisible can be useful when an element must exist or be visible. They do not establish that all images on the page have loaded. Combine an element wait with an image predicate if both conditions matter.
4. Timeouts and reliable capture behavior
Use a context deadline appropriate for the pages and environment you capture. Navigation and image loading can both take time; a deadline bounds the overall operation. When chromedp.Run returns an error, report it and do not treat the screenshot as complete. If your application can tolerate missing images, handle that as an explicit policy instead of removing the timeout or ignoring all errors.
- Keep navigation, readiness checks, and capture in one ordered
chromedp.Runsequence. - Return errors from
chromedp.Runand from writing the output file. - Choose the capture target before deciding whether to use viewport emulation: full-page capture overrides device emulation settings.
- For dynamic pages, wait on an application-specific signal as well as—or instead of—a generic image condition.
The example uses a 60-second overall deadline as a starting point, not a guarantee that every site will finish in that time. Adjust it to your workload and failure policy.
5. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| The wait times out | An image failed, has no usable source, or is still loading; alternatively, the page never finished navigation within the deadline. | Inspect the page’s images and network behavior. Decide whether every image must succeed. If some failures are acceptable, use a page-specific predicate that reflects that policy. |
| The screenshot is missing images lower on the page | Lazy loading did not start because those images were not brought into view. | Scroll through the relevant content to trigger loading, then wait again before capturing. |
| The image wait passes but a visual is absent | The visual may be a CSS background, canvas drawing, or content inserted after the predicate succeeded. | Trigger the relevant page behavior and wait on an application-specific completion signal. |
| The output file is empty or missing | The capture returned an error, or the write failed. | Check and return both the chromedp.Run error and os.WriteFile error before reporting success. |
| The mobile or emulated layout is not reflected in a full-page image | FullScreenshot overrides device emulation settings. |
Choose the capture action based on the required output and account for that behavior when planning emulated captures. |
| The image is JPEG when PNG was expected | FullScreenshot uses quality to choose the format. |
Use quality 100 for PNG; lower supported quality values produce JPEG. |
6. Performance, reliability, and cost
Waiting for every current DOM image can increase capture time, especially on pages with slow or unreliable assets. The page may also never satisfy the strict predicate if one image stays broken, so use a deadline and choose deliberately whether a failed image should block the result. Scrolling to trigger lazy loading adds more page work and should be limited to the content needed for the capture.
The Go example runs a browser and writes an image locally; resource use and operating cost depend on the browser environment and your workload. The reviewed chromedp references do not provide a universal throughput benchmark or per-capture cost, so measure against the pages and runtime you actually use. For repeat jobs, record capture failures separately from successful output and avoid retrying indefinitely.
7. cURL, Python, and Node.js alternatives
These are ScreenshotNeo API examples for developers who want a managed screenshot request instead of operating a Chrome browser through chromedp. See the ScreenshotNeo API documentation for request options and formats.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
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', new Uint8Array(await res.arrayBuffer()));
The Node.js example uses Bun’s file-writing API. In a Node.js project, replace the final line with your chosen filesystem write method, such as writeFile from node:fs/promises.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns a screenshot or PDF. Cookie banners, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
One thousand screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots. Every feature is available on every plan. Create a free ScreenshotNeo account to get started.
9. FAQ
Does WaitVisible wait for an image to download?
No. It waits for element visibility. Check image loading separately with a predicate or a page-specific readiness condition.
Should one broken image prevent the screenshot?
That depends on the use case. The strict example waits for every current DOM image to have positive natural width. Keep that behavior when completeness is required; otherwise define a readiness condition that permits known optional failures.
Can FullScreenshot preserve device emulation?
It overrides device emulation settings. Account for that when choosing the capture sequence and target.
Does the predicate include CSS background images?
No. It checks current document.images elements. Use a page-specific condition for backgrounds, canvas, or application-managed visuals.


