Brotli Compression. (#12)
Co-authored-by: João Gonçalves <joao.goncalves01@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ Via takes a radical stance:
|
||||
- No front-end fatigue.
|
||||
- Single SSE stream.
|
||||
- Full reactivity.
|
||||
- Built-in Brotli compression.
|
||||
- Pure Go.
|
||||
|
||||
|
||||
|
||||
7
go.mod
7
go.mod
@@ -5,6 +5,8 @@ go 1.25.4
|
||||
require maragu.dev/gomponents v1.2.0
|
||||
|
||||
require (
|
||||
github.com/CAFxX/httpcompression v0.0.9
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-via/via-plugin-picocss v0.0.0-20251112183909-4485ba2e31d8
|
||||
github.com/starfederation/datastar-go v1.0.3
|
||||
@@ -12,12 +14,11 @@ 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/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/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
|
||||
)
|
||||
|
||||
10
go.sum
10
go.sum
@@ -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/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
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/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
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 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
15
via.go
15
via.go
@@ -18,6 +18,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/CAFxX/httpcompression"
|
||||
"github.com/go-via/via/h"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
@@ -30,6 +31,7 @@ var datastarJS []byte
|
||||
type V struct {
|
||||
cfg Options
|
||||
mux *http.ServeMux
|
||||
handler http.Handler
|
||||
contextRegistry map[string]*Context
|
||||
contextRegistryMutex sync.Mutex
|
||||
documentHeadIncludes []h.H
|
||||
@@ -203,7 +205,7 @@ func (v *V) Start() {
|
||||
v.devModeRestore()
|
||||
}
|
||||
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) {
|
||||
@@ -274,8 +276,19 @@ func (v *V) devModeRestore() {
|
||||
// New creates a new *V application with default configuration.
|
||||
func New() *V {
|
||||
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{
|
||||
mux: mux,
|
||||
handler: compressionAdapter(mux),
|
||||
contextRegistry: make(map[string]*Context),
|
||||
devModePageInitFnMap: make(map[string]func(*Context)),
|
||||
cfg: Options{
|
||||
|
||||
165
via_test.go
Normal file
165
via_test.go
Normal 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"))
|
||||
}
|
||||
Reference in New Issue
Block a user