-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
Conversation
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.
I noticed that the context does not offer methods to write to the
http.ResponseWriterwithout using intermediate buffers. We can still access theResponseWriterbut then we need to callWriteHeadermanually, and do not forget to add the content type, which are handled by echo otherwise.So I added two methods,
BlobWriteandHTMLWritewhich instead of[]bytetake as parameter a functionfunc (io.Writer) error. I also added changed the methodRenderto useHTMLWrite, thus avoiding the intermediate buffer and writing directly to theResponseWriterobject.I wrote a benchmark for the
Rendermethod, here are the results without and with my change: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
Renderfunction of theRendererobject is called, the headers have already been written to theResponseWriter. 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 :)