George Iliadis

Templating and the cost of JSON

Introduction

Recently on linkedIn, I made a post claiming that maybe you all you need is a templating system for your backend. The origin of such a claim started when I discovered the go templ library. Now, on your typical modern day SPA web application architecture. You will have a frontend/backend separation, where dedicated teams for each will work to provide a complete product together. Typically the frontend side, is where most of the performance issues are noticed, thus there will come a time that you might consider moving initial rendering on the server. Which in turn, means that following the stereotypical path you will pull out a fullstack JS framework, that uses the same framework you do in the front, and applies it in the backend for the rendering. Chances are, you will use NextJS, Nuxt, Astro, Analog or whatever. And you will do that together will your already existant backend infrastructure.

Given you made the above change, each time when you make a request for a page, you make it to your "NextJS" server, from there you will proxy the request to another service, get the typical JSON response that you would get if you had a CSR application, run client side code on your backend to render your html, then add a bunch of JS and then send it to your client for final consumption.

Although this is a thousand times better than doing everything client side, it is still not optimal. Why proxy a request over a network? All you do is extra time with a round trip, you are adding points of failure in between, creating services that need to be maintained and be in sync with their data types. Not to mention, you are also running slow JS server side (compared to most backend languages), increasing your server bills, listen Matteo preach. And you still, have to serialize and deserialize JSON which is expensive.

The weight of JSON

Now, If that last sentence made you go "What?!" or "So what?" this article is for you and this is what we will focus on from now on. You might think that serializing and deserializing things is cheap, but it isn't. I once heared a ridiculous claim that the majority of cloud service processing costs are due to serialization. Here is a relevant link. Thus, today we will explore this claim and benchmark it ourselves.

As always, I built an isolated demo myself to explore and measure the processing time. I will be using GO a much loved and fast language used to build backend services, a language in which I absolutely suck at btw. On both scenarions I will be using echo as a server. For the test, I will be making a request to an endpoint and trigger a response a bunch of times. For each version, I will be benchmarking the processing time of those actions.

On scenario A, I will import some JSON, serialize it and then send a raw JSON response. I will only be measuring the cost of serialization on the backend. I wont be measuring the added cost of deserializing it on a client, nor the time it takes to build a view as those are irrelevant.

On scenario B, I will use the templ package mentioned above, to actually create a FULL html page response. I will be using a Layout template along with a Home page template. I will be using a product struct similar to the one above to enhance the page.

Off we go!

A) The JSON version.

The JSON at hand:

  {
    "name": "Example Product",
    "price": 19.99,
    "quantity": 100
  }

The Go code:

  var product Product
  // Start timer 
  start := time.Now()
  err = json.Unmarshal(jsonData, &product)
  if err != nil {
    fmt.Println("Error parsing JSON:", err)
  }
  // End timer
  end := time.Now()
  fmt.Println("Time to render in nanoseconds: ", end.Nanosecond() - start.Nanosecond())

Pretty simple. Now lets curl it a bunch of times in a row and we get the below timings in NANOSECONDS

Time to render in nanoseconds:  237000
Time to render in nanoseconds:  164000
Time to render in nanoseconds:  29000
Time to render in nanoseconds:  29000
Time to render in nanoseconds:  33000
Time to render in nanoseconds:  30000
Time to render in nanoseconds:  39000
Time to render in nanoseconds:  27000
Time to render in nanoseconds:  27000
Time to render in nanoseconds:  30000

Disclaimer: I am not sure why the first couple of times are significantly slower, I suspect it is some Cold Start thing going on, if not please shoot me an email. But, i will ignore those slow ones and take only the rest of values for an average of ~30.500 ns

And what we get back as a response:

{"name":"Example Product","price":19.99,"quantity":100}

B) The template.

The Go code:

  func (u HomePageController) HomePageControllerView(c echo.Context) error {
    product := Product{"test_product", 12.99, 100}
    return render(c, view.HomePageTemplate(product.Name,product.Price,product.Quantity))
  }

  // The render method that we will be benchmarking
  func render (c echo.Context, comp templ.Component) error {
    start := time.Now()
    template := comp.Render(c.Request().Context(),c.Response().Writer)
    end := time.Now()
    fmt.Println("Time to render in nanoseconds: ", end.Nanosecond() - start.Nanosecond())
    return template
  }

  //The rendering template code
  templ HomePageTemplate(name string, price float32, quantity int){
      @LayoutTemplate(){
        <div>
            <span>{name}</span>
            <span>{price}</span>
            <span>{quantity}</span>
        </div>    
      }
  }

templ LayoutTemplate(){
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
        <title>Document</title>
        <script src="assets/js/main.js" type="module"/>
    </head>
    <body>
        <header>
            <nav>
                <a href="/">Home</a>
                <a href="/something">Random Param route</a>
            </nav>
        </header>
        <h1>Hello from the go template!</h1>
        {children...}
    </body>
    </html>
  }

Again pretty simple, we have a page layout template, a route template, and a product struct that we use to built the view. We are benchmarking the time of the template builting itself.

Again lets curl it a bunch of times:

Time to render in nanoseconds:  82000
Time to render in nanoseconds:  23000
Time to render in nanoseconds:  33000
Time to render in nanoseconds:  23000
Time to render in nanoseconds:  21000
Time to render in nanoseconds:  23000
Time to render in nanoseconds:  19000
Time to render in nanoseconds:  17000
Time to render in nanoseconds:  20000
Time to render in nanoseconds:  22000

Similarly with the previous example the first time its actually quite slow and then it speeds up. So again I will only consider later values of the test, which amount to ~20100 ns average.

And the end product of the response is, a complete html page:

<!doctype html>
<html lang="en">
      <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <script src="assets/js/main.js" type="module"></script>
      <script async="" src="//localhost:35729/livereload.js?snipver=1" id="livereloadscript">
      </script>
    </head>
    <body>
      <header>
      <nav>
          <a href="/">Home</a> 
          <a href="/something">Random Param route</a>
        </nav>
      </header>
     <h1>Hello from the go template!</h1>
     <div>                         
      <span>test_product</span> 
      <span>12.99</span> 
      <span>100</span>
    </div>
  </body>
</html>

Its clear from the above the template way is about 33% faster from the traditionaly used JSON, especially considering that the end result was a finalized html page. Even if those 2 scored similarly in time, you still would have the cost or deserializing and doing the actual rendering, either on the server or worse the client. Seems we have a winner.

So next time you are about to rush to SSR solution for a website from a service you own. You might want to consider templating instead of a dedicated fullstack application, just for your frontend.

Date: