perf: 优化前端加载速度

- 改用 mime 标准库替代硬编码 MIME 类型(更优雅)
- 全局 gzip 中间件替代手动压缩(覆盖所有响应)
- ECharts 改为动态 import,首屏不再加载 1.1MB
- manualChunks 拆分 vue-vendor/ui-vendor 减小主包
- 首屏 gzip 体积从 544KB 降至 236KB(减少 57%)
This commit is contained in:
2026-05-05 13:18:40 +08:00
parent bc30afe106
commit d584b578f1
4 changed files with 387 additions and 151 deletions
+86 -59
View File
@@ -4,8 +4,10 @@ import (
"compress/gzip"
"embed"
"fmt"
"io"
"io/fs"
"log"
"mime"
"net/url"
"os"
"strings"
@@ -225,9 +227,19 @@ func startServer() {
r := gin.Default()
r.MaxMultipartMemory = 32 << 20
mime.AddExtensionType(".js", "application/javascript; charset=utf-8")
mime.AddExtensionType(".mjs", "application/javascript; charset=utf-8")
mime.AddExtensionType(".css", "text/css; charset=utf-8")
mime.AddExtensionType(".html", "text/html; charset=utf-8")
mime.AddExtensionType(".json", "application/json; charset=utf-8")
mime.AddExtensionType(".svg", "image/svg+xml")
mime.AddExtensionType(".woff", "font/woff")
mime.AddExtensionType(".woff2", "font/woff2")
r.Use(middleware.Logger())
r.Use(middleware.Recovery())
r.Use(middleware.Cors())
r.Use(gzipMiddleware())
r.Static("/uploads", "uploads")
router.SetupRoutes(r)
@@ -273,20 +285,13 @@ func setupEmbeddedFrontend(r *gin.Engine) {
return
}
contentType := getContentType(decodedPath)
c.Header("Cache-Control", "public, max-age=31536000, immutable")
if strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") && isCompressible(decodedPath) {
c.Header("Content-Encoding", "gzip")
c.Header("Vary", "Accept-Encoding")
c.Writer.WriteHeader(200)
c.Writer.Header().Set("Content-Type", contentType)
gw := gzip.NewWriter(c.Writer)
gw.Write(data)
gw.Close()
} else {
c.Data(200, contentType, data)
contentType := mime.TypeByExtension(decodedPath)
if contentType == "" {
contentType = "application/octet-stream"
}
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Data(200, contentType, data)
})
r.NoRoute(func(c *gin.Context) {
@@ -301,7 +306,10 @@ func setupEmbeddedFrontend(r *gin.Engine) {
decodedPath = path
}
if data, err := fs.ReadFile(distFS, decodedPath); err == nil {
contentType := getContentType(decodedPath)
contentType := mime.TypeByExtension(decodedPath)
if contentType == "" {
contentType = "application/octet-stream"
}
c.Data(200, contentType, data)
return
}
@@ -345,52 +353,71 @@ func getSettingString(settings map[string]interface{}, key, defaultValue string)
return defaultValue
}
func isCompressible(filename string) bool {
ext := strings.ToLower(filename[strings.LastIndex(filename, "."):])
switch ext {
case ".js", ".mjs", ".css", ".html", ".json", ".svg", ".xml", ".txt":
return true
default:
return false
func isCompressibleContentType(contentType string) bool {
return strings.HasPrefix(contentType, "text/") ||
strings.Contains(contentType, "javascript") ||
strings.Contains(contentType, "json") ||
strings.Contains(contentType, "xml") ||
strings.Contains(contentType, "svg")
}
func gzipMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
c.Next()
return
}
c.Header("Vary", "Accept-Encoding")
c.Writer = &gzipWriter{Writer: gzip.NewWriter(c.Writer), ResponseWriter: c.Writer}
c.Next()
if gw, ok := c.Writer.(*gzipWriter); ok {
contentType := c.Writer.Header().Get("Content-Type")
if !isCompressibleContentType(contentType) {
gw.ResponseWriter = &noopWriter{}
gw.Writer.Close()
c.Writer = gw.ResponseWriter
return
}
gw.Writer.Close()
}
}
}
func getContentType(filename string) string {
ext := strings.ToLower(filename[strings.LastIndex(filename, "."):])
switch ext {
case ".js", ".mjs":
return "application/javascript; charset=utf-8"
case ".css":
return "text/css; charset=utf-8"
case ".html":
return "text/html; charset=utf-8"
case ".json":
return "application/json; charset=utf-8"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".svg":
return "image/svg+xml"
case ".ico":
return "image/x-icon"
case ".webp":
return "image/webp"
case ".woff":
return "font/woff"
case ".woff2":
return "font/woff2"
case ".ttf":
return "font/ttf"
case ".eot":
return "application/vnd.ms-fontobject"
case ".xml":
return "application/xml"
case ".txt":
return "text/plain; charset=utf-8"
default:
return "application/octet-stream"
}
type gzipWriter struct {
gin.ResponseWriter
Writer *gzip.Writer
}
func (w *gzipWriter) Write(data []byte) (int, error) {
contentType := w.Header().Get("Content-Type")
if !isCompressibleContentType(contentType) {
return w.ResponseWriter.Write(data)
}
if w.Header().Get("Content-Encoding") == "" {
w.Header().Set("Content-Encoding", "gzip")
}
return w.Writer.Write(data)
}
func (w *gzipWriter) WriteString(s string) (int, error) {
contentType := w.Header().Get("Content-Type")
if !isCompressibleContentType(contentType) {
return w.ResponseWriter.WriteString(s)
}
if w.Header().Get("Content-Encoding") == "" {
w.Header().Set("Content-Encoding", "gzip")
}
return io.WriteString(w.Writer, s)
}
type noopWriter struct {
gin.ResponseWriter
}
func (w *noopWriter) Write(data []byte) (int, error) {
return len(data), nil
}