A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
72
fork

Configure Feed

Select the types of activity you want to include in your feed.

research minifing css/js through go generate

+398
+398
docs/MINIFY.md
··· 1 + # CSS/JS Minification for ATCR 2 + 3 + ## Overview 4 + 5 + ATCR 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 + 17 + Use 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 30 + go get github.com/tdewolff/minify/v2 31 + ``` 32 + 33 + This will update `go.mod`: 34 + ```go 35 + require github.com/tdewolff/minify/v2 v2.20.37 36 + ``` 37 + 38 + ### Step 2: Create Minification Script 39 + 40 + Create `pkg/appview/static/minify_assets.go`: 41 + 42 + ```go 43 + //go:build ignore 44 + 45 + package main 46 + 47 + import ( 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 + 58 + func 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 + 88 + func 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 + 120 + Add 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 126 + var staticFS embed.FS 127 + ``` 128 + 129 + ### Step 4: Update HTML Templates 130 + 131 + Update 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 153 + go generate ./pkg/appview 154 + 155 + # Build binary (embeds minified assets) 156 + go build -o bin/atcr-appview ./cmd/appview 157 + 158 + # Or build all 159 + go generate ./... 160 + go build -o bin/atcr-appview ./cmd/appview 161 + go build -o bin/atcr-hold ./cmd/hold 162 + ``` 163 + 164 + ### Step 6: Add to .gitignore 165 + 166 + Add minified files to `.gitignore` since they're generated: 167 + 168 + ``` 169 + # Generated minified assets 170 + pkg/appview/static/css/*.min.css 171 + pkg/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 + 178 + Use 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 197 + var staticFS embed.FS 198 + 199 + func StylePath() string { return "/static/css/style.css" } 200 + func 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 210 + var staticFS embed.FS 211 + 212 + func StylePath() string { return "/static/css/style.min.css" } 213 + func ScriptPath() string { return "/static/js/app.min.js" } 214 + ``` 215 + 216 + **Usage:** 217 + ```bash 218 + # Development build (unminified) 219 + go build ./cmd/appview 220 + 221 + # Production build (minified) 222 + go generate ./pkg/appview 223 + go build -tags production ./cmd/appview 224 + ``` 225 + 226 + ## Alternative Approaches 227 + 228 + ### Option 2: External Minifier (cssnano, esbuild) 229 + 230 + Use 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 + 248 + Compress assets at runtime (complementary to minification): 249 + 250 + ```go 251 + import "github.com/NYTimes/gziphandler" 252 + 253 + // Wrap static handler 254 + mux.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 270 + import "github.com/andybalholm/brotli" 271 + 272 + // Custom handler with brotli 273 + func 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 + 318 + 1. **Edit source files:** 319 + - Modify `pkg/appview/static/css/style.css` 320 + - Modify `pkg/appview/static/js/app.js` 321 + 322 + 2. **Test locally:** 323 + ```bash 324 + # Development build (unminified) 325 + go run ./cmd/appview serve 326 + ``` 327 + 328 + 3. **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 + 337 + 4. **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 + 359 + ATCR already uses `go:generate` for: 360 + - CBOR generation (`pkg/atproto/lexicon.go`) 361 + - License downloads (`pkg/appview/licenses/licenses.go`) 362 + 363 + Minification follows the same pattern: 364 + ```bash 365 + # Generate all (CBOR, licenses, minified assets) 366 + go generate ./... 367 + 368 + # Build all binaries 369 + go build -o bin/atcr-appview ./cmd/appview 370 + go build -o bin/atcr-hold ./cmd/hold 371 + go build -o bin/docker-credential-atcr ./cmd/credential-helper 372 + ``` 373 + 374 + ## Recommendation 375 + 376 + **For ATCR:** 377 + 378 + 1. **Immediate:** Implement Option 1 (`tdewolff/minify`) 379 + - Pure Go, no external dependencies 380 + - Integrates with existing `go:generate` workflow 381 + - ~30% size reduction 382 + 383 + 2. **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 + 388 + 3. **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