- Any DOM element can trigger HTTP requests
- Any HTTP methods can be used (not just
- The response from the server is HTML or HTML fragment
- State management is is server-side, frontend is simple and dummy
HTML as content⌗
The concept of using HTML as content is nothing new 😉. We did that in the old days, before the time of SPAs and React and their friends, and we do it even now for mostly static content - you are looking at an example of this at the moment.
UI performance got more and more important, Google introduced the Core Web Vitals for ranking sites, so (initial) rendering started to move back to the backend. But instead of using the classic approach, many frameworks (like Next) provided a way to render the SPA application in the backend side - using the same AJAX calls, virtual DOM and other stuff, that aren’t ment to be in the backend originally.
Instead of just serving HTML by the backend. Quite crazy isn’t it?
JSON or HTML⌗
Rendering HTML or JSON in the backend from the same data should use basically very similar resources. If we want to do some escaping for the HTML part, than it’s a bit more resource heavy, but we have pretty good, reliable HTML template engines (and we need to escape/sanitize that data eventually somewhere anyway). It is also possible to cache these HTML fragments, if they can be re-used later.
Sending them over the network is also very similar, the HTML will be bigger in most cases, but usually it can be compressed quite well.
Displaying the new content in a browser might be a different topic. In case of HTML, we just swap the content of a DOM node, and we’re done. For JSON case there are a lot of options based on the framework. Let’s use the most straightforward way: do the escaping (if it hasn’t been done in the backend), render the HTML (using for example template literals, that are pretty fast), then replacing the content in the DOM.
Since we took the same steps, it should take pretty much the same time - but while we’re doing the rendering the frontend, the UI is blocked. And we also have to deliver the code for the frontend into the browser somehow, which takes extra time.
Moving the initial rendering phase of a SPA to the backend using nodejs and other tricks might help with the initial page load, especially if the rendered content can be cached as static HTML files, but in my experience it’s still far behind the classic approach in terms of performance.
I recently witnessed migrating a quite heavy site from a more classical approach to Next.js, and LCP got about 3 times worse (1.5 => 5+ sec). The content is hard to cache, storing the pre-rendered HTMLs is not an option. I’m still curious what the experts gonna come up with.
So I decided I give it a try, and created a small
golang project, where I can test HTMX and compare the HTML and the JSON based approach. For backend templates I used templ, I just found it recently, and looked like something made for the job. (It also does the escaping part, and security is an important topic for me.)
You can find the code here
I created to API endpoints, one that returns the POSTed data as JSON (using the JSON encoder from
golangs standard library), and one that returns it as HTML (via
I used ali to load test the backend. Of course this is a very basic demo, very far from real world usage, but I think we could see if the concept was fundamentally wrong.
Results (10s, unlimited rate, everything else on default, best of 3 runs):
json: 95250 req/s, 100% success rate, P90: ~0.6 ms
templ: 95687 req/s, 100% success rate, P90: ~0.59 ms
Well, ok, that’s basically just sending back a name/email pair as a JSON or HTML, but still, quite quick, and no difference. (I used to use python for such tasks, it’s in a completely different league.)
I’ve also created a small frontend, to test HTMX and to compare the JSON and HTML based approach. Even though HTMX implements the later, it does a lot of other things too, so I’ve created a bare minimum implementation for that approach, too.
There’s a form that is POSTed to the backend, and the card with the name and email address is replaced with the response. I’ve also implemented some kind of auto-repeat function to be able to test the performance, it re-sends the form when the card is replaced in the DOM.
Results (average of 10000 form posts, best of 3 runs):
json: 1.5864 ms/req
html: 1.5621 ms/req
htmx: 1.8445 ms/req
As I mentioned before, comparing my bare minimum implementations to HTMX is not fair, because it does a lot of other things, dispatches a butch of DOM events and such, but it still performed quite well, was only about 20% slower than the others.
The difference between the JSON and the HTML based approach is negligible (~1.5%), but it worth to mention that all the HTML runs were faster than the fastest JSON run.
I like this HTML fragment based approach, and while it might not fit for every website, I definitely will consider to use it in the future.