all repos — gomponents @ b0cec2ca4942ef8455090e71d1026efab673a7ee

HTML components in pure Go

Reduce number of allocations for rendering elements and attributes (#265) This change does some optimizations to reduce the number of memory heap allocations. Also: - Checks for `io.StringWriter` and uses that if available. - Pre-allocates byte slices for commonly used strings. - Fixed the benchmarks to not count `strings.Builder` allocations. These are the benchmark results from before the changes (with the benchmark fix): ``` go test -bench . -benchmem ./... goos: darwin goarch: arm64 pkg: maragu.dev/gomponents cpu: Apple M3 Max BenchmarkAttr/boolean_attributes-16 25371446 46.81 ns/op 40 B/op 3 allocs/op BenchmarkAttr/name-value_attributes-16 13534495 88.24 ns/op 72 B/op 4 allocs/op BenchmarkEl/normal_elements-16 14068998 86.28 ns/op 48 B/op 5 allocs/op PASS ok maragu.dev/gomponents 4.894s PASS ok maragu.dev/gomponents/components 0.163s goos: darwin goarch: arm64 pkg: maragu.dev/gomponents/html cpu: Apple M3 Max BenchmarkLargeHTMLDocument-16 526 2092062 ns/op 2950444 B/op 90031 allocs/op PASS ok maragu.dev/gomponents/html 1.463s PASS ok maragu.dev/gomponents/http 0.168s ? maragu.dev/gomponents/internal/assert [no test files] PASS ok maragu.dev/gomponents/internal/import 0.135s ``` After: ``` go test -bench . -benchmem ./... goos: darwin goarch: arm64 pkg: maragu.dev/gomponents cpu: Apple M3 Max BenchmarkAttr/boolean_attributes-16 51947022 19.75 ns/op 8 B/op 1 allocs/op BenchmarkAttr/name-value_attributes-16 18138727 64.87 ns/op 24 B/op 2 allocs/op BenchmarkEl/normal_elements-16 21048692 55.48 ns/op 24 B/op 2 allocs/op PASS ok maragu.dev/gomponents 3.687s PASS ok maragu.dev/gomponents/components 0.158s goos: darwin goarch: arm64 pkg: maragu.dev/gomponents/html cpu: Apple M3 Max BenchmarkLargeHTMLDocument-16 714 1535017 ns/op 2630426 B/op 40028 allocs/op PASS ok maragu.dev/gomponents/html 1.398s PASS ok maragu.dev/gomponents/http 0.171s ? maragu.dev/gomponents/internal/assert [no test files] PASS ok maragu.dev/gomponents/internal/import 0.137s ```

Markus Wüstenberg
commit

b0cec2ca4942ef8455090e71d1026efab673a7ee

parent

bf0db98f68ce95c18bb060913a6602de981fa715

1 file changed, 55 insertions(+), 8 deletions(-)

changed files
M gomponents_test.gogomponents_test.go
@@ -60,16 +60,22 @@ }
func BenchmarkAttr(b *testing.B) { b.Run("boolean attributes", func(b *testing.B) { + var sb strings.Builder + for i := 0; i < b.N; i++ { a := g.Attr("hat") - _ = a.Render(&strings.Builder{}) + _ = a.Render(&sb) + sb.Reset() } }) b.Run("name-value attributes", func(b *testing.B) { + var sb strings.Builder + for i := 0; i < b.N; i++ { a := g.Attr("hat", "party") - _ = a.Render(&strings.Builder{}) + _ = a.Render(&sb) + sb.Reset() } }) }
@@ -140,17 +146,33 @@ assert.Equal(t, `<div><br><br></div>`, e)
}) t.Run("returns render error on cannot write", func(t *testing.T) { - e := g.El("div") - err := e.Render(&erroringWriter{}) - assert.Error(t, err) + // This weird little constructs makes sure we test error handling of all writes + for i := 0; i <= 33; i++ { + t.Run(fmt.Sprintf("failing write %v", i), func(t *testing.T) { + e := g.El("div", g.Attr("id", "hat"), g.Attr("required"), g.El("span"), + g.Group{g.El("span"), g.Attr("class", "party-hat")}, + g.Text("foo"), g.Textf("ba%v", "r"), g.Raw("baz"), g.Rawf("what comes after %v?", "baz")) + + w := &erroringWriter{failingWrite: i} + err := e.Render(w) + assert.Error(t, err) + + sw := &stringWriter{w: &erroringWriter{failingWrite: i}} + err = e.Render(sw) + assert.Error(t, err) + }) + } }) } func BenchmarkEl(b *testing.B) { b.Run("normal elements", func(b *testing.B) { + var sb strings.Builder + for i := 0; i < b.N; i++ { e := g.El("div") - _ = e.Render(&strings.Builder{}) + _ = e.Render(&sb) + sb.Reset() } }) }
@@ -161,10 +183,29 @@ _ = e.Render(os.Stdout)
// Output: <div><span></span></div> } -type erroringWriter struct{} +type erroringWriter struct { + failingWrite int + actualWrite int +} func (w *erroringWriter) Write(p []byte) (n int, err error) { - return 0, errors.New("no thanks") + if w.failingWrite == w.actualWrite { + return 0, errors.New("no thanks") + } + w.actualWrite++ + return 0, nil +} + +type stringWriter struct { + w io.Writer +} + +func (w *stringWriter) Write(p []byte) (n int, err error) { + return w.w.Write(p) +} + +func (w *stringWriter) WriteString(s string) (n int, err error) { + return w.w.Write([]byte(s)) } func TestText(t *testing.T) {
@@ -295,6 +336,12 @@ e := g.Group{g.El("div"), g.El("span")}
assert.Equal(t, "<div></div><span></span>", e) assert.Equal(t, "<div></div>", e[0]) assert.Equal(t, "<span></span>", e[1]) + }) + + t.Run("returns render error on cannot write", func(t *testing.T) { + e := g.Group{g.El("span")} + err := e.Render(&erroringWriter{}) + assert.Error(t, err) }) }