diff --git a/client/web/assets.go b/client/web/assets.go index c4f4e9e3bcf66..f9b9632d25d32 100644 --- a/client/web/assets.go +++ b/client/web/assets.go @@ -16,7 +16,9 @@ import ( "strings" "time" + "github.com/klauspost/compress/zstd" prebuilt "github.com/tailscale/web-client-prebuilt" + "tailscale.com/tsweb" ) var start = time.Now() @@ -63,7 +65,33 @@ func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { }), nil } -func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) { +type zstFile struct { + f fs.File + *zstd.Decoder +} + +func newZSTFile(f fs.File) (*zstFile, error) { + zr, err := zstd.NewReader(f) + if err != nil { + return nil, err + } + return &zstFile{f: f, Decoder: zr}, nil +} + +func (z *zstFile) Close() error { + z.Close() + return z.f.Close() +} + +func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (io.ReadCloser, error) { + if f, err := fs.Open(path + ".zst"); err == nil { + if tsweb.AcceptsEncoding(r, "zstd") { + w.Header().Set("Content-Encoding", "zstd") + return f, nil + } + return newZSTFile(f) + } + // TODO(raggi): remove this code path when no longer used if f, err := fs.Open(path + ".gz"); err == nil { w.Header().Set("Content-Encoding", "gzip") return f, nil diff --git a/cmd/build-webclient/build-webclient.go b/cmd/build-webclient/build-webclient.go index f92c0858fae25..c0dc6ea41316d 100644 --- a/cmd/build-webclient/build-webclient.go +++ b/cmd/build-webclient/build-webclient.go @@ -73,10 +73,14 @@ func build(toolDir, appDir string) error { if err := os.Remove(f); err != nil { log.Printf("Failed to cleanup %q: %v", f, err) } - // Removing intermediate ".br" version, we use ".gz" asset. + // Removing ".br" version, we use the ".zst" asset. if err := os.Remove(f + ".br"); err != nil { log.Printf("Failed to cleanup %q: %v", f+".gz", err) } + // Removing ".gz" version, we use the ".zst" asset. + if err := os.Remove(f + ".gz"); err != nil { + log.Printf("Failed to cleanup %q: %v", f+".gz", err) + } } return nil diff --git a/util/precompress/precompress.go b/util/precompress/precompress.go index 6d1a26efdd767..a0cdf63673485 100644 --- a/util/precompress/precompress.go +++ b/util/precompress/precompress.go @@ -17,6 +17,7 @@ import ( "path/filepath" "github.com/andybalholm/brotli" + "github.com/klauspost/compress/zstd" "golang.org/x/sync/errgroup" "tailscale.com/tsweb" ) @@ -63,6 +64,12 @@ type Options struct { // OpenPrecompressedFile opens a file from fs, preferring compressed versions // generated by PrecompressDir if possible. func OpenPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) { + if tsweb.AcceptsEncoding(r, "zstd") { + if f, err := fs.Open(path + ".zst"); err == nil { + w.Header().Set("Content-Encoding", "zstd") + return f, nil + } + } if tsweb.AcceptsEncoding(r, "br") { if f, err := fs.Open(path + ".br"); err == nil { w.Header().Set("Content-Encoding", "br") @@ -104,6 +111,13 @@ func Precompress(path string, options Options) error { if err != nil { return err } + zstdLevel := zstd.WithEncoderLevel(zstd.SpeedBestCompression) + if options.FastCompression { + zstdLevel = zstd.WithEncoderLevel(zstd.SpeedFastest) + } + err = writeCompressed(contents, func(w io.Writer) (io.WriteCloser, error) { + return zstd.NewWriter(w, zstdLevel) + }, path+".zst", fi.Mode()) brotliLevel := brotli.BestCompression if options.FastCompression { brotliLevel = brotli.BestSpeed