Gzip the text responses
Nothing was compressed: the list page shipped ~123 KB of HTML, CSS and JS in the clear on every cold load, which is the wrong bill for a phone on mobile data. A stdlib middleware compresses the four content types this server actually serves in bulk and leaves woff2 alone, since it is already compressed. Measured on the live server: style.css 21.8 KB -> 6.5, htmx 50.9 -> 16.4, filter.js 7.6 -> 2.9. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -26,6 +27,71 @@ func withAuth(token string, next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// gzipWriter compresses the body and drops Content-Length, which no longer
|
||||
// describes what goes on the wire. WriteHeader is where the decision is made:
|
||||
// only then is Content-Type known.
|
||||
type gzipWriter struct {
|
||||
http.ResponseWriter
|
||||
gz *gzip.Writer
|
||||
decided bool
|
||||
}
|
||||
|
||||
// compressible covers what this server actually serves in bulk: HTML, CSS, JS
|
||||
// and JSON. Fonts are woff2, which is already compressed — gzipping them costs
|
||||
// CPU to add bytes.
|
||||
func compressible(contentType string) bool {
|
||||
ct, _, _ := strings.Cut(contentType, ";")
|
||||
switch strings.TrimSpace(ct) {
|
||||
case "text/html", "text/css", "text/javascript", "application/javascript",
|
||||
"application/json", "text/plain":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *gzipWriter) WriteHeader(status int) {
|
||||
if !w.decided {
|
||||
w.decided = true
|
||||
if compressible(w.Header().Get("Content-Type")) {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Del("Content-Length")
|
||||
w.gz = gzip.NewWriter(w.ResponseWriter)
|
||||
}
|
||||
}
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *gzipWriter) Write(b []byte) (int, error) {
|
||||
if !w.decided {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if w.gz != nil {
|
||||
return w.gz.Write(b)
|
||||
}
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// withGzip compresses text responses for clients that ask. The templates,
|
||||
// stylesheet and htmx together are ~120 KB uncompressed and roughly a quarter
|
||||
// of that gzipped, which is the difference between a fast and a slow first load
|
||||
// on mobile data.
|
||||
func withGzip(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Add("Vary", "Accept-Encoding")
|
||||
gw := &gzipWriter{ResponseWriter: w}
|
||||
defer func() {
|
||||
if gw.gz != nil {
|
||||
gw.gz.Close()
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(gw, r)
|
||||
})
|
||||
}
|
||||
|
||||
// withCORS reflects the request Origin only when it is in allowed, answers
|
||||
// preflight OPTIONS with 204, and passes everything else through. It wraps the
|
||||
// auth middleware so preflight (which carries no Authorization header) is never
|
||||
|
||||
Reference in New Issue
Block a user