dmitri.shuralyov.com/website/gido/...

Apply gzip compression to dynamic pages.

It should be beneficial to reduce bandwidth and speed up page load time.
Many dynamic pages will compress well (e.g., from 521 KB to 27.2 KB).
This is in large part because inline SVG are often repeated many times.

Only do so if the handler hasn't already taken care of compression.
This is determined by whether the Content-Encoding header is set
by the time WriteHeader is called.
dmitshur committed 5 months ago commit 58d6128437a163a2dd5351a1b77fbe5b892e6398
main.go
@@ -72,17 +72,17 @@ func run(ctx context.Context, router Router, analyticsFile string) error {
 		if err != nil {
 			return err
 		}
 	}
 
-	server := &http.Server{Addr: *httpFlag, Handler: top{&errorHandler{handler: (&handler{
+	server := &http.Server{Addr: *httpFlag, Handler: top{gzipHandler{&errorHandler{handler: (&handler{
 		rtr:           router,
 		analyticsHTML: analyticsHTML,
 		fontsHandler:  httpgzip.FileServer(assets.Fonts, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}),
 		assetsHandler: httpgzip.FileServer(assets.Assets, httpgzip.FileServerOptions{ServeError: httpgzip.Detailed}),
 		s:             newService(ctx),
-	}).ServeHTTP}}}
+	}).ServeHTTP}}}}
 
 	go func() {
 		<-ctx.Done()
 		err := server.Close()
 		if err != nil {
util.go
@@ -1,19 +1,22 @@
 package main
 
 import (
+	"compress/gzip"
 	"context"
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"net/http"
 	"net/url"
 	"os"
 	"time"
 
 	"github.com/shurcooL/httperror"
 	"github.com/shurcooL/users"
+	"golang.org/x/net/http/httpguts"
 )
 
 // errorHandler factors error handling out of the HTTP handler.
 type errorHandler struct {
 	handler func(w http.ResponseWriter, req *http.Request) error
@@ -105,10 +108,80 @@ func (rw *headerResponseWriter) Write(p []byte) (n int, err error) {
 func (rw *headerResponseWriter) WriteHeader(code int) {
 	rw.WroteHeader = true
 	rw.ResponseWriter.WriteHeader(code)
 }
 
+// gzipHandler applies gzip compression on top of Handler, unless Handler
+// has already handled it (i.e., the "Content-Encoding" header is set).
+type gzipHandler struct {
+	Handler http.Handler
+}
+
+func (h gzipHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	// If request doesn't accept gzip encoding, serve without compression.
+	if !httpguts.HeaderValuesContainsToken(req.Header["Accept-Encoding"], "gzip") {
+		h.Handler.ServeHTTP(w, req)
+		return
+	}
+
+	// Otherwise, use gzipResponseWriter to start gzip compression when WriteHeader
+	// is called, but only if the handler didn't already take care of it.
+	rw := &gzipResponseWriter{ResponseWriter: w}
+	defer rw.Close()
+	h.Handler.ServeHTTP(rw, req)
+}
+
+// gzipResponseWriter starts gzip compression when WriteHeader is called, unless compression
+// has already been applied by that time (i.e., the "Content-Encoding" header is set).
+type gzipResponseWriter struct {
+	http.ResponseWriter
+
+	// These fields are set by setWriterAndCloser
+	// during first call to Write or WriteHeader.
+	w io.Writer // When set, must be non-nil.
+	c io.Closer // May be nil.
+}
+
+func (rw *gzipResponseWriter) WriteHeader(code int) {
+	if rw.w != nil {
+		panic(fmt.Errorf("internal error: gzipResponseWriter: WriteHeader called twice or after Write"))
+	}
+	rw.setWriterAndCloser()
+	rw.ResponseWriter.WriteHeader(code)
+}
+func (rw *gzipResponseWriter) Write(p []byte) (n int, err error) {
+	if rw.w == nil {
+		rw.setWriterAndCloser()
+	}
+	return rw.w.Write(p)
+}
+
+func (rw *gzipResponseWriter) setWriterAndCloser() {
+	if _, ok := rw.Header()["Content-Encoding"]; ok {
+		// Compression already handled by the handler.
+		rw.w = rw.ResponseWriter
+		return
+	}
+
+	// Update headers, start using a gzip writer.
+	rw.Header().Set("Content-Encoding", "gzip")
+	rw.Header().Del("Content-Length")
+	gw := gzip.NewWriter(rw.ResponseWriter)
+	rw.w = gw
+	rw.c = gw
+}
+
+func (rw *gzipResponseWriter) Close() {
+	if rw.c == nil {
+		return
+	}
+	err := rw.c.Close()
+	if err != nil {
+		log.Printf("gzipResponseWriter.Close: error closing *gzip.Writer: %v", err)
+	}
+}
+
 // top adds some instrumentation on top of Handler.
 type top struct{ Handler http.Handler }
 
 func (t top) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	path := req.URL.Path