Brotli Compression. (#12)

Co-authored-by: João Gonçalves <joao.goncalves01@gmail.com>
This commit is contained in:
Jeff Winkler
2025-11-13 10:03:45 -05:00
committed by GitHub
parent f6c9990f38
commit 7670926733
5 changed files with 191 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ Via takes a radical stance:
- No front-end fatigue. - No front-end fatigue.
- Single SSE stream. - Single SSE stream.
- Full reactivity. - Full reactivity.
- Built-in Brotli compression.
- Pure Go. - Pure Go.

7
go.mod
View File

@@ -5,6 +5,8 @@ go 1.25.4
require maragu.dev/gomponents v1.2.0 require maragu.dev/gomponents v1.2.0
require ( require (
github.com/CAFxX/httpcompression v0.0.9
github.com/andybalholm/brotli v1.2.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8 github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8
github.com/starfederation/datastar-go v1.0.3 github.com/starfederation/datastar-go v1.0.3
@@ -12,12 +14,11 @@ require (
) )
require ( require (
github.com/CAFxX/httpcompression v0.0.9 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/sys v0.13.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

10
go.sum
View File

@@ -16,6 +16,11 @@ github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQs
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -37,10 +42,9 @@ github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

15
via.go
View File

@@ -18,6 +18,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/CAFxX/httpcompression"
"github.com/go-via/via/h" "github.com/go-via/via/h"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
@@ -30,6 +31,7 @@ var datastarJS []byte
type V struct { type V struct {
cfg Options cfg Options
mux *http.ServeMux mux *http.ServeMux
handler http.Handler
contextRegistry map[string]*Context contextRegistry map[string]*Context
contextRegistryMutex sync.Mutex contextRegistryMutex sync.Mutex
documentHeadIncludes []h.H documentHeadIncludes []h.H
@@ -203,7 +205,7 @@ func (v *V) Start() {
v.devModeRestore() v.devModeRestore()
} }
v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress) v.logInfo(nil, "via started at [%s]", v.cfg.ServerAddress)
log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.mux)) log.Fatalf("[fatal] %v", http.ListenAndServe(v.cfg.ServerAddress, v.handler))
} }
func (v *V) devModePersist(c *Context) { func (v *V) devModePersist(c *Context) {
@@ -274,8 +276,19 @@ func (v *V) devModeRestore() {
// New creates a new *V application with default configuration. // New creates a new *V application with default configuration.
func New() *V { func New() *V {
mux := http.NewServeMux() mux := http.NewServeMux()
compressionAdapter, err := httpcompression.DefaultAdapter(
httpcompression.MinSize(1024),
httpcompression.BrotliCompressionLevel(5),
httpcompression.ZstandardCompressor(nil),
)
if err != nil {
log.Fatalf("failed to create compression adapter: %v", err)
}
v := &V{ v := &V{
mux: mux, mux: mux,
handler: compressionAdapter(mux),
contextRegistry: make(map[string]*Context), contextRegistry: make(map[string]*Context),
devModePageInitFnMap: make(map[string]func(*Context)), devModePageInitFnMap: make(map[string]func(*Context)),
cfg: Options{ cfg: Options{

165
via_test.go Normal file
View File

@@ -0,0 +1,165 @@
package via
import (
"compress/gzip"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/andybalholm/brotli"
"github.com/go-via/via/h"
"github.com/stretchr/testify/assert"
)
func TestCompressionBrotli(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
reader := brotli.NewReader(w.Body)
decompressed, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Contains(t, string(decompressed), "Hello Via!")
}
func TestCompressionGzip(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "gzip")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "gzip", w.Header().Get("Content-Encoding"))
reader, err := gzip.NewReader(w.Body)
assert.NoError(t, err)
decompressed, err := io.ReadAll(reader)
assert.NoError(t, err)
assert.Contains(t, string(decompressed), "Hello Via!")
}
func TestCompressionNone(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Header().Get("Content-Encoding"))
assert.Contains(t, w.Body.String(), "Hello Via!")
}
func TestCompressionMinSize(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text("Small"))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Header().Get("Content-Encoding"))
}
func TestCompressionLargeResponse(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("A", 2000)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
}
func TestCompressionDatastarJS(t *testing.T) {
v := New()
req := httptest.NewRequest("GET", "/_datastar.js", nil)
req.Header.Set("Accept-Encoding", "br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
}
func TestCompressionBrotliPreferred(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "gzip, br")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"))
}
func TestCompressionZstdDisabled(t *testing.T) {
v := New()
v.Page("/", func(c *Context) {
c.View(func() h.H {
return h.Div(h.Text(strings.Repeat("Hello Via! ", 200)))
})
})
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", "zstd, br, gzip")
w := httptest.NewRecorder()
v.handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "br", w.Header().Get("Content-Encoding"), "Should use Brotli, not zstd")
assert.NotEqual(t, "zstd", w.Header().Get("Content-Encoding"))
}