/chromessr

Server side rendering for client side apps with ... chrome

Primary LanguageGoOtherNOASSERTION

chromessr

Server side rendering achieved using chrome (for real).

... Why?

Trying to make a frontend app to run on server side in order to prerender content is way more tedious than it should be, especially for content that is discarded almost immediately by all browsers.

Arguably, the main interest for SSR is for content to be accessible by search engine bots. And those bots tend to visit the same pages a lot of times.

Given all of that:

  • why delay every single request, included those for real humans, for SSR?
  • why get into the hassle of making apps that run on server while it's clearly pure client side?

Chromessr is an other take on that problem. You write your code for the frontend, and instead of trying to make it run on server, we run a client side environment on the server - that is, a browser.

Here is how it works:

  1. when asked for an url, app checks if we dumped it already. If we did, it retrieves its content and serve the placeholder html file with its #root element replaced.
  2. If the dump does not exist, the placeholder is returned as is.
  3. if the dump does not exist, or if it's old, the url is added to a queue
  4. a background process unqueues urls, if any, and it uses chrome to generate a dump of that page
  5. next time the url is asked for, the dump is loaded in the placeholder

That way, the server side rendering is always done in a background process and does not slow down your request. Since its generated by chrome, you're confident it will be the same content that your browser would have generated. And you don't need to adapt your code for that.

Additionally, chrome will wait for one second for your app to render. This means it also work for frontend apps which retrieve content from APIs.

Install

go get github.com/raff/godet
go get github.com/oelmekki/chromessr

Quick start

chromessr is both a library that you can use in your go project and a standalone tool.

To try the standalone tool, you can use docker:

docker build . -t chromessr
docker run --rm --cap-add=SYS_ADMIN --network=host chromessr

The SYS_ADMIN cap is required to use chrome remote protocol.

Give it a few seconds to start, then:

curl --data 'payload={"url":"http://my_app/","placeholder":"<html><body><div id=\"root\"></div></body></html>"}' http://localhost:3001/retrieve

The first time you issue this command, your placeholder will be returned as is, and your app url will be queued to be processed.

Try again a few seconds later, and the full content will be injected into your placeholder.

For this to work, you need:

  1. to make sure the page at the url you provided contains a #root element (this is what will be retrieved)
  2. to make sure that the placeholder you provide contains the exact string <div id="root"></div> (default for create-react-app template)

Using as a library

Chrome needs to be accessible in your $PATH as google-chrome-stable (see the Dockerfile in this repos for an example about how to add it in a container).

You also need to set the environment variable SSR_CACHE_PATH, which is the directory where to cache retrieved content (please provide an absolute path).

If you put it in a container, you need to add the capability SYS_ADMIN, in order to use chrome remote protocol.

Usage

First, you need to initialize the library (only once):

err := ssr.Init() // will start chrome and initiate the processing queue
if err != nil { /* handle error */ }

Then, when you need a page, just call Retrieve(url, placeholder):

content, err := ssr.Retrieve(url, placeholder)
if err != nil { /* handle error */ }

The placeholder is a html template, containing the <div id="root"></div> exact string.

If the url was already known, it will return the content of placeholder with its body replaced with cached content, else it will return the placeholder as is.

If the url was not known or if its cached version was old, a new dump will be queued to be generated, and the placeholder will be returned unmodified.

TODO

If the queue starts to get too big, we can easily parallelize by creating new chrome tabs.

  1. if the queue length exceeds SSR_MAX_REQUESTS, a new tab is opened, up to SSR_MAX_TABS
  2. if the queue length is smaller than SSR_SCALE_DOWN_THRESHOLD, all tabs but one are closed