# CSS/JS Minification for ATCR ## Overview ATCR embeds static assets (CSS, JavaScript) directly into the binary using Go's `embed` directive. Currently: - **CSS Size:** 40KB (`pkg/appview/static/css/style.css`, 2,210 lines) - **Embedded:** All static files compiled into binary at build time - **No Minification:** Source files embedded as-is **Problem:** Embedded assets increase binary size and network transfer time. **Solution:** Minify CSS/JS before embedding to reduce both binary size and network transfer. ## Recommended Approach: `tdewolff/minify` Use the pure Go `tdewolff/minify` library with `go:generate` to minify assets at build time. **Benefits:** - Pure Go, no external dependencies (Node.js, npm) - Integrates with existing `go:generate` workflow - ~30-40% CSS size reduction (40KB → ~28KB) - Minifies CSS, JS, HTML, JSON, SVG, XML ## Implementation ### Step 1: Add Dependency ```bash go get github.com/tdewolff/minify/v2 ``` This will update `go.mod`: ```go require github.com/tdewolff/minify/v2 v2.20.37 ``` ### Step 2: Create Minification Script Create `pkg/appview/static/minify_assets.go`: ```go //go:build ignore package main import ( "fmt" "log" "os" "path/filepath" "github.com/tdewolff/minify/v2" "github.com/tdewolff/minify/v2/css" "github.com/tdewolff/minify/v2/js" ) func main() { m := minify.New() m.AddFunc("text/css", css.Minify) m.AddFunc("text/javascript", js.Minify) // Get the directory of this script dir, err := os.Getwd() if err != nil { log.Fatal(err) } // Minify CSS if err := minifyFile(m, "text/css", filepath.Join(dir, "pkg/appview/static/css/style.css"), filepath.Join(dir, "pkg/appview/static/css/style.min.css"), ); err != nil { log.Fatalf("Failed to minify CSS: %v", err) } // Minify JavaScript if err := minifyFile(m, "text/javascript", filepath.Join(dir, "pkg/appview/static/js/app.js"), filepath.Join(dir, "pkg/appview/static/js/app.min.js"), ); err != nil { log.Fatalf("Failed to minify JS: %v", err) } fmt.Println("✓ Assets minified successfully") } func minifyFile(m *minify.M, mediatype, src, dst string) error { // Read source file input, err := os.ReadFile(src) if err != nil { return fmt.Errorf("read %s: %w", src, err) } // Minify output, err := m.Bytes(mediatype, input) if err != nil { return fmt.Errorf("minify %s: %w", src, err) } // Write minified output if err := os.WriteFile(dst, output, 0644); err != nil { return fmt.Errorf("write %s: %w", dst, err) } // Print size reduction originalSize := len(input) minifiedSize := len(output) reduction := float64(originalSize-minifiedSize) / float64(originalSize) * 100 fmt.Printf(" %s: %d bytes → %d bytes (%.1f%% reduction)\n", filepath.Base(src), originalSize, minifiedSize, reduction) return nil } ``` ### Step 3: Add `go:generate` Directive Add to `pkg/appview/ui.go` (before the `//go:embed` directive): ```go //go:generate go run ./static/minify_assets.go //go:embed static var staticFS embed.FS ``` ### Step 4: Update HTML Templates Update all template files to reference minified assets: **Before:** ```html ``` **After:** ```html ``` **Files to update:** - `pkg/appview/templates/components/head.html` - Any other templates that reference CSS/JS directly ### Step 5: Build Workflow ```bash # Generate minified assets go generate ./pkg/appview # Build binary (embeds minified assets) go build -o bin/atcr-appview ./cmd/appview # Or build all go generate ./... go build -o bin/atcr-appview ./cmd/appview go build -o bin/atcr-hold ./cmd/hold ``` ### Step 6: Add to .gitignore Add minified files to `.gitignore` since they're generated: ``` # Generated minified assets pkg/appview/static/css/*.min.css pkg/appview/static/js/*.min.js ``` **Alternative:** Commit minified files if you want reproducible builds without running `go generate`. ## Build Modes (Optional Enhancement) Use build tags to serve unminified assets in development: **Development (default):** - Edit `style.css` directly - No minification, easier debugging - Faster build times **Production (with `-tags production`):** - Use minified assets - Smaller binary size - Optimized for deployment ### Implementation with Build Tags **pkg/appview/ui.go** (development): ```go //go:build !production //go:embed static var staticFS embed.FS func StylePath() string { return "/static/css/style.css" } func ScriptPath() string { return "/static/js/app.js" } ``` **pkg/appview/ui_production.go** (production): ```go //go:build production //go:generate go run ./static/minify_assets.go //go:embed static var staticFS embed.FS func StylePath() string { return "/static/css/style.min.css" } func ScriptPath() string { return "/static/js/app.min.js" } ``` **Usage:** ```bash # Development build (unminified) go build ./cmd/appview # Production build (minified) go generate ./pkg/appview go build -tags production ./cmd/appview ``` ## Alternative Approaches ### Option 2: External Minifier (cssnano, esbuild) Use Node.js-based minifiers via `go:generate`: ```go //go:generate sh -c "npx cssnano static/css/style.css static/css/style.min.css" //go:generate sh -c "npx esbuild static/js/app.js --minify --outfile=static/js/app.min.js" ``` **Pros:** - Best-in-class minification (potentially better than tdewolff) - Wide ecosystem of tools **Cons:** - Requires Node.js/npm in build environment - Cross-platform compatibility issues (Windows vs Unix) - External dependency management ### Option 3: Runtime Gzip Compression Compress assets at runtime (complementary to minification): ```go import "github.com/NYTimes/gziphandler" // Wrap static handler mux.Handle("/static/", gziphandler.GzipHandler(appview.StaticHandler())) ``` **Pros:** - Works for all static files (images, fonts) - ~70-80% size reduction over network - No build changes needed **Cons:** - Doesn't reduce binary size - Adds runtime CPU cost - Should be combined with minification for best results ### Option 4: Brotli Compression (Better than Gzip) ```go import "github.com/andybalholm/brotli" // Custom handler with brotli func BrotliHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "br") { h.ServeHTTP(w, r) return } w.Header().Set("Content-Encoding", "br") bw := brotli.NewWriterLevel(w, brotli.DefaultCompression) defer bw.Close() h.ServeHTTP(&brotliResponseWriter{Writer: bw, ResponseWriter: w}, r) }) } ``` ## Expected Benefits ### File Size Reduction **Current (unminified):** - CSS: 40KB - JS: ~5KB (estimated) - **Total embedded:** ~45KB **With Minification:** - CSS: ~28KB (30% reduction) - JS: ~3KB (40% reduction) - **Total embedded:** ~31KB - **Binary size savings:** ~14KB **With Minification + Gzip (network transfer):** - CSS: ~8KB (80% reduction from original) - JS: ~1.5KB (70% reduction from original) - **Total transferred:** ~9.5KB ### Performance Impact - **Build time:** +1-2 seconds (running minifier) - **Runtime:** No impact (files pre-minified) - **Network:** 75% less data transferred (with gzip) - **Browser parsing:** Slightly faster (smaller files) ## Maintenance ### Development Workflow 1. **Edit source files:** - Modify `pkg/appview/static/css/style.css` - Modify `pkg/appview/static/js/app.js` 2. **Test locally:** ```bash # Development build (unminified) go run ./cmd/appview serve ``` 3. **Build for production:** ```bash # Generate minified assets go generate ./pkg/appview # Build binary go build -o bin/atcr-appview ./cmd/appview ``` 4. **CI/CD:** ```bash # In GitHub Actions / CI go generate ./... go build ./... ``` ### Troubleshooting **Q: Minified assets not updating?** - Delete `*.min.css` and `*.min.js` files - Run `go generate ./pkg/appview` again **Q: Build fails with "package not found"?** - Run `go mod tidy` to download dependencies **Q: CSS broken after minification?** - Check for syntax errors in source CSS - Minifier is strict about valid CSS ## Integration with Existing Build ATCR already uses `go:generate` for: - CBOR generation (`pkg/atproto/lexicon.go`) - License downloads (`pkg/appview/licenses/licenses.go`) Minification follows the same pattern: ```bash # Generate all (CBOR, licenses, minified assets) go generate ./... # Build all binaries go build -o bin/atcr-appview ./cmd/appview go build -o bin/atcr-hold ./cmd/hold go build -o bin/docker-credential-atcr ./cmd/credential-helper ``` ## Recommendation **For ATCR:** 1. **Immediate:** Implement Option 1 (`tdewolff/minify`) - Pure Go, no external dependencies - Integrates with existing `go:generate` workflow - ~30% size reduction 2. **Future:** Add runtime gzip/brotli compression - Wrap static handler with compression middleware - Benefits all static assets - Standard practice for web servers 3. **Long-term:** Consider build modes (development vs production) - Use unminified assets in development - Use minified assets in production builds - Best developer experience ## References - [tdewolff/minify](https://github.com/tdewolff/minify) - Go minifier library - [NYTimes/gziphandler](https://github.com/NYTimes/gziphandler) - Gzip middleware - [Go embed directive](https://pkg.go.dev/embed) - Embedding static files - [Go generate](https://go.dev/blog/generate) - Code generation tool