My Story: How I Learnt Server Side Rendering with React, Redux, Routing and Apollo Client
Recently I started exploring some techniques that can help improve the performance of mobile web pages. Server Side Rendering (SSR) is a common technique that comes to mind for performance boost. My goal was to understand how SSR works and then finally how it can actually help to improve performance.
I will share the basic steps to understand Server Side Rendering, hopefully it will help anyone looking to dive into SSR. Big thanks to Front-End Team for helping me out with this!!
Here I am assuming the reader has basic knowledge of React and Redux. If you are new to React Ecosystem, go through these links first.
What is Server Side Rendering ?
So Server side rendering is nothing special. Any ordinary static website is server side rendered. The benefit of SSR is when we use it with a Single Page Application (SPA). It gives us advantages of both client-rendered apps and backend-rendered apps.
SPA along with SSR is sometimes referred to as a Universal App.
SPA + SSR = Universal/Isomorphic App
When we render a normal client-side application on the server and then send a fully rendered page to the client as stringified HTML output, it is called SSR.
Simple, isn’t it? Not really!! Let’s first understand the basic difference between Client rendering and server rendering.
Difference between Client Side Rendering and Server Side Rendering
Client Side Rendering
- In Client-side rendering, the browser downloads a minimal HTML page, with no body, just some script tags and the page is rendered.
- It then requests the JavaScript from the server.
- After that it requests data from server fills the content into the HTML page.
- We can observe here that there are many roundtrips to the server that can increase latency.
- However the load time will be less as only minimal HTML was rendered as first byte.
- Even though the Time to Interactive will be less the user does not see any meaningful content at this time.
Server Side Rendering
- In Server Side Rendering complete HTML content is rendered from server to browser
- Initial data is also added to the HTML output witch is in string format.
- The browser then requests for Javascript from server.
- App re-renders (hydrates) on the screen.
- We can observe here that there are less roundtrips to the server.
- However the page load time will be more as only complete HTML with data was rendered.
- Even though the Time to Interactive will be more the user will see some meaningful content on the page.
Now that we have understood the basic differences and pros and cons of SSR lets start with some basic code!!
A Simple Example of SSR with Redux Store
1. server.js
import React from 'react';
import Express from 'express';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';
import configureStore from './redux/configureStore';
import App from './App';
const app = new Express();
app.use((req, res) => {
const initialStore = {
isFetching: false,
apps: {},
}; // configure store
const store = configureStore(initialStore);
const preloadedState = store.getState();
const appTree = (
<Provider store={store}>
<App />
</Provider>
);
const content = ReactDOMServer.renderToString(appTree);
res.write(`
<!doctype html>
<html>
<body>
<div id="app">${content}</div>
<script>
window.__data=${JSON.stringify(preloadedState)};
</script>
</body>
</html>
`);
res.end();
});
- Here configureStore() returns a new Store instance and getState() provides the state of the store.
- The renderToString() method by react-dom/server renders our app on the server and returns the HTML produced.
- Finally we inject content to our html template to get the final HTML page.
- To pass along the preloadedState, we attach it to window.__data__ inside a <script> tag.
2. client.js
import React from 'react'
import {hydrate} from 'react-dom'
import {Provider} from 'react-redux'
import configureStore from './redux/configureStore'
import App from './components/app';const state = window.__data__;
delete window.__data__;
const store = configureStore(state)hydrate(
<Provider store={store} >
<App />
</Provider>,
document.querySelector('#app')
)
- Initial state from window.__data__ is passed to our configureStore() function as the initial state.
- We use hydrate function from react-dom to hydrate the server rendered nodes.
The hydrate function is used to hydrate a container whose contents are server rendered. React will attempt to attach event listeners to the existing markup.
- React expects that the rendered content is identical between the server and the client. However it can patch up differences in text content.
So now we know how to do SSR!!! But is it enough for application in our real world apps? We would probably need to know a few more things.
Routing with SSR
In a client rendered app we wrap the app inside <BrowserRouter> for matching our routes to the browser url. In SSR we instead use a stateless <StaticRouter>.
The <StaticRouter> provides us a context prop, that can be used to identify if there were any redirects to the page.
import { StaticRouter } from 'react-router';
const context = {};
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
In a static server environment we can’t change the app state. Instead, we can use the context prop to find out what the result of rendering was. If the context prop has a url this means there was a redirect.
// example to check a 301 redirect
if (context.url) {
res.writeHead(301, {
Location: context.url,
});
res.end();
}
With Redux store and Static Routing in SSR we should be good to go for most real world applications. We can additionally also learn about using SSR with Apollo Client to quickly fetch data with GraphQL, server to server.
SSR with Apollo Client
Apollo Client is the best way to build a UI that fetches data with GraphQL. Using Server side rendering with Apollo allows our initial set of queries to return data immediately without a server roundtrip.
const client = new ApolloClient({
ssrMode: true,
link: createHttpLink({
uri: 'http://localhost:3010',
credentials: 'same-origin',
headers: {
cookie: req.header('Cookie'),
},
}),
cache: new InMemoryCache(),
});const App = (
<ApolloProvider client={client}>
<StaticRouter location={req.url} context={context}>
<Layout />
</StaticRouter>
</ApolloProvider>
);
We create a apollo client instance on server and pass ssrMode: true option to the Apollo Client constructor to avoid repeated force-fetching.
getDataFromTree()
- The getDataFromTree Function takes the React tree, determines which queries are needed to render them, and then fetches them all. It does this recursively down the whole tree if there are nested queries.
- It returns a promise which resolves when the data is ready in Apollo Client store. At the point that the promise resolves, Apollo Client store will be completely initialized, which should mean the app will now render instantly.
import { getDataFromTree } from "react-apollo"
const client = new ApolloClient(....);
getDataFromTree(App).then(() => {
// We are ready to render for real
const content = ReactDOM.renderToString(App);
const initialState = client.extract();
const html = <Html content={content} state={initialState} />;
res.status(200);
res.send(`<!doctype html>\n${ReactDOM.renderToString(html)}`);
res.end();
});
Hurray!!! Now we are ready to implement any kind of application with Server side rendering with Redux store, Routing and Apollo Client!!!
Now lets find the answer to another important question. When should we use SSR ?? Lets find out first the pros and cons of using SSR.
When should we use SSR ?
Pros of using SSR
- SEO — Major benefit of using SSR is in having an app that can be crawled for its content even for crawlers that don’t execute JavaScript code
- Performance — SSR can improve performance because a fully loaded app is sent down from the server on the first request.
- Another major improvement is in Time to Interactive.
- The point of SSR is a faster render of meaningful content.
- Reduces number of round trips between client and server.
Cons of using SSR
- SSR can improve performance if the application is small. But it can also degrade performance if it is heavy.
- It increases initial response size (html + data), which means the page takes longer to load.
- SSR sites become interactive and then block the main thread again. The page will go from interactive to frozen and back to interactive.
- SSR can make a small application complex.
Conclusion
- SEO : Even with JS Enabled Google Bot , JS rendering is a very expensive operation for Google Bot. Many other browsers like Opera Mini and UCBrowser still don’t execute JS. Thus SSR is extremely important for SEO purpose.
- Performance : If the cost of waiting for API calls to finish on the client is too high then SSR can improve the performance.
SSR can improve performance when HTTP calls block a meaningful paint.
- For pages that have mostly static content and do not require JS, SSR can improve performance drastically.
- If the bundle size of your app is huge, then SSR can help in performance.
At the end we can conclude that depending on the scale and technology stack used in your application we can use SSR for it’s pros by using it where it can give maximum benefit.
These basic steps and learnings of SSR can be used with any framework, i.e Vue.js or Angular. Here are some links.
Further Reading
- https://vuejs.org/v2/guide/ssr.html
- https://angular.io/guide/universal
- For more in-depth analysis of SEO and SSR here is a great article — https://medium.com/@benjburkholder/javascript-seo-server-side-rendering-vs-client-side-rendering-bc06b8ca2383