United Kingdom: +44 (0)7916 161 228

Server-side rendering with the SAFE stack

As part of our commitment to continuous learning, each month one of the team presents on a technical topic which we think could help us on our projects. This month I gave a session on server-side rendering.

What is server-side rendering in a single-page web app?

Normally in a single-page app, a minimal HTML file is served to the client with an empty <div> that serves as the app root. The client also receives a JavaScript bundle, which it runs to create the initial view inside the app root.

Server-side rendering means creating the initial view on the server and including it in the HTML served to the client.

What are the benefits?

  • The user sees a meaningful page much faster, especially if it's their first visit to the site and they don't have your JavaScript bundle cached. A limitation here is that parts of the page may be unresponsive until the JavaScript is dowloaded and executed, because that's when events are added to buttons and so on.
  • It is easier for search engines to index your home page content, which could result in better search rankings.

What are the costs?

  • More work is done on the server for each page request. The server has to render the page, and then do all the work it would do anyway to serve all necessary files. You may be able to limit the number of times the page is rendered by caching the rendered HTML if it doesn't change very frequently.
  • More data needs to be delivered to a client overall. The initial HTML will be larger because it cotains a rendered view
    and initial Elmish model state must also be injected into it.
  • More development complexity. Parts of the UI code must run in both .NET and JS. Care must be taken that all functions used are supported on both environments. If parts of your view are outside of the Elmish architecture, such as custom JavaScript components, then extra steps are needed to make them work correctly on initial load.

How do you use server-side rendering with the SAFE stack?

Let's assume that we have a purely Elmish front end code, e.g. The SAFE template. What are the pieces that we need?

  • All of the types and functions needed to produce the starting state (the state we want to pre-render) and state of the app should be moved to shared code, accessible from both the client and the server.
  • The server needs to be able to render a model into a view, and convert that to HTML to send as the initial page.
  • The server needs to provide the initial state to the client as a JavaScript object.

In the SAFE template, the client loads the page and immediately makes a request to the server to get the initial state, which sets a counter to a fixed value of 42. To demonstrate server-side rendering, we will change this so that the initial state is rendered by the server and the web request is no longer needed. We will also make the initial state a random number for each page request instead of a fixed number to prove that the client gets the correct initial state with each page load.

Rendering the view on the server

  • First, we add Fable.Elmish.React and Fable.React into the server's paket.references file.
  • Move all of the model types and view code into the shared code file.
  • Add an initialModel function into shared code, and use it in the Elmish init function:
let initialModel () = { Counter = Some { Value = 42 } }

let init () : Model * Cmd<Msg> =
    initialModel(), Cmd.none
  • Remove the static index.html file and replace it with a makeInitialHtml function in server code:
let makeInitialHtml model =
    html [ ] [
        head [ ] [
            title [ ] [ str "SAFE Template" ]
            ...
            ]
        body [ ] [
            div [ Id "elmish-app" ] [ view model ignore ]
            script [ Src "./bundle.js" ] [ ] ] ]

Note that the makeInitialHtml takes a model and inside the elmish-app div in the body, it renders the model into full view, instead of the empty <div> we had in the static HTML file.

  • Next, we need to add a server route to create the HTML using this function, instead of serving the static file.
get "/" (fun next ctx ->
    task {
        let html = initialModel () |> makeInitialHtml |> Fable.Helpers.ReactServer.renderToString
        return! htmlString html next ctx
    })

Here we create an initial model, which is actually still a fixed value for now, turn it into a React element and then convert that to a string using ReactServer.renderToString. This function can run on the server and create an HTML string. It doesn't add events since it's not running the client-side context of a web page with a DOM.

This is now enough to have the basic server-side rendering working. In the browser's dev tools we now see that the initial page load contains the HTML for the starting state of the view.
We don't have to make a request to the server immediately after loading the page, so we no longer see the "Loading..." text.

Passing state to the client

What we have so far only works correctly because the initial model is a fixed value that is calculated in the same way on the server and the client. What if the server needs to calculate the initial model using data the client does not have access to? To handle this case, we need to create the initial model only once on the server and pass it to the client.

  • Let's move the initialModel function to the server and make it generate a random number for the counter each time it's called:
let rand = System.Random()
let initialModel () = { Counter = Some { Value = rand.Next(0, 50) } }
  • Now we need to update our server-side makeInitialHtml function so that it puts the initial model directly into the HTML served to the client:
let makeInitialHtml model =
    html [ ] [
        head [ ] [
            title [ ] [ str "SAFE Template" ]
            ...
            ]
        body [ ] [
            div [ Id "elmish-app" ] [ view model ignore ]
            script [ ] [ RawText (sprintf "var __INIT_MODEL__ = %s" (Thoth.Json.Net.Encode.Auto.toString(0, model))) ]
            script [ Src "./bundle.js" ] [ ] ] ]

Here we are generating a <script> tag with a small amount of JavaScript code to assign the current model to a __INIT_MODEL__ global variable. This needs to be inserted into the HTML before the main bundle so that the variable is ready to be read when the main client code is run. This is what it might look like in the raw HTML:

<script>
    var __INIT_MODEL__ = {
        "Counter": {
            "Value": 28
            }
        }
</script>
  • Now we need to need to update the client init function to read this variable, instead of attempting to create its own initial state
let init () : Model * Cmd<Msg> =
    let model : Model = Browser.window?__INIT_MODEL__
    model, Cmd.none

Now we have an app where the initial state is calculated once on the server and passed to the client along with the initial HTML view. The client starts an Elmish app loading in the initial state from the HTML so that the state is correctly synchronised with the view.

Efficient rendering with React

The final step we need to take is to replace our usage of Program.withReact with Program.withReactHydrate in our Client.fs file:

...
|> Program.withReactHydrate "elmish-app"
...

This function takes an existing plain DOM rendered by the server and adds event listeners to it, rather than build up the initial DOM from scratch. This should reduce the initial rendering work done by the client, to take full advantage of server-side rendering.

Further reading

The full code repository shows the final result of the steps described here.

For more details see the fable-react docs on server-side rendering.