Further performance optimization
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
@@ -8,6 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// assetVersions maps each embedded asset (path relative to assets/, e.g.
|
||||
@@ -34,6 +37,45 @@ func hashAssets() map[string]string {
|
||||
return versions
|
||||
}
|
||||
|
||||
// assetGzip holds a gzip copy of every embedded asset that compresses well,
|
||||
// keyed like assetVersions. Built once at startup so serving compressed costs
|
||||
// no CPU per request; already-compressed files (fonts) are left out.
|
||||
var assetGzip = gzipAssets()
|
||||
|
||||
func gzipAssets() map[string][]byte {
|
||||
out := map[string][]byte{}
|
||||
_ = fs.WalkDir(assets, "assets", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
b, err := assets.ReadFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
zw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression)
|
||||
zw.Write(b)
|
||||
zw.Close()
|
||||
// Not worth a Content-Encoding round trip unless it saves ≥10%.
|
||||
if buf.Len() < len(b)*9/10 {
|
||||
out[strings.TrimPrefix(p, "assets/")] = buf.Bytes()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// acceptsGzip reports whether the client accepts a gzip response body.
|
||||
func acceptsGzip(r *http.Request) bool {
|
||||
for _, enc := range strings.Split(r.Header.Get("Accept-Encoding"), ",") {
|
||||
enc, q, _ := strings.Cut(strings.TrimSpace(enc), ";")
|
||||
if strings.TrimSpace(enc) == "gzip" && strings.ReplaceAll(q, " ", "") != "q=0" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// assetURL is the "asset" template func: {{asset "style.css"}} renders
|
||||
// /_/style.css?v=<hash>. An unknown name fails the template so a typo or a
|
||||
// removed file surfaces as a render error instead of a silent 404.
|
||||
@@ -57,18 +99,36 @@ var assetFuncs = template.FuncMap{"asset": assetURL}
|
||||
// change);
|
||||
// - anything else (unversioned or stale ?v=, e.g. icons from CSS) gets the
|
||||
// content hash as ETag and revalidates, answered with 304 when unchanged.
|
||||
//
|
||||
// Assets with a precompressed copy (see assetGzip) are sent gzipped to
|
||||
// clients that accept it; the gzip variant gets its own ETag.
|
||||
func serveStatic() http.Handler {
|
||||
staticFS, _ := fs.Sub(assets, "assets")
|
||||
static := http.StripPrefix("/_/", http.FileServer(http.FS(staticFS)))
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.TrimPrefix(r.URL.Path, "/_/")
|
||||
v, ok := assetVersions[name]
|
||||
gz, hasGz := assetGzip[name]
|
||||
useGz := hasGz && acceptsGzip(r)
|
||||
if hasGz {
|
||||
w.Header().Set("Vary", "Accept-Encoding")
|
||||
}
|
||||
switch {
|
||||
case ok && r.URL.Query().Get("v") == v, strings.HasPrefix(name, "fonts/"):
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||
case ok:
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("ETag", `"`+v+`"`)
|
||||
if useGz {
|
||||
w.Header().Set("ETag", `"`+v+`-gz"`)
|
||||
} else {
|
||||
w.Header().Set("ETag", `"`+v+`"`)
|
||||
}
|
||||
}
|
||||
if useGz {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
// ServeContent derives Content-Type from the name's extension.
|
||||
http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(gz))
|
||||
return
|
||||
}
|
||||
static.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user