Screenshot Webpages as PNG in Go
Capture viewport, full-page, or element screenshots as PNG in Go with chromedp, then compare a managed ScreenshotNeo API workflow.
Use the maintained chromedp Go package with a Chromium-based browser. Navigate to the page, run a screenshot action, and write the returned bytes to a file. For a full-page PNG, call chromedp.FullScreenshot(&png, 100); quality 100 selects PNG output.
1. Set up a Go screenshot program
Chromedp drives Chromium through the Chrome DevTools Protocol (CDP). A Go image library by itself cannot render HTML, CSS, JavaScript, fonts, or modern layout; a browser engine is required.
Install the dependency
go mod init example.com/webshot
go get github.com/chromedp/chromedp
Make Chromium or Chrome available in the runtime environment. In containers and CI, install a browser image or package and ensure the executable is on PATH. If the browser is in a custom location, create an allocator with an explicit executable path before creating the browser context.
Minimal full-page PNG example
package main
import (
"context"
"log"
"os"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var png []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.FullScreenshot(&png, 100),
)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("page.png", png, 0o644); err != nil {
log.Fatal(err)
}
}
The returned slice contains binary image data. Use os.WriteFile, an io.Writer, or an object-storage upload; do not convert the bytes to a string.
2. Choose the capture scope
| Goal | Action | Result |
|---|---|---|
| Visible browser area | chromedp.CaptureScreenshot(&buf) |
PNG bytes for the current viewport |
| One element | chromedp.Screenshot("#content", &buf, chromedp.NodeVisible) |
The first matching visible element |
| Entire page | chromedp.FullScreenshot(&buf, 100) |
Full-page PNG |
Viewport screenshot
var buf []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.CaptureScreenshot(&buf),
)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("viewport.png", buf, 0o644); err != nil {
log.Fatal(err)
}
CaptureScreenshot captures the current browser viewport, as documented in the chromedp screenshot source.
Element screenshot
var elementPNG []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible("#content", chromedp.ByID),
chromedp.Screenshot("#content", &elementPNG, chromedp.NodeVisible),
)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("content.png", elementPNG, 0o644); err != nil {
log.Fatal(err)
}
Use a selector that is stable for the page. If several nodes match, chromedp captures the first matching element. A missing selector causes the action to fail, so wait for it when the page renders asynchronously.
3. Control the browser before capture
Set a viewport and device scale
Viewport dimensions affect responsive breakpoints and the amount of content visible in a viewport capture. The screenshot scale affects output density; chromedp.ScreenshotScale changes the page scale factor.
package main
import (
"context"
"log"
"os"
"github.com/chromedp/cdproto/emulation"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var png []byte
err := chromedp.Run(ctx,
emulation.SetDeviceMetricsOverride(1440, 900, 1, false),
chromedp.Navigate("https://example.com"),
chromedp.CaptureScreenshot(&png),
)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("desktop.png", png, 0o644); err != nil {
log.Fatal(err)
}
}
For reproducible visual comparisons, keep the viewport, scale, browser version, fonts, timezone, and page state consistent.
Wait for dynamic content
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/dashboard"),
chromedp.WaitVisible("[data-ready='true']", chromedp.ByQuery),
chromedp.Sleep(500*time.Millisecond),
chromedp.FullScreenshot(&png, 100),
)
Prefer a meaningful readiness selector over an arbitrary delay. A short delay is useful after the selector appears when fonts, transitions, or images still settle.
Lazy-loaded images
Full-page capture records the rendered page. Pages that load images only after scrolling may need a scroll action before capture so those resources enter the viewport and load.
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com/gallery"),
chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight)`, nil),
chromedp.Sleep(500*time.Millisecond),
chromedp.FullScreenshot(&png, 100),
)
Use a page-specific readiness signal where possible; scrolling alone does not guarantee that every application has finished its network work.
4. Clip a region with the DevTools Protocol
The underlying CDP method is Page.captureScreenshot. Its parameters include format, clip, and captureBeyondViewport. The chromedp binding returns decoded image bytes. See the Page.captureScreenshot documentation.
import "github.com/chromedp/cdproto/page"
var png []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.ActionFunc(func(ctx context.Context) error {
data, err := page.CaptureScreenshot().
WithFormat(page.CaptureScreenshotFormatPng).
WithCaptureBeyondViewport(true).
WithClip(&page.Viewport{X: 0, Y: 0, Width: 800, Height: 600, Scale: 1}).
Do(ctx)
if err != nil {
return err
}
png = data
return nil
}),
)
if err != nil {
log.Fatal(err)
}
Use a clip when you need a fixed rectangle. Coordinates are page and viewport dependent, so calculate them from the page layout when the target can move.
5. PNG versus JPEG
FullScreenshot keeps PNG output when quality is 100. Quality values below 100 select JPEG output. PNG is usually the safer choice for text, diagrams, and pixel comparisons; JPEG can reduce files for photographic content but introduces lossy artifacts.
6. A reusable capture function
package shot
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
func FullPagePNG(parent context.Context, url, output string) error {
ctx, cancel := context.WithTimeout(parent, 90*time.Second)
defer cancel()
var png []byte
if err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.FullScreenshot(&png, 100),
); err != nil {
return fmt.Errorf("capture %s: %w", url, err)
}
if err := os.WriteFile(output, png, 0o644); err != nil {
return fmt.Errorf("write %s: %w", output, err)
}
return nil
}
Use a context timeout so a stalled navigation cannot occupy a worker indefinitely. Create separate browser contexts for independent jobs, and cancel each context when its job ends.
7. Or skip the browser setup
ScreenshotNeo provides a GET screenshot API when you want a managed browser capture from Go or any HTTP client. Its default call returns a WebP image; request PNG with the API parameters documented at the ScreenshotNeo docs.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
q := url.Values{}
q.Set("access_key", "YOUR_API_KEY")
q.Set("url", "https://stripe.com")
q.Set("format", "png")
res, err := http.Get("https://api.screenshotneo.com/v1/shot?" + q.Encode())
if err != nil {
panic(err)
}
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
body, _ := io.ReadAll(res.Body)
panic(fmt.Sprintf("ScreenshotNeo returned %s: %s", res.Status, body))
}
out, err := os.Create("shot.png")
if err != nil {
panic(err)
}
defer out.Close()
if _, err := io.Copy(out, res.Body); err != nil {
panic(err)
}
}
The same endpoint works from cURL, Python, and Node.js:
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. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server gives Claude, Cursor, and other MCP clients take_screenshot, get_page_info, and capture_pdf tools. One thousand screenshots per month are free with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser cannot start | Chromium is missing or not discoverable | Install Chromium/Chrome in the runtime, verify its executable path, and configure a chromedp allocator when it is not on PATH. |
| Navigation times out | Slow, blocked, or never-ending page load | Use a context timeout, wait for a specific readiness selector, and investigate the target URL from the same network environment. |
| Element screenshot is empty or fails | Selector does not match, element is hidden, or rendering is not complete | Check the selector, use WaitVisible, and capture with chromedp.NodeVisible. |
| Full page misses images | Images are lazy-loaded | Scroll through the page or trigger the application’s own load mechanism, then wait for completion. |
| Output is JPEG | FullScreenshot quality is below 100 | Pass 100 for PNG. |
| File is corrupted | Image bytes were treated as text | Write the byte slice directly with os.WriteFile or copy the response body to a binary file. |
| Different pixels between runs | Responsive layout, fonts, animations, time, or remote data changed | Fix viewport and scale, wait for a stable selector, disable or await animations, and keep browser and font versions consistent. |
9. Performance, reliability, and cost considerations
- Browser startup and page rendering dominate work; reuse a browser process where your service architecture allows it, while keeping job contexts isolated.
- Limit concurrency according to available CPU and memory. The supplied sources provide no universal speed, memory, or PNG-size benchmark, so measure with your own pages and runtime.
- Use explicit timeouts and cancellation. Treat navigation, selector waits, screenshot capture, and file upload as separate failure points.
- PNG preserves sharp text but can be larger than JPEG. Store or stream bytes instead of holding many full-page images in memory at once.
- For API usage, cache only when the page can be stale. ScreenshotNeo supports caching with a caller-selected TTL, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, custom headers/cookies/user agents, blocking controls, and signed links for public image tags.
- ScreenshotNeo bills only clean shots; bot checks, blank pages, timeouts, failed loads, and cache hits are free. Check
X-Page-VerdictandX-Billedwhen reconciling usage.
10. Practical checklist
- Chromium is installed and reachable by the process.
- The URL is navigable from the capture environment.
- The viewport and scale match the intended output.
- A readiness selector or other deterministic wait is used for dynamic pages.
- Lazy-loaded content has been triggered before a full-page capture.
- PNG bytes are written or uploaded as binary data.
- Contexts have deadlines and are canceled after each job.
- Failures are logged with the URL and action that failed.
FAQ
Can Go take a webpage screenshot without a browser?
Not for a modern rendered webpage. HTML, CSS, JavaScript, fonts, and layout require a browser engine such as Chromium. Go can then save the resulting bytes.
What is the simplest full-page PNG call?
chromedp.FullScreenshot(&png, 100) after chromedp.Navigate.
How do I screenshot only a CSS selector?
Call chromedp.Screenshot("#content", &buf, chromedp.NodeVisible), usually after waiting for the selector.
Can I capture a page larger than the viewport?
Use FullScreenshot for the complete page, or call CDP Page.captureScreenshot with a clip and captureBeyondViewport when you need explicit geometry.
When should I use an API instead of chromedp?
Use chromedp when you need browser-level control in your Go process. Use ScreenshotNeo when you prefer one HTTP call, automatic removal of consent banners and overlays, verdict-based billing, or MCP tools for AI agents.


