From 68984da332f1068a22596d101b8329dc9a99a751 Mon Sep 17 00:00:00 2001 From: jvoisin Date: Sat, 9 Aug 2025 21:37:18 +0200 Subject: [PATCH] perf(static): minimize the SVG Since tdewolff/minify supports SVG minimization, let's make use of it. As we need to keep the license in the SVG because we're nice netizens, we can at least use SPDX identifiers instead of using it verbatim. This does save a couple of kB. --- internal/cli/cli.go | 8 ++--- internal/ui/static/bin/sprite.svg | 26 ++-------------- internal/ui/static/static.go | 50 +++++++++++++++++-------------- internal/ui/static_app_icon.go | 15 +++------- internal/ui/static_favicon.go | 14 +++------ 5 files changed, 41 insertions(+), 72 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 64d660d6..da754bc5 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -156,16 +156,16 @@ func Parse() { slog.Info("The default value for DATABASE_URL is used") } - if err := static.CalculateBinaryFileChecksums(); err != nil { - printErrorAndExit(fmt.Errorf("unable to calculate binary file checksums: %v", err)) + if err := static.GenerateBinaryBundles(); err != nil { + printErrorAndExit(fmt.Errorf("unable to generate binary files bundle: %v", err)) } if err := static.GenerateStylesheetsBundles(); err != nil { - printErrorAndExit(fmt.Errorf("unable to generate stylesheets bundles: %v", err)) + printErrorAndExit(fmt.Errorf("unable to generate stylesheets bundle: %v", err)) } if err := static.GenerateJavascriptBundles(); err != nil { - printErrorAndExit(fmt.Errorf("unable to generate javascript bundles: %v", err)) + printErrorAndExit(fmt.Errorf("unable to generate javascript bundle: %v", err)) } db, err := database.NewConnectionPool( diff --git a/internal/ui/static/bin/sprite.svg b/internal/ui/static/bin/sprite.svg index 7ff7b1c7..e96364db 100644 --- a/internal/ui/static/bin/sprite.svg +++ b/internal/ui/static/bin/sprite.svg @@ -1,29 +1,7 @@ diff --git a/internal/ui/static/static.go b/internal/ui/static/static.go index 72c2df36..74a5af2e 100644 --- a/internal/ui/static/static.go +++ b/internal/ui/static/static.go @@ -6,13 +6,15 @@ package static // import "miniflux.app/v2/internal/ui/static" import ( "bytes" "embed" - "fmt" + "log/slog" + "strings" "miniflux.app/v2/internal/crypto" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/css" "github.com/tdewolff/minify/v2/js" + "github.com/tdewolff/minify/v2/svg" ) type asset struct { @@ -22,9 +24,9 @@ type asset struct { // Static assets. var ( - StylesheetBundles map[string]asset - JavascriptBundles map[string]asset - binaryFileChecksums map[string]string + StylesheetBundles map[string]asset + JavascriptBundles map[string]asset + BinaryBundles map[string]asset ) //go:embed bin/* @@ -36,40 +38,42 @@ var stylesheetFiles embed.FS //go:embed js/*.js var javascriptFiles embed.FS -// CalculateBinaryFileChecksums generates hash of embed binary files. -func CalculateBinaryFileChecksums() error { +func GenerateBinaryBundles() error { dirEntries, err := binaryFiles.ReadDir("bin") if err != nil { return err } - binaryFileChecksums = make(map[string]string, len(dirEntries)) + BinaryBundles = make(map[string]asset, len(dirEntries)) + + minifier := minify.New() + minifier.Add("image/svg+xml", &svg.Minifier{ + KeepComments: true, // needed to keep the license + }) for _, dirEntry := range dirEntries { - data, err := LoadBinaryFile(dirEntry.Name()) + name := dirEntry.Name() + data, err := binaryFiles.ReadFile("bin/" + name) if err != nil { return err } - binaryFileChecksums[dirEntry.Name()] = crypto.HashFromBytes(data) + if strings.HasSuffix(name, ".svg") { + // minifier.Bytes returns the data unchanged in case of error. + data, err = minifier.Bytes("image/svg+xml", data) + if err != nil { + slog.Error("Unable to minimize the svg file", slog.String("filename", name), slog.Any("error", err)) + } + } + + BinaryBundles[name] = asset{ + Data: data, + Checksum: crypto.HashFromBytes(data), + } } return nil } -// LoadBinaryFile loads an embed binary file. -func LoadBinaryFile(filename string) ([]byte, error) { - return binaryFiles.ReadFile("bin/" + filename) -} - -// GetBinaryFileChecksum returns a binary file checksum. -func GetBinaryFileChecksum(filename string) (string, error) { - data, found := binaryFileChecksums[filename] - if !found { - return "", fmt.Errorf(`static: unable to find checksum for %q`, filename) - } - return data, nil -} - // GenerateStylesheetsBundles creates CSS bundles. func GenerateStylesheetsBundles() error { var bundles = map[string][]string{ diff --git a/internal/ui/static_app_icon.go b/internal/ui/static_app_icon.go index 8a99fb62..92dff2fa 100644 --- a/internal/ui/static_app_icon.go +++ b/internal/ui/static_app_icon.go @@ -16,19 +16,13 @@ import ( func (h *handler) showAppIcon(w http.ResponseWriter, r *http.Request) { filename := request.RouteStringParam(r, "filename") - etag, err := static.GetBinaryFileChecksum(filename) - if err != nil { + value, ok := static.BinaryBundles[filename] + if !ok { html.NotFound(w, r) return } - response.New(w, r).WithCaching(etag, 72*time.Hour, func(b *response.Builder) { - blob, err := static.LoadBinaryFile(filename) - if err != nil { - html.ServerError(w, r, err) - return - } - + response.New(w, r).WithCaching(value.Checksum, 72*time.Hour, func(b *response.Builder) { switch filepath.Ext(filename) { case ".png": b.WithoutCompression() @@ -36,8 +30,7 @@ func (h *handler) showAppIcon(w http.ResponseWriter, r *http.Request) { case ".svg": b.WithHeader("Content-Type", "image/svg+xml") } - - b.WithBody(blob) + b.WithBody(value.Data) b.Write() }) } diff --git a/internal/ui/static_favicon.go b/internal/ui/static_favicon.go index 2775ca81..e2618a88 100644 --- a/internal/ui/static_favicon.go +++ b/internal/ui/static_favicon.go @@ -13,22 +13,16 @@ import ( ) func (h *handler) showFavicon(w http.ResponseWriter, r *http.Request) { - etag, err := static.GetBinaryFileChecksum("favicon.ico") - if err != nil { + value, ok := static.BinaryBundles["favicon.ico"] + if !ok { html.NotFound(w, r) return } - response.New(w, r).WithCaching(etag, 48*time.Hour, func(b *response.Builder) { - blob, err := static.LoadBinaryFile("favicon.ico") - if err != nil { - html.ServerError(w, r, err) - return - } - + response.New(w, r).WithCaching(value.Checksum, 48*time.Hour, func(b *response.Builder) { b.WithHeader("Content-Type", "image/x-icon") b.WithoutCompression() - b.WithBody(blob) + b.WithBody(value.Data) b.Write() }) }