Artifact b7386eec6aaf0df3f6234ed830b2e654659936807f6b87619946254a945b3758:
- File
internal/web/compress.go
— part of check-in
[d598ecafa3]
at
2026-08-31 16:08:37
on branch trunk
— gzip what benefits, never the event streams
Nothing was compressed: a request carrying Accept-Encoding got byte-identical bytes back. Measured here, the timeline page is 309,584 to 36,599 (88%), a ticket list 84%, a highlighted source page 92%.
gzip rather than brotli deliberately — brotli q5 beat gzip 6 on both size and time, but that is a dependency for ~7% on a page already 88% smaller. SSE is excluded by content type: 22 stream writers, and a buffered stream is a live update that never arrives. (user: quarry, size: 6546) [annotate] [blame] [check-ins using]
// compress.go adds Content-Encoding to responses that benefit from it. // // Measured on this forge before it existed: the timeline page was // 309,584 bytes and gzipped to 36,599 (88% off), a ticket list 209,058 // to 33,194 (84%), a highlighted source page 2,335,223 to 196,463 // (92%). Nothing was compressed — a request carrying `Accept-Encoding: // gzip` got byte-identical bytes back. // // gzip rather than brotli, deliberately. Brotli at quality 5 beat gzip // at 6 on BOTH size (34,551 vs 37,166) and time (3.0ms vs 3.3ms) in the // same benchmark, so it is the better algorithm — but it is a third // party dependency for a ~7% gain on a page already 88% smaller, and // the standard library covers the rest. If a measurement ever justifies // the dependency the negotiation below is where it goes. package web import ( "compress/gzip" "net/http" "strings" "sync" ) // gzipMinBytes is the size below which compressing costs more than it // saves — a header, a trailer and a syscall against a few hundred // bytes. Fragments and small JSON answers fall under it. const gzipMinBytes = 1024 var gzipPool = sync.Pool{New: func() any { return gzip.NewWriter(nil) }} // Compress wraps h so responses are gzipped when the client asked and // the body is worth compressing. func Compress(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !acceptsGzip(r.Header.Get("Accept-Encoding")) { h.ServeHTTP(w, r) return } // Announced whether or not this particular response ends up // compressed: the same URL answers differently by header, and // a cache that does not know that will serve gzip to a client // that cannot read it. w.Header().Add("Vary", "Accept-Encoding") cw := &gzipResponseWriter{ResponseWriter: w} defer cw.Close() h.ServeHTTP(cw, r) }) } // acceptsGzip reads the header rather than searching it for a // substring: "gzip;q=0" is a client saying it does NOT want gzip, and a // contains-check reads that as a yes. func acceptsGzip(header string) bool { for _, part := range strings.Split(header, ",") { name, params, _ := strings.Cut(strings.TrimSpace(part), ";") if !strings.EqualFold(strings.TrimSpace(name), "gzip") { continue } for _, p := range strings.Split(params, ";") { k, v, ok := strings.Cut(strings.TrimSpace(p), "=") if ok && strings.EqualFold(strings.TrimSpace(k), "q") && strings.TrimSpace(v) == "0" { return false } } return true } return false } // gzipResponseWriter decides at WriteHeader time, because that is the // first moment the Content-Type is known. type gzipResponseWriter struct { http.ResponseWriter gz *gzip.Writer decided bool // pending holds bytes written before the decision, which happens // when a handler writes without calling WriteHeader. pending []byte } func (w *gzipResponseWriter) WriteHeader(status int) { w.decide(status) w.ResponseWriter.WriteHeader(status) } // decide installs the encoder, or does not, and must be called before // the header is sent. func (w *gzipResponseWriter) decide(status int) { if w.decided { return } w.decided = true h := w.Header() // Never twice: a handler that already encoded its body owns it. if h.Get("Content-Encoding") != "" { return } // 204 and 304 carry no body; 1xx is not a response yet. if status == http.StatusNoContent || status == http.StatusNotModified || status < 200 { return } if !compressible(h.Get("Content-Type")) { return } h.Set("Content-Encoding", "gzip") // The length of the ENCODED body is not known yet, and a stale one // is worse than none: the client would truncate or hang. h.Del("Content-Length") gz := gzipPool.Get().(*gzip.Writer) gz.Reset(w.ResponseWriter) w.gz = gz } func (w *gzipResponseWriter) Write(b []byte) (int, error) { if !w.decided { // A handler that never called WriteHeader: hold the first bytes // until there are enough to be worth compressing, so a small // fragment is not wrapped in a gzip envelope larger than itself. w.pending = append(w.pending, b...) if len(w.pending) < gzipMinBytes { return len(b), nil } pending := w.pending w.pending = nil w.WriteHeader(http.StatusOK) if w.gz != nil { return writeAll(w.gz, pending, len(b)) } return writeAll(w.ResponseWriter, pending, len(b)) } if w.gz != nil { return w.gz.Write(b) } return w.ResponseWriter.Write(b) } func writeAll(dst interface{ Write([]byte) (int, error) }, buf []byte, report int) (int, error) { if _, err := dst.Write(buf); err != nil { return 0, err } return report, nil } // Flush is what keeps server-sent events working. quarry has 22 stream // writers, and a middleware that buffers turns live updates into a page // that never arrives — the failure mode is silence, which is why the // SSE content type is excluded above rather than relied upon to flush. func (w *gzipResponseWriter) Flush() { if !w.decided && len(w.pending) > 0 { pending := w.pending w.pending = nil w.WriteHeader(http.StatusOK) if w.gz != nil { w.gz.Write(pending) } else { w.ResponseWriter.Write(pending) } } if w.gz != nil { w.gz.Flush() } if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } func (w *gzipResponseWriter) Close() { if !w.decided && len(w.pending) > 0 { pending := w.pending w.pending = nil w.WriteHeader(http.StatusOK) if w.gz != nil { w.gz.Write(pending) } else { w.ResponseWriter.Write(pending) } } if w.gz != nil { w.gz.Close() gzipPool.Put(w.gz) w.gz = nil } } // compressible reports whether a content type is worth encoding. // // text/event-stream is the important exclusion: SSE is a long-lived // stream, and gzip buffers until it has something worth emitting, so a // compressed stream delivers its first event late or never. Images, // fonts and archives are already compressed and only grow. func compressible(ct string) bool { mt, _, _ := strings.Cut(ct, ";") mt = strings.ToLower(strings.TrimSpace(mt)) if mt == "" || mt == "text/event-stream" { return false } switch mt { case "image/png", "image/jpeg", "image/gif", "image/webp", "image/x-icon", "font/woff", "font/woff2", "application/zip", "application/gzip", "application/x-fossil", "application/octet-stream": return false } if strings.HasPrefix(mt, "text/") { return true } switch mt { case "application/json", "application/javascript", "application/xml", "application/rss+xml", "application/atom+xml", "image/svg+xml": return true } return false }