A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at refactor 398 lines 9.6 kB view raw view rendered
1# CSS/JS Minification for ATCR 2 3## Overview 4 5ATCR embeds static assets (CSS, JavaScript) directly into the binary using Go's `embed` directive. Currently: 6 7- **CSS Size:** 40KB (`pkg/appview/static/css/style.css`, 2,210 lines) 8- **Embedded:** All static files compiled into binary at build time 9- **No Minification:** Source files embedded as-is 10 11**Problem:** Embedded assets increase binary size and network transfer time. 12 13**Solution:** Minify CSS/JS before embedding to reduce both binary size and network transfer. 14 15## Recommended Approach: `tdewolff/minify` 16 17Use the pure Go `tdewolff/minify` library with `go:generate` to minify assets at build time. 18 19**Benefits:** 20- Pure Go, no external dependencies (Node.js, npm) 21- Integrates with existing `go:generate` workflow 22- ~30-40% CSS size reduction (40KB → ~28KB) 23- Minifies CSS, JS, HTML, JSON, SVG, XML 24 25## Implementation 26 27### Step 1: Add Dependency 28 29```bash 30go get github.com/tdewolff/minify/v2 31``` 32 33This will update `go.mod`: 34```go 35require github.com/tdewolff/minify/v2 v2.20.37 36``` 37 38### Step 2: Create Minification Script 39 40Create `pkg/appview/static/minify_assets.go`: 41 42```go 43//go:build ignore 44 45package main 46 47import ( 48 "fmt" 49 "log" 50 "os" 51 "path/filepath" 52 53 "github.com/tdewolff/minify/v2" 54 "github.com/tdewolff/minify/v2/css" 55 "github.com/tdewolff/minify/v2/js" 56) 57 58func main() { 59 m := minify.New() 60 m.AddFunc("text/css", css.Minify) 61 m.AddFunc("text/javascript", js.Minify) 62 63 // Get the directory of this script 64 dir, err := os.Getwd() 65 if err != nil { 66 log.Fatal(err) 67 } 68 69 // Minify CSS 70 if err := minifyFile(m, "text/css", 71 filepath.Join(dir, "pkg/appview/static/css/style.css"), 72 filepath.Join(dir, "pkg/appview/static/css/style.min.css"), 73 ); err != nil { 74 log.Fatalf("Failed to minify CSS: %v", err) 75 } 76 77 // Minify JavaScript 78 if err := minifyFile(m, "text/javascript", 79 filepath.Join(dir, "pkg/appview/static/js/app.js"), 80 filepath.Join(dir, "pkg/appview/static/js/app.min.js"), 81 ); err != nil { 82 log.Fatalf("Failed to minify JS: %v", err) 83 } 84 85 fmt.Println("✓ Assets minified successfully") 86} 87 88func minifyFile(m *minify.M, mediatype, src, dst string) error { 89 // Read source file 90 input, err := os.ReadFile(src) 91 if err != nil { 92 return fmt.Errorf("read %s: %w", src, err) 93 } 94 95 // Minify 96 output, err := m.Bytes(mediatype, input) 97 if err != nil { 98 return fmt.Errorf("minify %s: %w", src, err) 99 } 100 101 // Write minified output 102 if err := os.WriteFile(dst, output, 0644); err != nil { 103 return fmt.Errorf("write %s: %w", dst, err) 104 } 105 106 // Print size reduction 107 originalSize := len(input) 108 minifiedSize := len(output) 109 reduction := float64(originalSize-minifiedSize) / float64(originalSize) * 100 110 111 fmt.Printf(" %s: %d bytes → %d bytes (%.1f%% reduction)\n", 112 filepath.Base(src), originalSize, minifiedSize, reduction) 113 114 return nil 115} 116``` 117 118### Step 3: Add `go:generate` Directive 119 120Add to `pkg/appview/ui.go` (before the `//go:embed` directive): 121 122```go 123//go:generate go run ./static/minify_assets.go 124 125//go:embed static 126var staticFS embed.FS 127``` 128 129### Step 4: Update HTML Templates 130 131Update all template files to reference minified assets: 132 133**Before:** 134```html 135<link rel="stylesheet" href="/static/css/style.css"> 136<script src="/static/js/app.js"></script> 137``` 138 139**After:** 140```html 141<link rel="stylesheet" href="/static/css/style.min.css"> 142<script src="/static/js/app.min.js"></script> 143``` 144 145**Files to update:** 146- `pkg/appview/templates/components/head.html` 147- Any other templates that reference CSS/JS directly 148 149### Step 5: Build Workflow 150 151```bash 152# Generate minified assets 153go generate ./pkg/appview 154 155# Build binary (embeds minified assets) 156go build -o bin/atcr-appview ./cmd/appview 157 158# Or build all 159go generate ./... 160go build -o bin/atcr-appview ./cmd/appview 161go build -o bin/atcr-hold ./cmd/hold 162``` 163 164### Step 6: Add to .gitignore 165 166Add minified files to `.gitignore` since they're generated: 167 168``` 169# Generated minified assets 170pkg/appview/static/css/*.min.css 171pkg/appview/static/js/*.min.js 172``` 173 174**Alternative:** Commit minified files if you want reproducible builds without running `go generate`. 175 176## Build Modes (Optional Enhancement) 177 178Use build tags to serve unminified assets in development: 179 180**Development (default):** 181- Edit `style.css` directly 182- No minification, easier debugging 183- Faster build times 184 185**Production (with `-tags production`):** 186- Use minified assets 187- Smaller binary size 188- Optimized for deployment 189 190### Implementation with Build Tags 191 192**pkg/appview/ui.go** (development): 193```go 194//go:build !production 195 196//go:embed static 197var staticFS embed.FS 198 199func StylePath() string { return "/static/css/style.css" } 200func ScriptPath() string { return "/static/js/app.js" } 201``` 202 203**pkg/appview/ui_production.go** (production): 204```go 205//go:build production 206 207//go:generate go run ./static/minify_assets.go 208 209//go:embed static 210var staticFS embed.FS 211 212func StylePath() string { return "/static/css/style.min.css" } 213func ScriptPath() string { return "/static/js/app.min.js" } 214``` 215 216**Usage:** 217```bash 218# Development build (unminified) 219go build ./cmd/appview 220 221# Production build (minified) 222go generate ./pkg/appview 223go build -tags production ./cmd/appview 224``` 225 226## Alternative Approaches 227 228### Option 2: External Minifier (cssnano, esbuild) 229 230Use Node.js-based minifiers via `go:generate`: 231 232```go 233//go:generate sh -c "npx cssnano static/css/style.css static/css/style.min.css" 234//go:generate sh -c "npx esbuild static/js/app.js --minify --outfile=static/js/app.min.js" 235``` 236 237**Pros:** 238- Best-in-class minification (potentially better than tdewolff) 239- Wide ecosystem of tools 240 241**Cons:** 242- Requires Node.js/npm in build environment 243- Cross-platform compatibility issues (Windows vs Unix) 244- External dependency management 245 246### Option 3: Runtime Gzip Compression 247 248Compress assets at runtime (complementary to minification): 249 250```go 251import "github.com/NYTimes/gziphandler" 252 253// Wrap static handler 254mux.Handle("/static/", gziphandler.GzipHandler(appview.StaticHandler())) 255``` 256 257**Pros:** 258- Works for all static files (images, fonts) 259- ~70-80% size reduction over network 260- No build changes needed 261 262**Cons:** 263- Doesn't reduce binary size 264- Adds runtime CPU cost 265- Should be combined with minification for best results 266 267### Option 4: Brotli Compression (Better than Gzip) 268 269```go 270import "github.com/andybalholm/brotli" 271 272// Custom handler with brotli 273func BrotliHandler(h http.Handler) http.Handler { 274 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 275 if !strings.Contains(r.Header.Get("Accept-Encoding"), "br") { 276 h.ServeHTTP(w, r) 277 return 278 } 279 w.Header().Set("Content-Encoding", "br") 280 bw := brotli.NewWriterLevel(w, brotli.DefaultCompression) 281 defer bw.Close() 282 h.ServeHTTP(&brotliResponseWriter{Writer: bw, ResponseWriter: w}, r) 283 }) 284} 285``` 286 287## Expected Benefits 288 289### File Size Reduction 290 291**Current (unminified):** 292- CSS: 40KB 293- JS: ~5KB (estimated) 294- **Total embedded:** ~45KB 295 296**With Minification:** 297- CSS: ~28KB (30% reduction) 298- JS: ~3KB (40% reduction) 299- **Total embedded:** ~31KB 300- **Binary size savings:** ~14KB 301 302**With Minification + Gzip (network transfer):** 303- CSS: ~8KB (80% reduction from original) 304- JS: ~1.5KB (70% reduction from original) 305- **Total transferred:** ~9.5KB 306 307### Performance Impact 308 309- **Build time:** +1-2 seconds (running minifier) 310- **Runtime:** No impact (files pre-minified) 311- **Network:** 75% less data transferred (with gzip) 312- **Browser parsing:** Slightly faster (smaller files) 313 314## Maintenance 315 316### Development Workflow 317 3181. **Edit source files:** 319 - Modify `pkg/appview/static/css/style.css` 320 - Modify `pkg/appview/static/js/app.js` 321 3222. **Test locally:** 323 ```bash 324 # Development build (unminified) 325 go run ./cmd/appview serve 326 ``` 327 3283. **Build for production:** 329 ```bash 330 # Generate minified assets 331 go generate ./pkg/appview 332 333 # Build binary 334 go build -o bin/atcr-appview ./cmd/appview 335 ``` 336 3374. **CI/CD:** 338 ```bash 339 # In GitHub Actions / CI 340 go generate ./... 341 go build ./... 342 ``` 343 344### Troubleshooting 345 346**Q: Minified assets not updating?** 347- Delete `*.min.css` and `*.min.js` files 348- Run `go generate ./pkg/appview` again 349 350**Q: Build fails with "package not found"?** 351- Run `go mod tidy` to download dependencies 352 353**Q: CSS broken after minification?** 354- Check for syntax errors in source CSS 355- Minifier is strict about valid CSS 356 357## Integration with Existing Build 358 359ATCR already uses `go:generate` for: 360- CBOR generation (`pkg/atproto/lexicon.go`) 361- License downloads (`pkg/appview/licenses/licenses.go`) 362 363Minification follows the same pattern: 364```bash 365# Generate all (CBOR, licenses, minified assets) 366go generate ./... 367 368# Build all binaries 369go build -o bin/atcr-appview ./cmd/appview 370go build -o bin/atcr-hold ./cmd/hold 371go build -o bin/docker-credential-atcr ./cmd/credential-helper 372``` 373 374## Recommendation 375 376**For ATCR:** 377 3781. **Immediate:** Implement Option 1 (`tdewolff/minify`) 379 - Pure Go, no external dependencies 380 - Integrates with existing `go:generate` workflow 381 - ~30% size reduction 382 3832. **Future:** Add runtime gzip/brotli compression 384 - Wrap static handler with compression middleware 385 - Benefits all static assets 386 - Standard practice for web servers 387 3883. **Long-term:** Consider build modes (development vs production) 389 - Use unminified assets in development 390 - Use minified assets in production builds 391 - Best developer experience 392 393## References 394 395- [tdewolff/minify](https://github.com/tdewolff/minify) - Go minifier library 396- [NYTimes/gziphandler](https://github.com/NYTimes/gziphandler) - Gzip middleware 397- [Go embed directive](https://pkg.go.dev/embed) - Embedding static files 398- [Go generate](https://go.dev/blog/generate) - Code generation tool