Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Don't use an intermediate buffer when rendering templates #2863

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

Open
erdnaxeli wants to merge 1 commit into labstack:v5
base: v5
Choose a base branch
Loading
from erdnaxeli:feat/render-without-buffer

Conversation

@erdnaxeli
Copy link

@erdnaxeli erdnaxeli commented Jan 3, 2026

I noticed that the context does not offer methods to write to the http.ResponseWriter without using intermediate buffers. We can still access the ResponseWriter but then we need to call WriteHeader manually, and do not forget to add the content type, which are handled by echo otherwise.

So I added two methods, BlobWrite and HTMLWrite which instead of []byte take as parameter a function func (io.Writer) error. I also added changed the method Render to use HTMLWrite, thus avoiding the intermediate buffer and writing directly to the ResponseWriter object.

I wrote a benchmark for the Render method, here are the results without and with my change:

BenchmarkContextRenderTemplate-16 1000000 1172 ns/op 1433 B/op 15 allocs/op
BenchmarkContextRenderTemplate-16 1000000 1596 ns/op 1545 B/op 17 allocs/op

Note that for bigger templates (like a real full HTML page) the difference would probably be even bigger.

The only downside of this is that when the Render function of the Renderer object is called, the headers have already been written to the ResponseWriter. If the caller would change the behavior if the template rendering fails (like redirect to another page), it can't. Maybe there should be two methods: one that renders first (with an intermediate buffer), and another one that assume there will be no error and is more performant (without the intermediate buffer).

And I am not a big fan of the naming I used, but I could not find something better.

Let me know what you think of that :)

Copy link
Contributor

aldas commented Jan 4, 2026

basically we are comparing these two implementations

// Render renders a template with data and sends a text/html response with status
// code. Renderer must be registered using `Echo.Renderer`.
func (c *Context) Render(code int, name string, data any) (err error) {
	if c.echo.Renderer == nil {
		return ErrRendererNotRegistered
	}
	buf := new(bytes.Buffer)
	if err = c.echo.Renderer.Render(c, buf, name, data); err != nil {
		return
	}
	return c.HTMLBlob(code, buf.Bytes())
}
func (c *Context) Render2(code int, name string, data any) (err error) {
	if c.echo.Renderer == nil {
		return ErrRendererNotRegistered
	}
	c.writeContentType(MIMETextHTMLCharsetUTF8)
	c.response.WriteHeader(code)
	return c.echo.Renderer.Render(c, c.Response(), name, data)
}

Benchmarks

func BenchmarkContextRenderTemplateXOld(b *testing.B) {
	e := New()
	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
	tmpl := &TemplateRenderer{
		Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
	}
	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		rec := httptest.NewRecorder()
		c := e.NewContext(req, rec)
		c.Echo().Renderer = tmpl
		_ = c.Render(http.StatusOK, "hello", "Jon Snow")
	}
}
func BenchmarkContextRenderTemplateXNew(b *testing.B) {
	e := New()
	req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
	tmpl := &TemplateRenderer{
		Template: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
	}
	b.ResetTimer()
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		rec := httptest.NewRecorder()
		c := e.NewContext(req, rec)
		c.Echo().Renderer = tmpl
		_ = c.Render2(http.StatusOK, "hello", "Jon Snow")
	}
}

run benchmarks

go test -run="-" -benchmem -bench="BenchmarkContextRenderTemplateXOld" -count=8 > render_old.bench
go test -run="-" -benchmem -bench="BenchmarkContextRenderTemplateXNew" -count=8 > render_new.bench

compare benchmarks

sed -i 's/BenchmarkContextRenderTemplateXNew/BenchmarkContextRenderTemplateXOld/g' render_new.bench
go install golang.org/x/perf/cmd/benchstat@latest
benchstat render_old.bench render_new.bench

result:

goos: linux
goarch: amd64
pkg: github.com/labstack/echo/v5
cpu: Intel(R) Core(TM) i5-14600K
 │ render_old.bench │ render_new.bench │
 │ sec/op │ sec/op vs base │
ContextRenderTemplateXOld-20 3.339μ ± 15% 3.244μ ± 12% ~ (p=0.130 n=8)
 │ render_old.bench │ render_new.bench │
 │ B/op │ B/op vs base │
ContextRenderTemplateXOld-20 1.618Ki ± 0% 1.509Ki ± 0% -6.76% (p=0.000 n=8)
 │ render_old.bench │ render_new.bench │
 │ allocs/op │ allocs/op vs base │
ContextRenderTemplateXOld-20 22.00 ± 0% 20.00 ± 0% -9.09% (p=0.000 n=8)

ChatGPT says:

Execution time: No statistically significant change
Memory usage: Statistically significant improvement
Allocations: Statistically significant reduction
Overall: A clear memory optimization with neutral CPU impact

Me:
This is definitively improvement from memory usage side but I do not know if removing that buffer and ability to handle errors as the response will not be already written to client, is good a trade-off. I think these new methods are too much and decision point is should Context.Render internals should be changed or not.

p.s.: I looked how other do it (for example Gin) I think they are writing status and headers and then try to render.
p.s.s. this buffer logic was added 2015 with this commit 5a71f20

I will hold this PR on standby until v5 is released so I can concentrate getting release out.

erdnaxeli reacted with thumbs up emoji

@aldas aldas self-assigned this Jan 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

2 participants

AltStyle によって変換されたページ (->オリジナル) /