Back Original

EXTREME SERVER SIDE RENDERING

What is (Normal) Server Side Rendering?

Before we get into extreme server side rendering (XSSR), we have to talk about normal server side rendering (SSR). This comes in two flavours, which I'm calling old-school and new-school.

Old-school SSR involves having a server which uses some logic to create the HTML of the web page on-the-fly. For example, you might hit /users/39, and it might give you the details of user 39. These details might be from a database, or they might come from somewhere else. The important part is there's no corresponding 39.html on the disk. The HTML is created dynamically by the back-end server. On the front-end side, there's no JavaScript or other logic required to render the page. As a result, once the page is loaded, there's no ability for it to be dynamic.

New-school SSR is similar to old-school SSR, but it does involve a bit of front-end JavaScript logic. Generally, the way it's implemented is that when the user will interacts with the web page, the JS will make a request to the back-end. Then, the back-end will return some HTML, and the JS front-end will insert that HTML somewhere into the page. This allows us to have dynamic pages, at the cost of having to maintain some front-end logic.

Wouldn't it be nice if there was a better way?

Very slow HTTP servers

As a quick aside, let's talk about HTTP servers! When you make an HTTP request, what actually happens? Well, the server returns a bunch of ASCII text corresponding to some headers and the actual HTML content itself. This is sent one character at a time. Well, there's some buffering, but we can ignore this as it's not relevant to our purpose.

What happens if we send the HTML slowly, like at human rates? Below is a video where I use netcat to be a human HTTP server, and I have a browser connect to me. Netcat is line-buffered, meaning that each line is sent to the browser when I press enter.

You'll notice that the page renders on-the-fly as new HTML comes in. This is awesome, and it even works across both browsers I tested (Firefox and Chromium). Furthermore, even CSS works, so you could conceivably constantly move elements around (or hide existing elements and then add new ones). So, if we keep the HTTP connection open indefinitely, we can dynamically update the page from the server, without any JavaScript!

Getting User Input

Updating the page from the server is cool, but it would be nice to accept user input as well. The normal way to do this without JS is a <form> element. Unfortunately, this requires a page load, which defeats the point of XSSR! If we're willing to deal with reloading the page, normal old-school SSR would work fine. My friend DJ Chase pointed out a pretty cool trick, though:

  <iframe style="display: none;" name="transFrame" id="transFrame"></iframe>
  <form target="transFrame" method="POST" action="/action">
    <input type="hidden" name="uuid" value="USER-UUID-GOES-HERE" />
    <input type="Submit" value="" />
  </form>

Basically, you can point a form at an iframe, which will cause that iframe to be reloaded on form submission instead of the main page. If you hide that iframe, then user input is completely seamless!

Also worth pointing out is the uuid value, which can be generated by the server on initial page load. This is used to tie events to the original page (to prevent mixing up users when responding to events).

Building something with XSSR

Okay, so in theory we have everything we need to build a responsive web app. What should we build?

Why not a Flappy Bird clone? (Source code is available here.)

There's a few things to point out here. First of all, notice that we have everything a "real" Flappy Bird clone needs. We have dynamic text for the score, the pipes move as expected, and clicking causes the bird to jump. The latter is accomplished using the iframe trick above, combined with a hidden button that covers the entire screen. This allows you to click anywhere - in fact, after clicking once to select the invisible button, spacebar and enter both work to continue clicking.

Second of all, notice that the page never stops loading. This is because the physics engine runs entirely in the back-end, and new CSS is constantly being sent to update the position of the pipes, the bird, and the content of the score text at 60 FPS. Again, no front-end JS is required - turn it off and notice the site works exactly the same! You can also try curl-ing the page to see the infinite CSS in all its glory.

This isn't as bad as you might think

Performance

Performance, both client-side and server-side, is actually not awful. I will concede that the site does get laggy after a few minutes on both Firefox and Chromium. That said, we are streaming at 60 FPS, and for a page that updates e.g. only after user interaction, this might not even be a practical issue.

Server-side is even better. On my simple Rust server implementation, I can run about 500 games at the same time per core on a relatively old server in my homelab. This is significantly better than even a moderately-complex Ruby on Rails app! (I say this with love - I love Ruby on Rails). If you lowered the framerate, or used XSSR for a page that only updates after user interaction, performance could be increased significantly.

Bandwidth

Bandwidth usage is... entirely reasonable! One game takes about 20KiB/s of bandwidth to run. For perspective, this means it would take about 49 seconds of gaming to use the same bandwidth as loading a 1MB JavaScript application (which unfortunately is on the smaller side these days). Again, cutting down the framerate would improve this significantly.

Latency

oh no.

So, depending on where you are in the world, that demo might have majorly sucked. This is because every time you click, it requires a round trip to the server - both to update the physics engine on the server with your action, and then to return the new position of the bird to your browser. This is almost entirely dominated by the on-the-wire transit time. As a result, the demo is extremely playable if you're in Atlantic Canada (near-ish my house, where the server lives), and gradually gets more laggy the further you get. Lower down the East Coast, the latency is noticeable, but correctable as a human (you just need to click a little earlier). On the West Coast, it's distracting, and on the other side of the world, it's unplayable. If you were unimpressed by the demo, and you live far away from me, I highly recommend cloning the source code and running it locally.

But all the cool kids are into putting things on the edge, right? So uhh just do that I guess, and then it doesn't matter!

Taking Down Fedi

I was pretty proud of my demo, so I posted it on my Fediverse server (running Pleroma). Shortly after, it went down and stopped allowing posts to load. After about 20 minutes or so, it came back up, and I got a burst of notifications telling me people had reposted my post. Then it went down again! This cycle repeated 3 or 4 times before things stabilized. I can only assume that other Pleroma servers that saw the post went through similar rolling blackouts.

Turns out that some applications really don't like sites that keep the connection open indefinitely, slowly trickling data through! My friend Olivia went through Pleroma's source code and found that it was an issue when generating link previews, which happens server-side. I ended up logging a security bug, since this is a DoS attack vector, which was quickly patched.

Future Work

I think it would be funny to build a React fork that runs entirely server-side. The virtual DOM could be diff'd, and the diffs could be transmitted to the front-end via XSSR (again, no front-end JS required). I don't think this is very practical but it would be cool.

As a slightly more useful idea, I wonder how XSSR works on older devices. I assume it works reasonably well - slower internet may mean that dynamic rendering on-the-fly was an intentional feature. This makes me think you could write a proxy service that uses a headless browser instance to render web pages into a DOM. This DOM could be transmitted to the front-end via XSSR. I think this would let you use modern web pages on older devices that don't support JavaScript at all.