Use chromedp to drive a Chrome DevTools Protocol browser, then save the returned image bytes with a .jpg or .jpeg name. For a true JPEG, set a quality below 100 when using chromedp’s FullScreenshot; in the reviewed implementation, quality 100 selects PNG instead. If you need explicit encoding control, call the lower-level page.CaptureScreenshot command with FormatJpeg and a quality from 0 through 100.
Choose the capture you actually need
A browser screenshot can mean three different things:
- Viewport: only the currently visible browser area.
- Element: one DOM element, such as a chart or product card.
- Full page: content extending below the viewport.
The choice affects the API call. chromedp’s FullScreenshot helper captures beyond the viewport, while ordinary screenshot tasks can target the visible page or an element. The official example shows element and full-page task structures, but writes files with .png names; a filename does not change the bytes’ encoding.
Prerequisites and project shape
You need a Go module, a Chrome/Chromium browser that supports the Chrome DevTools Protocol, and the chromedp and cdproto/page packages. The sources used here do not define a browser-version compatibility matrix or a universal browser-install procedure, so use the browser setup appropriate for your operating system and deployment image.
#1 Best Overall
Initialize a module and add the packages with your normal Go dependency workflow. Keep the browser executable available to the process, and make sure the account running the program can launch it in your environment.
Capture a full webpage as JPEG with chromedp
This complete program navigates to a URL, waits for the document body, captures the full page, and writes the returned bytes to a JPEG file.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/chromedp/chromedp"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible("body", chromedp.ByQuery),
chromedp.FullScreenshot(&image, 90),
)
if err != nil {
panic(err)
}
if err := os.WriteFile("page.jpg", image, 0644); err != nil {
panic(err)
}
fmt.Printf("wrote %d bytes to page.jpgn", len(image))
}
The second argument to FullScreenshot is JPEG quality in the helper’s non-100 path. In the reviewed source, quality 100 selects PNG; values other than 100 select JPEG. Therefore, do not use 100 when your output must be JPEG. The exact visual and file-size trade-off depends on the page and browser encoding; the cited sources do not establish a single best quality value.
Why the extension is not enough
os.WriteFile("page.jpg", image, ...) only chooses a name. It does not transcode data. If the helper produced PNG bytes, the file is still PNG even if it ends in .jpg. Select JPEG in the capture API, then use a matching extension.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Capture only the viewport
For a screenshot of what is currently visible, use the ordinary screenshot action rather than FullScreenshot:
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible("body", chromedp.ByQuery),
chromedp.CaptureScreenshot(&image),
)
if err != nil {
return err
}
if err := os.WriteFile("viewport.jpg", image, 0644); err != nil {
return err
}
Check the helper’s encoding behavior for your dependency version before assuming this action returns JPEG. When format must be unambiguous, use the protocol-level call shown next.
Set JPEG format and quality directly with cdproto/page
The Chrome DevTools Protocol page package exposes screenshot format and JPEG quality explicitly. The protocol documents JPEG quality as 0–100; PNG is the default format unless you choose another format.
package main
import (
"context"
"os"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
func capture(ctx context.Context, url string) error {
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitVisible("body", chromedp.ByQuery),
chromedp.ActionFunc(func(ctx context.Context) error {
data, err := page.CaptureScreenshot().
WithFormat(page.CaptureScreenshotFormatJpeg).
WithQuality(90).
Do(ctx)
if err != nil {
return err
}
image = data
return nil
}),
)
if err != nil {
return err
}
return os.WriteFile("page.jpg", image, 0644)
}
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
if err := capture(ctx, "https://example.com"); err != nil {
panic(err)
}
}
This path makes the requested format visible in your code review: WithFormat(page.CaptureScreenshotFormatJpeg) requests JPEG and WithQuality(90) supplies a value in the documented range. The browser returns encoded image bytes; no second conversion step is required.
Recommended Free Tools
Capture one element
The chromedp example repository demonstrates element screenshot tasks. A typical pattern is to select an element and capture its box:
var image []byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.WaitVisible(".hero", chromedp.ByQuery),
chromedp.Screenshot(".hero", &image, chromedp.ByQuery),
)
if err != nil {
return err
}
return os.WriteFile("hero.jpg", image, 0644)
As with viewport capture, verify the action’s format behavior in the version you use, or replace the action with a protocol call when strict JPEG output matters. If the selector matches nothing, the wait or screenshot action fails; use the selector that exists in the rendered DOM, not merely in server-side source.
Wait for the page you intend to capture
Navigation completion does not guarantee that images, fonts, charts, or client-rendered content are visible. Add a wait that represents your page’s ready state:
- Wait for a stable selector with
WaitVisible. - Use a delay only when the page has no reliable readiness element.
- For application pages, wait for a chart container or a “loaded” state rather than an arbitrary short sleep.
Give the context a deadline. Without one, a stalled navigation can keep a worker occupied indefinitely. A full-page capture can also be much larger than a viewport image, so allocate and store the returned byte slice accordingly.
Rank #4
Common failures and fixes
Chrome cannot be started
Symptom: the context fails before navigation. Cause: no usable Chrome/Chromium executable, missing runtime libraries, or a restricted sandbox. Fix: install or expose a supported browser in the runtime image and follow the launch requirements for that environment. The chromedp project documentation does not promise a single setup for every operating system.
Timeout during navigation
Symptom: the context deadline expires. Fix: confirm the URL is reachable from the worker, increase the deadline for genuinely slow pages, and wait on a page-specific readiness selector rather than capturing immediately.
Blank or incomplete image
Cause: capture ran before client rendering, the selector was hidden, or content requires scrolling or interaction. Fix: wait for visible content, perform the required interaction as a chromedp task, and use FullScreenshot when below-viewport content is required.
The file says JPEG but tools identify PNG
Cause: the bytes were encoded as PNG, commonly because quality 100 selected PNG in FullScreenshot. Fix: choose a non-100 quality for that helper or explicitly set FormatJpeg with the protocol API, then save with a matching extension.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Best Value
Images or fonts are missing
Cause: resources are still loading, blocked by the network, or require a later application state. Fix: wait for a selector that appears only after the resources are ready, and check the page under the same network and authentication conditions as the capture worker.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance, reliability, and cost considerations
- Full page costs more memory: the returned byte slice and encoded image grow with page dimensions and detail.
- Reuse browser infrastructure carefully: creating a context per job gives isolation; a long-lived browser can reduce startup work but needs cleanup and limits for stuck pages.
- Control concurrency: too many simultaneous browser pages can exhaust CPU, memory, file descriptors, or network capacity.
- Make outputs observable: log URL, capture type, elapsed time, byte count, and errors. Do not treat a successful navigation as proof that the image contains the intended content.
- Quality is a trade-off: lower JPEG quality generally changes size and detail, but the supplied API documentation does not provide a benchmark or recommended value.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF, so your Go service can download the response without managing a local browser:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
endpoint := "https://api.screenshotneo.com/v1/shot"
q := url.Values{}
q.Set("access_key", "YOUR_API_KEY")
q.Set("url", "https://stripe.com")
resp, err := http.Get(endpoint + "?" + q.Encode())
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("ScreenshotNeo returned %s", resp.Status))
}
out, err := os.Create("shot.webp")
if err != nil {
panic(err)
}
defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil {
panic(err)
}
}
See the ScreenshotNeo API documentation for request options and response details. Before capture, it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server includes take_screenshot, get_page_info, and capture_pdf 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.
Create a free ScreenshotNeo account to get the 1,000 monthly screenshots without a card.
Free tools Windows power users keep installed
One-click scans. No signup required.
Which approach should you use?
| Need | Best fit | Why |
|---|---|---|
| Local browser control, custom interactions, or authenticated sessions | chromedp | Go tasks drive a real CDP browser and can wait or interact before capture. |
| Strict JPEG format and quality control | cdproto/page | The protocol call exposes format and quality directly. |
| Managed capture without browser installation | ScreenshotNeo #1 | It removes common page clutter, bills only clean shots, and its paid entry plan is $5. |
FAQ
Does a .jpg filename convert PNG bytes?
No. The encoder selected by the screenshot API determines the bytes; the extension is only a label.
What does quality 100 mean in chromedp FullScreenshot?
In the reviewed implementation, quality 100 selects PNG, while other quality values select JPEG.
Can I capture content below the fold?
Yes. Use FullScreenshot or an equivalent protocol capture configured for the full page rather than a viewport-only action.
Is JPEG quality 0–100?
The cdproto/page documentation defines the JPEG quality parameter as 0 through 100.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




