From e5e5ebda44e7d28df279e89d3cc3a8b904843304 Mon Sep 17 00:00:00 2001 From: Sonia Appasamy Date: Thu, 7 Dec 2023 14:02:08 -0500 Subject: [PATCH] client/web: precompress assets Precompress webclient assets with precompress util. This cuts our css and js build sizes to about 1/3 of non-compressed size. Similar compression done on tsconnect and adminhttp assets. Updates #10261 Signed-off-by: Sonia Appasamy --- client/web/assets.go | 43 ++++++++++-- client/web/build/index.html | 7 +- cmd/build-webclient/build-webclient.go | 95 ++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 cmd/build-webclient/build-webclient.go diff --git a/client/web/assets.go b/client/web/assets.go index ccef6a0e1..0f92d93d9 100644 --- a/client/web/assets.go +++ b/client/web/assets.go @@ -4,6 +4,7 @@ package web import ( + "io" "io/fs" "log" "net/http" @@ -13,10 +14,13 @@ import ( "os/exec" "path/filepath" "strings" + "time" prebuilt "github.com/tailscale/web-client-prebuilt" ) +var start = time.Now() + func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { if devMode { // When in dev mode, proxy asset requests to the Vite dev server. @@ -25,19 +29,46 @@ func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { } fsys := prebuilt.FS() - fileserver := http.FileServer(http.FS(fsys)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := fs.Stat(fsys, strings.TrimPrefix(r.URL.Path, "/")) - if os.IsNotExist(err) { - // rewrite request to just fetch /index.html and let + path := strings.TrimPrefix(r.URL.Path, "/") + f, err := openPrecompressedFile(w, r, path, fsys) + if err != nil { + // Rewrite request to just fetch index.html and let // the frontend router handle it. r = r.Clone(r.Context()) - r.URL.Path = "/" + path = "index.html" + f, err = openPrecompressedFile(w, r, path, fsys) + } + if f == nil { + http.Error(w, err.Error(), http.StatusNotFound) + return } - fileserver.ServeHTTP(w, r) + defer f.Close() + + // fs.File does not claim to implement Seeker, but in practice it does. + fSeeker, ok := f.(io.ReadSeeker) + if !ok { + http.Error(w, "Not seekable", http.StatusInternalServerError) + return + } + + // Aggressively cache static assets, since we cache-bust our assets with + // hashed filenames. + w.Header().Set("Cache-Control", "public, max-age=31535996") + w.Header().Set("Vary", "Accept-Encoding") + + http.ServeContent(w, r, path, start, fSeeker) }), nil } +func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) { + if f, err := fs.Open(path + ".gz"); err == nil { + w.Header().Set("Content-Encoding", "gzip") + return f, nil + } + return fs.Open(path) // fallback +} + // startDevServer starts the JS dev server that does on-demand rebuilding // and serving of web client JS and CSS resources. func startDevServer() (cleanup func()) { diff --git a/client/web/build/index.html b/client/web/build/index.html index c0d39ba94..0af7ea24c 100644 --- a/client/web/build/index.html +++ b/client/web/build/index.html @@ -6,10 +6,11 @@ - - + + + - +