Demystifying SSR

Written last year
#learn#webdev

Browsers render HTML content

When you visit a site like https://example.com, you see the content for that page. That content is rendered with HTML.

The homepage content for example.com

We know that HTML must be somewhere on this webpage, because we can see the "Example Domain" heading, along with other text content.

But how did our server render that HTML? If you right-click the page in your browser, and click "View Page Source", you'll be able to see what HTML was sent to our browser:

<!DOCTYPE html>
<html>
<head>
  <title>Example Domain</title>
  <meta charset="utf-8" />
  <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style type="text/css">...</style>    
</head>
<body>
  <div>
    <h1>Example Domain</h1>
    <p>This domain is for use in illustrative examples in documents. You may use this
    domain in literature without prior coordination or asking for permission.</p>
    <p><a href="https://www.iana.org/domains/example">More information...</a></p>
  </div>
</body>
</html>

When we view page source, we see what the server sent before any JavaScript runs.

From the HTML above, we see that <h1>Example Domain</h1> was part of the initial HTML that came with our webpage.

This means our HTML content was definitely "server rendered", because JavaScript couldn't have run yet.

But that doesn't tell us the whole story!

Rendering on a "static" server

One approach to rendering HTML from the server is to just load it directly from the filesystem.

FilesystemServerBrowserFilesystemServerBrowserπŸ™‹ GET https://example.comπŸ“‚ "~/index.html"βœ… File existsβœ… 200 OK

In a static server setup, HTML is served straight from a folder. The web server translates the requested URL paths to filepaths.

The server sends the HTML content that was saved in those files as-is back to the browser.

FilesystemServerBrowserFilesystemServerBrowserπŸ™‹ GET /πŸ“‚ "~/index.html"βœ… File existsβœ… 200 OKπŸ™‹ GET /about-meπŸ“‚ "~/about-me.html"βœ… File existsβœ… 200 OKπŸ™‹ GET /people/ryanπŸ“‚ "~/people/ryan.html"βœ… File existsβœ… 200 OK

If a user visits a URL like /people/scott, and there is no scott.html file in the people folder, our static server will return a 404 status.

FilesystemServerBrowserFilesystemServerBrowserπŸ™‹ GET /people/scottπŸ“‚ "~/people/scott.html"πŸ”₯ File not found!πŸ”₯ 404 NOT FOUND

In many cases, the server will redirect to a static 404.html file, so users don't receive a blank page.

Rendering on a "dynamic" server

Some applications don't show the same content for each user. If the server needs to render dynamic HTML for each page visit, it will need to run some code to create that HTML on the fly.

ServerBrowserServerBrowser🧠 Run some code, return some HTMLπŸ™‹ GET https://example.comβœ… 200 OK

If we wanted to implement something like this with Node.js, it would look like this:

import express from 'express'
const app = express()

// Define a handler for the "/" route
app.get('/', (req, res) => {
  console.log("Got homepage request!")
  res.send(`<!DOCTYPE html> ... </html>`)
})

// Start server on port 3000
app.listen(3000)

In order to run this website, I would need to run a Node.js server in production. The server would run that JavaScript function each time someone visited the homepage.

Better with Databases!

It's more common to see dynamic servers working with databases before they respond with the rendered HTML. They can use that data to inform what HTML should be rendered for a given URL.

DatabaseServerBrowserDatabaseServerBrowserπŸ™‹ GET /people/ryanπŸ’Ύ "SELECT * FROM people WHERE id=ryan"βœ… "{ id: "ryan", name: "Ryan", ... }"βœ… 200 OKπŸ™‹ GET /people/scottπŸ’Ύ "SELECT * FROM people WHERE id=scott"πŸ”₯ Record not foundπŸ”₯ 404 NOT FOUND

A "dynamic server" can access data that is being updated by another application– and always show HTML content that is up-to-date with the latest information.

In our "static server" setup from before, that would involve editing HTML files and deploying those to production anytime the content needs to change.

For the rest of this article, when I refer to "SSR", I'll specifically be referring to servers that dynamicly render content with code.

Rendering on the client

Let's take a look at another website: https://elm-spa.dev. This was an older project of mine, and created as a "single page application".

The homepage for elm-spa (please use Elm Land instead!)

Even though the user is still seeing HTML content, that HTML was rendered in the browser using JavaScript:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/style.css">
  <link rel="shortcut icon" href="/favicon.png" type="image/x-png">
  <title>elm-spa</title>
  <meta name="description" content="single page apps made easy">
  <meta name="image" content="https://elm-spa.dev/content/images/this-site.png">
</head>
<body>
  <script src="/main.js"></script>
</body>
</html>

When the main.js file runs, it injects the HTML in the page based on the current route. Because I'm using Elm, rather than JavaScript, the logic looks something like this:

import Pages.Home
import Pages.Guide
import Pages.Examples
import Pages.Example
import Pages.NotFound

view : Url -> Html Msg
view url of
    case url.path of
        "/" -> Pages.Home.view
        "/guide" -> Pages.Guide.view
        "/examples" -> Pages.Examples.view
        "/examples/hello-world" -> Pages.Example.view { id = "hello-world" }
        _ -> Pages.NotFound.view
// In "/main.js"
import Elm from './src/Main.elm'

// Start our app and injects HTML into the `<body>` tag
let app = Elm.init({
  node: document.querySelector('body')
})

If this website has multiple webpages, why do we call this a "single page application"?

With an SPA, we use the same "static server" approach, but with from single HTML file.

FilesystemServerBrowserFilesystemServerBrowserπŸ™‹ GET /πŸ“‚ "~/index.html"βœ… File existsβœ… 200 OKπŸ™‹ GET /guideπŸ“‚ "~/index.html"βœ… File existsβœ… 200 OKπŸ™‹ GET /examples/hello-worldπŸ“‚ "~/index.html"βœ… File existsβœ… 200 OK

The key difference from the "static server" is that every page opens the same ~/index.html file. That means we only need to define one HTML file for the entire application.

A more accurate name would be a "Single HTML File Application"– but SHFA didn't catch on for some reason...

Better with APIs

Our dynamic SSR server could render content from the database. When building web applications, you can use a static "single page application" that talks to any API you like. That API could be hosted by you, or something like GitHub's REST API.

FilesystemServerBrowserGitHub APIFilesystemServerBrowserGitHub APIπŸ™‹ GET /people/ryanπŸ“‚ "~/index.html"βœ… File existsβœ… 200 OKπŸ™‹ GET /api/users/ryanβœ… "{ id: "ryan", name: "Ryan", ... }"

Because our web client is able to send HTTP calls, it can communicate directly with the GitHub API. That API can only happen once the HTML is fetched, and the JavaScript is running.

Which approach is best?

I hope to write a follow-up article to this one soon– there's a lot of debate in this area. (And there doesn't need to be!)

In that article, I'll cover a few things that you should consider for your next web project. See you then! πŸ‘‹