Open thevtm opened 3 weeks ago
@thevtm thanks for that! Yeah, I would expect just pushing one big string literal with raw
being faster. That's okay.
I would actually be interested in finding out how long some large documents with, say, a few thousand elements and attributes (built deterministically) take to render, and then outputting the result of that benchmark as part of the CI pipeline, to catch whether refactors make anything worse. If you'd be interested in contributing, let me know! 😊
Firstly, I want to say I’ve really enjoyed using this library, so I decided to explore its performance under load.
To do this, I ran a benchmark comparing a few straightforward optimization strategies. Specifically, I looked at performance differences between smaller components with just a few elements versus a larger component containing 1000 elements.
One observation stood out: collapsing components into a single
Raw()
significantly improved performance, showing gains over 300x for the larger component.While I don’t have specific recommendations at the moment, I thought it might be interesting to share these findings with you.
Code
```go package main import ( "bytes" "fmt" "testing" gc "maragu.dev/gomponents" h "maragu.dev/gomponents/html" ) func StaticSmall() gc.Node { return h.H1(gc.Text("Hello, World!")) } func BenchmarkStaticSmall(b *testing.B) { buf := new(bytes.Buffer) for i := 0; i < b.N; i++ { StaticSmall().Render(buf) buf.Reset() } } func BenchmarkStaticSmallToRaw(b *testing.B) { node := StaticSmall() buf := new(bytes.Buffer) node.Render(buf) raw_str := buf.String() component := func() gc.Node { return gc.Raw(raw_str) } buf = new(bytes.Buffer) for i := 0; i < b.N; i++ { component().Render(buf) buf.Reset() } } func DynamicSmall(s string) gc.Node { return h.H1(gc.Text(s)) } func BenchmarkDynamicSmall(b *testing.B) { buf := new(bytes.Buffer) for i := 0; i < b.N; i++ { DynamicSmall("foobar").Render(buf) buf.Reset() } } func BenchmarkDynamicSmallToRaw(b *testing.B) { node := DynamicSmall("%s") buf := new(bytes.Buffer) node.Render(buf) raw_str := buf.String() component := func(s string) gc.Node { return gc.Raw(fmt.Sprintf(raw_str, s)) } buf = new(bytes.Buffer) for i := 0; i < b.N; i++ { component("foobar").Render(buf) buf.Reset() } } func StaticLarge() gc.Node { lis := make(gc.Group, 1000) for i := 0; i < 1000; i++ { lis[i] = h.Li(gc.Textf("Item %d", i)) } return h.HTML( h.Head( h.Meta(gc.Attr("charset", "UTF-8")), h.Title("Hello, World!"), ), h.Body( h.H1(gc.Text("Hello, World!")), h.P(gc.Text("This is a paragraph")), h.A(gc.Text("Click me"), gc.Attr("href", "/")), h.Ul(lis), ), ) } func BenchmarkStaticLarge(b *testing.B) { buf := new(bytes.Buffer) for i := 0; i < b.N; i++ { StaticLarge().Render(buf) buf.Reset() } } func BenchmarkStaticLargeCache(b *testing.B) { buf := new(bytes.Buffer) node := StaticLarge() for i := 0; i < b.N; i++ { node.Render(buf) buf.Reset() } } func BenchmarkStaticLargeToRaw(b *testing.B) { node := StaticLarge() buf := new(bytes.Buffer) node.Render(buf) raw_str := buf.String() component := func() gc.Node { return gc.Raw(raw_str) } buf = new(bytes.Buffer) for i := 0; i < b.N; i++ { component().Render(buf) buf.Reset() } } func BenchmarkStaticLargeToRawCache(b *testing.B) { node := StaticLarge() buf := new(bytes.Buffer) node.Render(buf) raw_str := buf.String() component := func() gc.Node { return gc.Raw(raw_str) } node_raw := component() buf = new(bytes.Buffer) for i := 0; i < b.N; i++ { node_raw.Render(buf) buf.Reset() } } ```