PNG-to-WebP in Go has no pure-Go path for encoding — every viable library wraps libwebp or libvips. The choice is mostly about deployment: do you want native deps in your binary, or do you call out to a service? Once you have libwebp installed, performance is excellent.
Method 1: chai2010/webp (libwebp via cgo)
chai2010/webp is the most-used WebP encoder in Go. It wraps Google's libwebp via cgo with a clean image.Image interface.
apt install libwebp-dev # or brew install webp
go get github.com/chai2010/webp
package main
import (
"fmt"
"image/png"
"os"
"github.com/chai2010/webp"
)
func pngToWebP(inPath, outPath string, quality float32, lossless bool) error {
in, err := os.Open(inPath)
if err != nil {
return fmt.Errorf("open: %w", err)
}
defer in.Close()
img, err := png.Decode(in)
if err != nil {
return fmt.Errorf("decode: %w", err)
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
opts := &webp.Options{Quality: quality, Lossless: lossless}
return webp.Encode(out, img, opts)
}
func main() {
if err := pngToWebP("hero.png", "hero.webp", 85, false); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := pngToWebP("diagram.png", "diagram.webp", 0, true); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Two knobs that matter:
- Quality (0-100, float32) — 85 is the sweet spot for photos. Below 70 you'll see banding.
- Lossless — true for diagrams, screenshots, sharp-edge graphics. Quality is ignored when lossless is set.
cgo means you need a C compiler at build time and libwebp on the target system. For Lambda/Alpine, this often means adding gcc and musl-dev to the build image.
Method 2: govips (libvips via cgo, fastest)
govips wraps libvips — the same engine sharp uses. It's 5-10x faster than libwebp alone and handles batch pipelines elegantly.
apt install libvips-dev # or brew install vips
go get github.com/davidbyttow/govips/v2/vips
package main
import (
"fmt"
"os"
"github.com/davidbyttow/govips/v2/vips"
)
func pngToWebP(inPath, outPath string, quality int, lossless bool) error {
img, err := vips.NewImageFromFile(inPath)
if err != nil {
return fmt.Errorf("load: %w", err)
}
defer img.Close()
params := vips.NewWebpExportParams()
params.Quality = quality
params.Lossless = lossless
params.ReductionEffort = 6 // 0-6, higher = smaller files
bytes, _, err := img.ExportWebp(params)
if err != nil {
return fmt.Errorf("export: %w", err)
}
return os.WriteFile(outPath, bytes, 0o644)
}
func main() {
vips.Startup(nil)
defer vips.Shutdown()
if err := pngToWebP("hero.png", "hero.webp", 85, false); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
libvips is the fastest option in the Go ecosystem. ReductionEffort=6 is the slowest but smallest; for hot paths use 4 (default) for the speed/size tradeoff. Always call vips.Startup once at process start and Shutdown at exit.
Method 3: ChangeThisFile API (no cgo, no native libs)
If you can't use cgo (Lambda without custom layers, distroless containers, edge runtimes), the API does it. Free tier covers 1,000 conversions/month.
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
const apiKey = "ctf_sk_your_key_here"
func pngToWebP(inPath, outPath string) error {
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
f, err := os.Open(inPath)
if err != nil {
return err
}
defer f.Close()
fw, err := w.CreateFormFile("file", "input.png")
if err != nil {
return err
}
if _, err := io.Copy(fw, f); err != nil {
return err
}
_ = w.WriteField("source", "png")
_ = w.WriteField("target", "webp")
_ = w.Close()
req, err := http.NewRequest("POST", "https://changethisfile.com/v1/convert", body)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", w.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(resp.Body)
return fmt.Errorf("api %d: %s", resp.StatusCode, msg)
}
out, err := os.Create(outPath)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, resp.Body)
return err
}
func main() {
if err := pngToWebP("hero.png", "hero.webp"); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The API runs libvips server-side. Quality is identical to method 2 without your binary needing to link libvips.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| chai2010/webp | Single image conversions, simple setup | cgo + libwebp dependency |
| govips | Batch jobs, max performance, image pipelines | cgo + libvips (~30MB), Startup/Shutdown lifecycle |
| ChangeThisFile API | Lambda, distroless, edge runtimes | Network call, file size limit (25MB free) |
CLI alternative: cwebp
For one-off conversions or shell pipelines, Google's cwebp binary is the simplest option:
apt install webp # provides cwebp
cwebp -q 85 -m 6 hero.png -o hero.webp
cwebp -lossless diagram.png -o diagram.webp
# Batch with find:
find . -name "*.png" -exec sh -c 'cwebp -q 85 "$1" -o "${1%.png}.webp"' _ {} \;
// From Go, shell out via os/exec:
cmd := exec.Command("cwebp", "-q", "85", "-m", "6", "hero.png", "-o", "hero.webp")
out, err := cmd.CombinedOutput()
cwebp uses the same encoder as chai2010/webp (libwebp), so quality is identical. Use cwebp for ad-hoc conversions; use chai2010 or govips when WebP encoding is part of a larger Go pipeline.
Production tips
- For govips, manage the lifecycle. Call
vips.Startup(nil)once at process start,vips.Shutdown()on exit. Forgetting Startup means leaks; forgetting Shutdown leaves cached threads. - Use Quality=85 as the default. Below 70 you'll see banding on photos. Above 90 you get little visible gain at much larger file sizes.
- Lossless for diagrams, lossy for photos. A 1MB PNG diagram can become a 100KB lossless WebP. The same diagram at quality=85 lossy might be larger because lossy encoding penalizes hard edges.
- For batch jobs, parallelize with bounded concurrency. libvips releases the goroutine, so a worker pool with numCPU workers maxes out the host without OOM.
- Strip metadata for web-served images. EXIF and ICC profiles add KB. govips: img.RemoveMetadata(); chai2010: re-encode without metadata.
For most Go services, chai2010/webp with quality=85 is the right default. For high-throughput batch jobs, govips is significantly faster. For environments where cgo is a hassle, the API. Free tier covers 1,000 conversions/month at 25MB max per file.