Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance observations #237

Open
thevtm opened this issue Oct 29, 2024 · 1 comment
Open

Performance observations #237

thevtm opened this issue Oct 29, 2024 · 1 comment

Comments

@thevtm
Copy link

thevtm commented Oct 29, 2024

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.

$ go test -v --benchmem --bench . ./scratch/go-components-benchmark_test.go
goos: linux
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStaticSmall
BenchmarkStaticSmall-12              	 3588330	       323.9 ns/op	     104 B/op	       7 allocs/op
BenchmarkStaticSmallToRaw
BenchmarkStaticSmallToRaw-12         	36431824	        39.23 ns/op	      24 B/op	       1 allocs/op
BenchmarkDynamicSmall
BenchmarkDynamicSmall-12             	 3742520	       321.5 ns/op	      96 B/op	       7 allocs/op
BenchmarkDynamicSmallToRaw
BenchmarkDynamicSmallToRaw-12        	 6603398	       204.6 ns/op	      72 B/op	       4 allocs/op
BenchmarkStaticLarge
BenchmarkStaticLarge-12              	    2380	    635167 ns/op	  206971 B/op	   10810 allocs/op
BenchmarkStaticLargeCache
BenchmarkStaticLargeCache-12         	    3696	    320086 ns/op	   39575 B/op	    5030 allocs/op
BenchmarkStaticLargeToRaw
BenchmarkStaticLargeToRaw-12         	  530688	      2081 ns/op	   18432 B/op	       1 allocs/op
BenchmarkStaticLargeToRawCache
BenchmarkStaticLargeToRawCache-12    	  573744	      2024 ns/op	   18432 B/op	       1 allocs/op
PASS
ok  	command-line-arguments	11.965s
Code
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()
	}
}
@markuswustenberg
Copy link
Member

@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! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants