Isomorphic JavaScript, or universal JavaScript, is more and more popular nowadays. Basicly, it means JavaScript applications that can run on both client and server side. It has lots of advantages over traditional web development paradigms (e.g, Ruby on Rails, Django, etc.), in terms of SEO, response speed and code maintainability. The key part of isomporhic JavaScript is Server Side Rendering (SSR). This post will introduce the basic concepts of SSR and discuss some SSR implementations.

What is SSR?

The most common use case for server-side rendering is to handle the initial render when a user (or search engine crawler) first requests our app. When the server receives the request, it renders the required component(s) into an HTML string, and then sends it as a response to the client. From that point on, the client takes over rendering duties.

To make it clear, SSR includes the following points:

  1. It happens at the first request
  2. The server renders required components into string and sends it to the client
  3. The following rendering duty is on the client.

Why SSR?

Orginally, server-side MVC frameworks like Django and RoR render all the parts of web pages through templates. They have the following drawbacks:

  1. Long response time. Usually, the server needs to retrieve all the needed data first, renders the whole page into HTML string and then sends the fully-fledged page to the client. Too much work needs to do before the server sends the first byte of HTML page.
  2. Different logics on client and server side. In the server side, rendering logic is written in template languages such as Jinja2, ERB, etc., but in the client, all the rendering logic is written in JavaScript. Different logics require more efforts and add the burden of maintainance.

Then, some frontend MVC frameworks emerge, which take over all data fetching and rendering work and corresponding the server divides into web server and API server. Web server just sends the basic page to the client, and the client fetches data through AJAX request to API server and construct the whole page. It responds faster compared to rendering whole page with templates in server-side and the rendering logic is all written in JavaScript. However, it still has the following drawbacks:

  1. Data requests on the client are more time-consuming compared with those triggered in the server side. Usually, it’s faster to get all data packed in the server due to the data locality, and there’re also contraints on concurrent request number in the browser.
  2. Not very friendly for the web crawlers. Not all web crawlers are equiped with JavaScript runtime, and it’s bad for SEO since the first look of the website is nearly empty.

SSR can sovle the problem tactfully. It uses JavaScript both in client and server side, and renders the required components when the first request comes. Compared to rendering all the parts of the page, it has lower latency; and the initial response are richer than pure client side rendering.

How to achieve SSR?

With Redux, it’s easy to implement SSR. The basic idea is that the server assembles the basic store, obtain the initial state from it and send the state to the client along with the rendered HTML string. The client will recreate the store with the received state and take over the rendering job after that. Let’s see an example to make things clear.

Assuming there’s a page with three parts, i.e, head, body and footer, and each part has to get a color propery to render from an API invocation. With Redux, we need to construct the initial store:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const p1 = fetch('http://localhost:3000/api/color?part=head').then(res => res.json());
const p2 = fetch('http://localhost:3000/api/color?part=body').then(res => res.json());
const p3 = fetch('http://localhost:3000/api/color?part=footer').then(res => res.json());
Promise.all([p1, p2, p3]).then(([head, body, footer]) => {
// assemble an initial state
const preloadedState = {
head: { color: head.color},
body: { color: body.color},
footer: { color: footer.color}
};
// create a new Redux store instance
const store = configureStore(preloadedState);
...
})

Here configureStore is just a helper function to create a store from a preloadedState:

1
2
3
4
5
6
7
8
const configureStore = (preloadedState) => {
const store = createStore(
rootReducer,
preloadedState,
enhancer
);
return store;
};

Since we create the initial store, we can obtain the the HTML string with React’s renderToString method:

1
2
3
4
5
6
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
);

Here App contains a Head, a Body and a Footer:

1
2
3
4
5
6
7
8
9
10
11
12
// App/container.js
const App = () => {
return (
<div>
<Head />
<Body />
<Footer />
</div>
);
};
export default connect()(App);

Then, we send the initial state with HTML string to the client:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
...
Promise.all([p1, p2, p3]).then(([head, body, footer]) => {
...
// Create a new Redux store instance
const store = configureStore(preloadedState);
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
);
// Grab the initial state from our Redux store
const finalState = store.getState();
// Send the rendered page back to the client
res.send(renderFullPage(html, finalState));
});
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="app">${html}</div>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\x3c')}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`;
}

We send the initial state to the client by setting a global variable __PRELOADED_STATE__, the client will restore state from it:

1
2
const preloadedState = window.__PRELOADED_STATE__;
const store = configureStore(preloadedState);

You can see that the logic is the same by using the helper configureStore. Then, the client will re-render the page and compare the checksum to see whether it’s the same with the markup rendered at the server side (usually it should be identical). After that, the client will take over all the rendering work.

1
2
3
4
5
6
7
8
const rootElement = document.getElementById('app');
render(
<Provider store={store}>
<App/>
</Provider>,
rootElement
);

The source code can be found at my github, play around with it to better understand Redux SSR.

Stream the Rendering

There’re still something not so good of Redux SSR just mentionded above. We create three Promise objects to get the initial data, and only start rendering the page after all of them are solved, which is quite time-consuming. Ideally, if the color of the head is retrieved, the server should render the head and send it the client to make it inteactive so that the user will not wait too long. React-Server is born for that.

Each RootComponent of React-Server has a when property, and you can pass a Promise object to it. When the Promise object gets resolved, the server will render the component and send it to the client. Re-written the app with React-Server looks like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class HomePage {
handleRoute(next) {
...
const enhancer = composeEnhancers(
applyMiddleware(thunk)
);
const store = this.store = createStore(
rootReducer,
enhancer
);
this.headData = agent.get('/api/color?part=head').then((data) => {
store.dispatch(head.actions.receiveColor(data.body.color));
});
this.bodyData = agent.get('/api/color?part=body').then((data) => {
store.dispatch(body.actions.receiveColor(data.body.color));
});
this.footerData = agent.get('/api/color?part=footer').then((data) => {
store.dispatch(footer.actions.receiveColor(data.body.color));
});
...
return next();
}
getElements() {
const {store, headData, bodyData, footerData} = this;
return (
<RootContainer>
<RootElement when={headData}>
<Provider store={store}>
<Head />
</Provider>
</RootElement>
<TheFold />
<RootElement when={bodyData}>
<Provider store={store}>
<Body />
</Provider>
</RootElement>
<RootElement when={footerData}>
<Provider store={store}>
<Footer />
</Provider>
</RootElement>
</RootContainer>
);
}
}

Under the hood, each time the server parses a RootComponent, when the data is ready (when promise get resolved) the server will send it to the client. There is one special tag <TheFold>, which is used to specify all the components above the fold. Once the server sees it, it will send an additional <script> tag to the client and runtime engine in the client side will enable the event listeners on the components to make it interactive. After that, each RootComponent will come along with a <script> tag to make it interactive.

There is another trick of React-Server. To be isomorphic, it adds a cache layer of SuperAgent. The same codes run on both server side and client side, and once the server gets the data, the data will be cached and the client will obtain the data directly from the cache without triggering a network request.

The source code is also available at my github, you can compare it with the previous one.

Conclusion

This post introduced server side rendering and discussed its benifits. Two possible implementations has been given, the one using pure Redux has longer first-byte latency compared with React-Server. React-Server features fine granularity of streaming the SSR and better isomorphic JavaScript logic. Although its documentation is not comprehensive enough (anyway, it can improve your ability of reading source code), I believe it’s promising in the future.