Rendering in Node

Sometimes it's necessary to render code inside of Node; for serving hyper fast first requests, testing or other purposes. Applications that are capable of being rendered in both Node and the browser are called isomorphic.

Rendering in Node is slightly different than in the browser. First off, to maintain performance all calls to subscriptions, effects, and reducers are disabled. That means you need to know what the state of your application is going to be before you render it - no cheating!

Secondly, the send() method inside router and view has been disabled. If you call it your program will crash. Disabling all these things means that your program will render O(n), which is super neat. Off to 10.000 QPS we go!

To render in Node call the .toString() method instead of .start(). The first argument is the path that should be rendered, the second is the state:

const http = require('http')
const client = require('./client')  // path to client entry point
http.createServer(function (req, res) {
  const html = client.toString('/', { message: 'hello server!' })
  res.setHeader('Content-Type', 'text/html; charset=utf-8')
  res.end(html)
})

In order to make our choo app call app.start() in the browser and be require()-able in Node, we check if module.parent exists:

const choo = require('choo')
const app = choo()

app.router((route) => [
  route('/', (params, state, send) => choo.view`
    <h1>${state.message}</h1>
  `)
])

if (module.parent) module.exports = app
else document.body.appendChild(app.start())

Rehydration

Now that your application is succesfully rendering in Node, the next step would be to make it load a JavaScript bundle once has loaded the HTML. To do this we will use a technique called rehydration.

Rehydration is when you take the static, server-rendered version of your application (static HTML, dehydrated because it has no logic) and rehydrate it by booting up the JS and attaching event handlers on the DOM to make it dynamic again. It's like restoring flavor to cup noodles by adding hot water.

Because we're using something called morphdom under the hood, all we need is point at an id at the root of the application. The syntax for this is slightly different from what we've seen so far, because we're updating a dehydrated DOM nodes to make them dynamic, rather than a new DOM tree and attaching it to the DOM.

const choo = require('choo')
const app = choo()

app.router((route) => [
  route('/', (params, state, send) => choo.view`
    <h1 id="app-root">${state.message}</h1>
  `)
])

if (module.parent) module.exports = app
else app.start('#app-root'))

When the JS is booted on top of the dehydrated application, it will look for the #app-root id and load on top of it. You can choose any name you like for the id, but make sure it's the same on every possible top level DOM node, or else things might break.

And that's it!

Caching

The trick to improving initial load time is to reduce latency in loading applications. Reducing latency is often more important than reducing payload size. In order to reduce latency content should be served from a server physically close to person requesting the content. Content Distribution Networks (CDNs) are networks of computers that cache content from a central server and strategically cache it around the world - sometimes there are even multiple caches per country.

But in order to use CDNs, the content served should be cacheable. This means distributing the lowest common denominator of any given page throughout the CDN.

Say we'd be building GitHub, a few great things to cache would be:

  • the JS bundles
  • the CSS bundles
  • all the marketing site content
  • every single project README.md page

Stuff you don't want to be caching on the CDN:

  • user specific pages
  • any type of queries

When caching the user specific pages, ideally only the static content would be cached, leaving out any dynamic content, i.e. user-specific content. For example, in order to not confuse people it's beneficial if the baseline for the UI doesn't show if a user is logged in or logged out - that way once the JS kicks in the user-specific parts of the UI (think web 2.0) can be progressively layered on top and provide a more custom experience. Think about it this way: the reason why a user is visiting a page is probably to look at the content first, so make sure that's available as fast as possible.

Once CDNs are in place, the content could be sped up further by providing more caching. At the code layer you probably want to pre-render as much content into a buffer and save it in bl. On the service / application layer you probably want to use nginx or varnish to cache data at a higher level / cache more aggressively. At the database layer you probably want to be using [materialized views][materialize-views] so that multiple queries to the same data don't have to be recomputed every time - caching it for at least the time it takes to re-compute a next query.

So to summarize application performance:

  • cache aggressively
  • cache on every level of the application to guard against accidental meltdowns
  • create content that's cacheable - having a good balance takes balance
  • design UIs so that they can be progressively enhanced to add user-specific information after loading

results matching ""

    No results matching ""