How to build React apps that load quickly using server side rendering

Clients side frameworks are great. They can help you build interactive and fast web applications which users love to use.

Unfortunately it is not all roses. There are several drawbacks, too. One of the main disadvantages is the initial loading speed.

Client side frameworks receive very little HTML from the server, but they receive a lot of JavaScript.

Then they have to request and wait for the data that should be rendered. Finally they compute and render everything on the user own machine.

On the other hand, traditional web sites render everything on the server and as soon as HTML is delivered the page is displayed and ready for the user.

Moreover, most web servers can render the page faster than a client side rendering.

As a result, the initial loading is very quick.

React to the rescue

Naturally, you want to have the best of both worlds. Fast initial loading and quick and interactive application. React can help you with that.

Here is how it works. First, it has the ability to render any component, including its data, server side. The result will be some HTML which will be then send to the browser.

Once this HTML is displayed by the user’s browser, React will make the same computations locally. Its smart algorithm will see that the result is identical to what is already displayed on the page.

As a result it will not make any changes. It will only attach the necessary event handlers.

How is this faster? Aren’t we doing almost the same things client side?

Yes, but almost is the keyword here.

First, as soon as the server responds to the browser request, the user will see the page. Therefore the perceived speed is much faster then before.

Second, because React identifies that no change to the DOM is necessary it will not touch it. This is the slowest part of the rendering.

In addition one request is saved, because all the data is served already rendered and React doesn’t need to request it from the server.

Is it possible that when the loading happens the page is displayed but the user is unable to interact with it because the event handler are not yet attached?

Theoretically it is possible. However, with this technique we are avoiding all the costly operations and as a result not only the perceived speed is fast, but the attaching the even handlers happens very quickly.

As a result, your application will be always interactive and your users will not notice any problems.

Examples

Enough talk, let’s see how it all works in practice. Our first example will be a simple one. We want to display a hello message which when clicked shows a notification.

Our examples will use NodeJS for the server part, but everything we see here can be also applied on other platforms and languages like PHP, Ruby, Python, Java or .NET.

We need the following node packages:

$ npm install babel react react-dom express jade

We will use express and jade to create a server for our example application.

The react and react-dom packages will provide the server side rendering for the React components.

The babel package can will enable us to load JSX components directly in Node with require('some-component.jsx') or require('some-component.js').

babel is actually much more powerful than that. It allows you to use ES6 features right now.

The application will consist of only 3 files in the following structure:

public/components.js
views/index.jade
app.js

component.js will contain our React components. index.jade will provide the base site template which loads all the JavaScript and app.js will be our Node server.

Let’s see what the index.jade template contains:

doctype
html
  head
    title React Server Side Rendering Example
  body
    div(id='react-root')!= react

    script(src='https://fb.me/react-0.14.0.js')
    script(src='https://fb.me/react-dom-0.14.0.js')
    script(src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js')

    script(src='/components.js', type='text/babel')

div(id='react-root')!= react is the most interesting part here. Its purpose is to contain the root React component. In addition, the react variable will contain the HTML of the server side rendered React component.

The first two included JavaScript files are React itself. Then, you also need to include Babel if you would like to use JSX in your components.

The last file included is the components. We let babel know that it should process the file by setting the type to text/babel.

It provides some basic HTML structure and loads all the JavaScript and React components that you need.

Let’s look now at our simple server:

require('babel/register')

var express = require('express')
  , app = express()
  , React = require('react')
  , ReactDOM = require('react-dom/server')
  , components = require('./public/components.js')

var HelloMessage = React.createFactory(components.HelloMessage)


app.engine('jade', require('jade').__express)
app.set('view engine', 'jade')

app.use(express.static(__dirname + '/public'))

app.get('/', function(req, res){
  res.render('index', {
    react: ReactDOM.renderToString(HelloMessage({name: "John"}))
  })
})

app.listen(3000, function() {
  console.log('Listening on port 3000...')
})

Most of this file is the usual way that you setup an express application. Nothing special. However, there are a few lines that require more attention.

The very first line

require('babel/register')

hooks Babel to your require. From then, you can require directly react components containing JSX and they will be translated to JavaScript on the fly. Just like in this next line.

var components = require('./public/components.js')

var HelloMessage = React.createFactory(components.HelloMessage)

The first line loads the React component written with JSX. Then React.createFactory generates a function which can create HelloMessage components.

app.get('/', function(req, res){
  res.render('index', {
    react: ReactDOM.renderToString(HelloMessage({name: "John"}))
  })
})

This is the part where the React component is rendered and then a web page with it is rendered and sent to the browser.

First a new HelloMessage component is created with the name property John. Then by using React.renderToString we render the HTML for the component.

Bare in mind that the component is only rendered, but not mounted, so any methods related to mounting are not called.

After the component is created we pass its HTML to the index template, which displays it as you saw earlier.

Here is what our component looks like:

var isNode = typeof module !== 'undefined' && module.exports
  , React = isNode ? require('react') : window.React
  , ReactDOM = isNode ? require('react') : window.ReactDOM

var HelloMessage = React.createClass({
  handleClick: function () {
    alert('You clicked!')
  },

  render: function() {
    return <div onClick={this.handleClick}>Hello {this.props.name}</div>
  }
})

if (isNode) {
  exports.HelloMessage = HelloMessage
} else {
  ReactDOM.render(<HelloMessage name="John" />, document.getElementById('react-root'))
}

As you see this look like any other React component, which uses JSX, except at the beginning and at the end. This is where you must take care to make the component work with both the browser and Node.

Advanced Example: Loading Server Data

Real web apps are usually doing much more than what you have seen so far. They often need to interact with the server and load data from it.

However, we don’t want this to happen during the server side rendering.

Let’s make a few small changes to our existing application. First, the template will now also load jQuery, which we will use only for requesting data from the server.

doctype
html
  head
    title React Server Side Rendering Example
  body
    div(id='react-root')!= react

    script(src='https://fb.me/react-0.14.0.js')
    script(src='https://fb.me/react-dom-0.14.0.js')
    script(src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js')
    script(src='http://code.jquery.com/jquery-2.1.3.js')

    script(src='/components.js', type='text/babel')

Our server will have one more request path to serve.

require('babel/register')

var express = require('express')
  , app = express()
  , React = require('react')
  , ReactDOM = require('react-dom/server')
  , components = require('./public/components.js')

var HelloMessage = React.createFactory(components.HelloMessage)


app.engine('jade', require('jade').__express)
app.set('view engine', 'jade')

app.use(express.static(__dirname + '/public'))

app.get('/', function(req, res){
  res.render('index', {
    react: React.renderToString(HelloMessage({name: "John"}))
  })
})

app.get('/name', function(req, res){
  res.send("Paul, " + new Date().toString())
})

app.listen(3000, function() {
  console.log('Listening on port 3000...')
})

The only difference from the previous example is these 3 lines.

app.get('/name', function(req, res){
  res.send("Paul, " + new Date().toString())
})

All it does, when /name is requested, is to return the name Paul with a current date.

Let’s look at the most interesting and important part of our app, the React component.

var isNode = typeof module !== 'undefined' && module.exports
  , React = isNode ? require('react') : window.React
  , ReactDOM = isNode ? require('react-dom') : window.ReactDOM

var HelloMessage = React.createClass({
  getInitialState: function () {
    return {}
  },

  loadServerData: function() {
    $.get('/name', function(result) {
      if (this.isMounted()) {
        this.setState({name: result})
      }
    }.bind(this))
  },

  componentDidMount: function () {
    this.intervalID = setInterval(this.loadServerData, 3000)
  },

  componentWillUnmount: function() {
    clearInterval(this.intervalID)
  },

  handleClick: function () {
    alert('You clicked!')
  },

  render: function() {
    var name = this.state.name ? this.state.name : this.props.name
    return <div onClick={this.handleClick}>Hello {name}</div>
  }
})

if (isNode) {
  exports.HelloMessage = HelloMessage
} else {
  ReactDOM.render(<HelloMessage name="John" />, document.getElementById('react-root'))
}

We have added 4 new small methods but everything else is exactly the same as before.

getInitialState: function () {
  return {}
},

loadServerData: function() {
  $.get('/name', function(result) {
    if (this.isMounted()) {
      this.setState({name: result})
    }
  }.bind(this))
},

componentDidMount: function () {
  this.intervalID = setInterval(this.loadServerData, 3000)
},

componentWillUnmount: function() {
  clearInterval(this.intervalID)
},

Once the component is mounted every 3 seconds it will request a name from the /name and then it will display it.

componentDidMount and componentWillUnmount are never called when the component is rendered, but only when it is mounted.

As a result during the server side rendering they are never called and loadServerData is also never called.

These three methods will only execute once the component is mounted which only happens in the browser.

As you see it is very easy to separate the parts that need to happen only in the browser from the rest and still keep all the code together, and it will still be working!

What’s next?

You have learned how to create fast loading React applications with server side rendering. However, my examples are only for a NodeJS server.

If you are using another technology (like PHP, .NET, Ruby, Python or Java), you can still get the benefits of React and server side rendering and this should be your next step to look at.

In addition I use JSX in the browser directly, thanks to Babel, which also affects performance. For production app it will be much faster to turn JSX into JavaScript before serving it to the browser.

I am sure that you can find out how to do that in your favorite language and web framework.


Other articles that you may like

Did you like this article?

Please share it

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about development with Node, Express, React and other web & mobile technologies.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2024 Terlici Ltd · Terms · Privacy