How to Capture Webpages as WebP Images in Go
Capture viewport, full-page, or element screenshots as WebP in Go with Playwright, plus a chromedp conversion path and a hosted API option.
Use Playwright for Go when you need WebP output directly. Its screenshot options support WebP, full-page capture, element capture, quality, scale, and an output path. Use quality 100 for lossless WebP; lower values are lossy. If you already use chromedp, capture PNG or JPEG first and convert it with a WebP encoder because the documented Chrome DevTools screenshot formats are PNG and JPEG.
1. Install Playwright for Go
Create a module and install the Go binding:
mkdir webpage-webp
cd webpage-webp
go mod init example.com/webpage-webp
go get github.com/playwright-community/playwright-go
Install the browser binaries required by the binding:
go run github.com/playwright-community/playwright-go/cmd/playwright install chromium
Pin the binding version in go.mod and check that version’s generated option names. The API surface, including constants, can change between binding versions.
2. Capture a webpage directly as WebP
This complete program opens a page, waits for navigation to finish, captures the entire scrollable document, and writes page.webp.
package main
import (
"log"
"github.com/playwright-community/playwright-go"
)
func main() {
if err := playwright.Install(); err != nil {
log.Fatalf("install Playwright: %v", err)
}
pw, err := playwright.Run()
if err != nil {
log.Fatalf("start Playwright: %v", err)
}
defer pw.Stop()
browser, err := pw.Chromium.Launch()
if err != nil {
log.Fatalf("launch Chromium: %v", err)
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
log.Fatalf("create page: %v", err)
}
if _, err = page.Goto("https://example.com"); err != nil {
log.Fatalf("navigate: %v", err)
}
_, err = page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String("page.webp"),
FullPage: playwright.Bool(true),
Type: playwright.ScreenshotTypeWebp,
Quality: playwright.Int(85),
})
if err != nil {
log.Fatalf("screenshot: %v", err)
}
}
For a minimal one-off script, the playwright.Install() call can be replaced by installing browsers during deployment and simply calling playwright.Run(). Keep browser installation outside request handling in production.
3. Choose viewport, full-page, or element capture
Viewport screenshot
A normal screenshot captures only the current viewport. Set the viewport when creating the page:
page, err := browser.NewPage(playwright.BrowserNewPageOptions{
Viewport: &playwright.Size{Width: 1440, Height: 900},
})
if err != nil { log.Fatal(err) }
if _, err := page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String("viewport.webp"),
Type: playwright.ScreenshotTypeWebp,
Quality: playwright.Int(85),
}); err != nil {
log.Fatal(err)
}
Use viewport capture for above-the-fold previews, responsive regression tests, and social cards.
Full-page screenshot
Set FullPage: true to capture the complete scrollable document. This is useful for articles, documentation, and receipts. Very long pages can create large images and consume more memory.
_, err := page.Screenshot(playwright.PageScreenshotOptions{
Path: playwright.String("full-page.webp"),
FullPage: playwright.Bool(true),
Type: playwright.ScreenshotTypeWebp,
Quality: playwright.Int(80),
})
Element screenshot
Capture the smallest useful scope when you need a card, chart, hero, or component:
card := page.Locator("article .pricing-card").First()
if err := card.Screenshot(playwright.LocatorScreenshotOptions{
Path: playwright.String("pricing-card.webp"),
Type: playwright.ScreenshotTypeWebp,
Quality: playwright.Int(90),
}); err != nil {
log.Fatal(err)
}
Wait for the element before capturing it:
if err := page.Locator("article .pricing-card").WaitFor(); err != nil {
log.Fatal(err)
}
4. WebP quality, scale, and output handling
| Option | What it controls | Practical choice |
|---|---|---|
Type |
Image format | ScreenshotTypeWebp |
Quality |
WebP compression from 0 to 100 | 100 for lossless; 75–90 when smaller files matter |
Scale |
CSS-pixel or device-pixel output | CSS scale for smaller high-DPI assets; device scale for pixel-level fidelity |
Path |
File written by Playwright | Use an explicit temporary path or omit it and persist returned bytes |
FullPage |
Entire scrollable document versus viewport | Enable for long documents |
Quality 100 is lossless in Playwright. Lower values use lossy compression. There is no universal size or speed percentage to quote: page content, dimensions, fonts, and browser conditions dominate the result. Measure your own pages if storage or transfer cost is important.
When you omit Path, save the returned byte slice yourself:
data, err := page.Screenshot(playwright.PageScreenshotOptions{
Type: playwright.ScreenshotTypeWebp,
Quality: playwright.Int(85),
})
if err != nil { log.Fatal(err) }
if err := os.WriteFile("page.webp", data, 0o644); err != nil {
log.Fatal(err)
}
5. Make captures deterministic
- Wait for a meaningful selector instead of relying only on a fixed sleep.
- Use a fixed viewport and timezone when comparing images.
- Wait for fonts and critical images before capture.
- Disable animations with injected CSS when visual diffs must be stable.
- Use an element screenshot when a full document is unnecessary.
if _, err := page.Goto("https://example.com/dashboard"); err != nil {
log.Fatal(err)
}
if err := page.Locator("main.dashboard").WaitFor(); err != nil {
log.Fatal(err)
}
if _, err := page.AddStyleTag(playwright.PageAddStyleTagOptions{
Content: playwright.String(`*, *::before, *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}`),
}); err != nil {
log.Fatal(err)
}
Lazy-loaded images may not exist until they enter the viewport. For a full-page capture, scroll through the page or trigger the site’s lazy-loading behavior before taking the screenshot.
6. The chromedp alternative: capture, then convert
chromedp provides navigation, viewport screenshots, element screenshots, and full screenshots. Its documented full-screenshot behavior returns PNG when quality is 100 and JPEG otherwise. The underlying Chrome DevTools screenshot format list does not include WebP, so add a conversion step.
package main
import (
"context"
"image"
_ "image/jpeg"
_ "image/png"
"log"
"os"
"github.com/chromedp/chromedp"
"github.com/chai2010/webp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var pngBytes []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.FullScreenshot(&pngBytes, 100),
)
if err != nil {
log.Fatal(err)
}
img, _, err := image.Decode(bytes.NewReader(pngBytes))
if err != nil {
log.Fatal(err)
}
out, err := os.Create("page.webp")
if err != nil {
log.Fatal(err)
}
defer out.Close()
if err := webp.Encode(out, img, &webp.Options{Lossless: false, Quality: 85}); err != nil {
log.Fatal(err)
}
}
Add bytes to the imports in that example. The conversion library is a separate dependency; select and pin the encoder used by your project. With chromedp, the browser capture and WebP encoding are two distinct failure points, so check both errors.
7. Common errors and fixes
| Error or symptom | Cause | Fix |
|---|---|---|
| Browser executable not found | Chromium was not installed in the runtime image. | Run the Playwright browser install command during image build, or provide a managed browser path. |
| Blank or partially rendered image | Capture ran before the app finished rendering. | Wait for a stable selector, fonts, and required network resources. |
| Full page stops before lazy images | Images load only after scrolling. | Scroll incrementally or trigger lazy loading before capture. |
| WebP option does not compile | Binding version uses different generated names. | Inspect the installed version’s PageScreenshotOptions and WebP constant, then pin that version. |
| Huge output file | Lossless quality, device-pixel scale, or a very tall page. | Use a lower quality, CSS scale, element capture, or split the document. |
| Fonts differ in CI | Fonts are missing or loaded at different times. | Install the required fonts and wait for document.fonts.ready. |
| Navigation timeout | The page or a third-party resource never finishes. | Set a deliberate timeout, wait for the app’s ready selector, and handle the failed capture. |
| chromedp output is JPEG instead of WebP | Chrome’s screenshot action produced PNG/JPEG only. | Capture PNG and run a separate WebP encoder. |
8. Reliability, performance, and cost considerations
Reliability checklist
- Reuse a browser process when taking many screenshots, but create isolated pages or contexts per job.
- Close pages and browsers on every error path.
- Set navigation and screenshot timeouts and record the target URL with failures.
- Write to a temporary file, verify it, then rename it atomically when files must never be partial.
- Limit concurrent full-page captures because tall pages consume memory.
Performance checklist
- Capture an element or viewport when full-page output is not required.
- Use CSS scale when device-pixel output is unnecessary.
- Block nonessential resources only when doing so cannot change the visual result.
- Reuse browser processes and avoid reinstalling browser binaries per request.
- Choose WebP quality based on an application-specific visual review.
Screenshot cost in a self-hosted Go service includes browser CPU, memory, storage, and any CI or compute time. The dossier provides no authoritative benchmark for WebP savings or capture speed, so size and latency claims should come from your own pages.
9. Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. Its capture pipeline accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
See the ScreenshotNeo API documentation for all options, including full-page and element capture, dark mode, device presets, retina scale, custom CSS and JavaScript, waits, blocking rules, headers, cookies, user agents, geolocation, caching, signed links, asynchronous jobs, webhooks, bulk capture, and usage reporting.
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 failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also includes an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.
10. FAQ
Can Playwright for Go save WebP without another encoder?
Yes. Set the screenshot type to WebP and provide a quality value. The binding writes WebP bytes directly.
Is quality 100 always the best choice?
Quality 100 is lossless, but it can create larger files. Choose a lower value when a smaller asset is more useful and validate the visual result.
Should I use full-page or element capture?
Use full-page for an entire document and element capture for a component. The smaller scope usually reduces memory use and makes the output easier to process.
Can chromedp produce WebP directly?
Its documented screenshot formats are PNG and JPEG. Capture one of those and convert it with a separately selected WebP encoder.
How do I make screenshots repeatable?
Fix the viewport, wait for stable selectors and fonts, disable animations, and control lazy-loaded content before capture.


