A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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