SSR

created by Apptension

Kaj Białas

Daniel Capeletti

Artur Kudeł

Michał Przyszczypkowski

What is Single Page App?

How SPA works?

A web application that fits on a single web page with the goal of providing a user experience similar to that of a desktop application. In an SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions.

What problem this has?

  • problems with SEO 
  • sharing single pages/urls 
  • performance in big applications
    • large application size 
    • long loading time

Posible ways to solve problems?

  • problems with SEO
    • problem only with special search engines (asian markets etc.)
    • pre-rendering
  • sharing single pages/urls 
  • performance in big applications
    • code optimization and asynchronous loading of resources

Probably the best way to solve problems?

Server Side Rendering

What SSR gives us?

What SSR gives us?

  • SEO
  • Sharing
  • Speed
  • Cache
  • Additional layer

How?

A bit of history...

Fully server side rendering

SPA

(without server side rendering)

How bots see SPA

Users happy

but sad bots...

A bit of history...

Fully server side rendering

SPA

(without server side rendering)

Server Side Rendering

+

SPA

=

problem solved?

people and robots are happy

but how does it exactly work?

BROWSER

PROXY (RENDERING) SERVER

API SERVER

URL

API

requests

responses

Generated html

CDN

GET

STATIC

ASSETS

  • main.min.js
  • main.min.css
  • ...

 

non-interactive

time

What we need?

on server side:

But in our example will be

  • python-react

  • React.NET

  • react-php-v8js

  • and more...

On client side?

  • React
  • Angular 2+
  • Vue.js
  • and probably some more frameworks...

SSR in React + Redux

App

switch (action.type) {
    case PREPARE_NUMBER:
      return Object.assign({}, state, { number: action.number * 10 });

    default:
      return state;
  }
export class App extends React.Component {
  render() {
    return (
      <Switch>
        <Route exact={true} path="/:id" component={StaticContainer} />
        <Route component={NotFound} />
      </Switch>
    )
  }
}
constructor(props) {
    super(props);
    this.props.prepareNumber(this.props.match.params.id);
  }

  render() {
    return (
      <div>
        <h1>Static number: {this.props.number} </h1>
      </div>
    )
  }

Server side code

import Express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createStore } from 'redux';
import { App } from './app/App';
import createReducer from './app/reducers/app.redux';
import { StaticRouter } from 'react-router';
import { Provider } from 'react-redux';
const app = Express();
const port = 3000;

app.use(Express.static('public'));

app.use(handleRender);

function handleRender(req, res) {
  let store = createStore(createReducer());
  const context = {};
  renderToString(
    <Provider store={store}>
      <StaticRouter context={context} location={req.url}>
        <App/>
      </StaticRouter>
    </Provider>
  );

  const preloadedState = store.getState();
  store = createStore(createReducer(), preloadedState);

  const html = renderToString(
    <Provider store={store}>
      <StaticRouter context={context} location={req.url}>
        <App/>
      </StaticRouter>
    </Provider>
  );


  res.send(`
    <!doctype html>
    <html>
        <header>
            <title>Simple SSR in React</title>
        </header>
        <body>
           <script>window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}</script>
            <div id='app'>${html}</div>
            <script src='bundle.js'></script>
        </body>
    </html>
`)
}

app.listen(port);

Client side code

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import {App} from './App';
import createReducer from './reducers/app.redux';

const preloadedState = window.__PRELOADED_STATE__;

delete window.__PRELOADED_STATE__;

const store = createStore(createReducer(), preloadedState);

export default class Main extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <BrowserRouter>
          <App/>
        </BrowserRouter>
      </Provider>
    );
  }
}

ReactDOM.render((
  <Main/>
), document.getElementById('app'));

React checksum

Problems?

Fetching data

  • server needs to wait for all API calls if data is needed to render the page
  • it may overload server if there is a lot of API calls for each request
  • use caching when possible
  • only load data that is required to render the page
  • don't load same data twice (don't requst on client if it is already loaded by server)

Different JS environment

  • node.js vs browser
  • some objects are not available on server side (document, window etc.)
  • don't run any code that need browser-specific objects on server
    • measuring window / orientation
    • initializing iScroll etc.
    • checking browser compatibility

React Lifecycle

  • componentWillMount & contructor are run on server
  • anything else is run on client only

Caching strategies

  • cache API calls
  • cache whole server requests
  • cache components instances
    • https://github.com/walmartlabs/react-ssr-optimization

SSR in practice (Apptension Boilerplate)

First things first

We need a NodeJS server

const express = require('express');
const app = express();
.
.
.

// Start your app.
app.listen(port, host, (err) => {
  if (err) {
    return logger.error(err.message);
  }

  // Connect to ngrok in dev mode
  if (ngrok) {
    ngrok.connect(port, (innerErr, url) => {
      if (innerErr) {
        return logger.error(innerErr);
      }

      logger.appStarted(port, prettyHost, url);
    });
  } else {
    logger.appStarted(port, prettyHost);
  }
});

Let's handle a connection

//Somewhere in the server:
const handleSSR = require('./middlewares/handleSSR');
app.get('*', handleSSR);

/* --------- */

const app = express();
app.use(handleSSR);

/* --------- */

module.exports = function handleSSR(req, res) {
  const options = {
    assets,
    webpackDllNames: extractWebpackDllNamesFromPackage(),
    lang: req.acceptsLanguages(appLocales),
  };

  renderAppToStringAtLocation(req.url, options, (response) => {
    if (response.error) {
      res.status(500).send(response.error.message);
      printError(response.error);
    } else if (response.redirectLocation) {
      res.redirect(302, response.redirectLocation);
    } else if (response.notFound) {
      res.status(404).send(response.html);
    } else {
      res.status(200).send(response.html);
    }
  });
};

What do we need to render a react application on backend? (in a nutshell) 

// In a nutshell
import { renderToStaticMarkup } from 'react-dom/server';

const doc = renderToStaticMarkup(
   <HtmlDocument
       appMarkup={appMarkup}
       lang={state.locales.language}
       state={state}
       head={Helmet.rewind()}
       assets={assets}
   />
);

Well... we need a bit more than that

import 'babel-polyfill';

import React from 'react';
import { Provider } from 'react-redux';
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
import { createMemoryHistory, match, RouterContext } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
import Helmet from 'react-helmet';
import configureStore from './modules/store';
import routes from './routes';
import HtmlDocument from './htmlDocument';

import { selectLocationState } from './modules/router/router.selectors';

function renderAppToString(store, renderProps) {
  return renderToString(
    <Provider store={store}>
      <RouterContext {...renderProps} />
    </Provider>
  );
}

function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
  const memoryHistory = createMemoryHistory(url);
  const store = configureStore({}, memoryHistory);

  syncHistoryWithStore(memoryHistory, store, {
    selectLocationState: selectLocationState(),
  });

  match({ routes, location: url }, (error, redirectLocation, renderProps) => {
    if (error) {
      callback({ error });
    } else if (redirectLocation) {
      callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
    } else if (renderProps) {
      store.rootSaga.done.then(() => {
        const state = store.getState().toJS();

        const appMarkup = renderAppToString(store, renderProps);

        const doc = renderToStaticMarkup(
          <HtmlDocument
            appMarkup={appMarkup}
            lang={state.locales.language}
            state={state}
            head={Helmet.rewind()}
            assets={assets}
            webpackDllNames={webpackDllNames}
          />
        );

        const html = `<!DOCTYPE html>\n${doc}`;
        callback({ html });
      }).catch((e) => {
        callback({ e });
      });

      renderAppToString(store, renderProps);
      store.close();
    } else {
      callback({ error: new Error('Unknown error') });
    }
  });
}

export {
  renderAppToStringAtLocation,
};

Let's break this down a bit

React router match

function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {

 match({ routes, location: url }, (error, redirectLocation, renderProps) => {
    if (error) {
      callback({ error });
    } else if (redirectLocation) {
      callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
    } else if (renderProps) {

//Rest of code goes here 

This function is to be used for server-side rendering. It matches a set of routes to a location, without rendering, and calls a callback(error, redirectLocation, renderProps) when it's done.

http://knowbody.github.io/react-router-docs/api/match.html

After we match the route

.
.
} else if (renderProps) {
    //Stuff going on here, will get back in a minute ;)
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
    .
      renderAppToString(store, renderProps);
      store.close();

We call renderApptoString

here

This will render the application, trigger the sagas, make the API calls and so on!

Next, we gonna stop the sagas

store.close = () => store.dispatch(END);

Why do we stop sagas?

After we stop sagas

store.rootSaga = sagaMiddleware.run(rootSaga);
      

    store.rootSaga.done.then(() => {
        const state = store.getState().toJS();

        const appMarkup = renderAppToString(store, renderProps);

        const doc = renderToStaticMarkup(
          <HtmlDocument
            appMarkup={appMarkup}
            lang={state.locales.language}
            state={state}
            head={Helmet.rewind()}
            assets={assets}
            webpackDllNames={webpackDllNames}
          />
        );

        const html = `<!DOCTYPE html>\n${doc}`;
        callback({ html });
      }).catch((e) => {
        callback({ e });
      });

      renderAppToString(store, renderProps);
      store.close();

So, when our sagas finishes

We render the app again with new state

Render to static markup (html)

Respond our rendered html :)

Curious about HtmlDocument?

render() {
    const { lang, head, appMarkup, state, assets, webpackDllNames } = this.props;
    const attrs = head.htmlAttributes.toComponent();

    return (
      <html lang={lang} {...attrs}>

      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />

        <meta name="mobile-web-app-capable" content="yes" />

        {head.title.toComponent()}
        {head.meta.toComponent()}

        <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700" rel="stylesheet" />
        <link href={assets.main.css} rel="stylesheet" />
      </head>

      <body>

      <noscript>
        If you are seeing this message, that means <strong>JavaScript has been disabled on your browser</strong>
        , please <strong>enable JS</strong> to make this app work.
      </noscript>

      <div id="app">
        <div dangerouslySetInnerHTML={{ __html: appMarkup }} />
      </div>

      <script dangerouslySetInnerHTML={{ __html: `APP_STATE = ${htmlescape(state)}` }} />


      {(webpackDllNames || []).map((dllName) =>
        <script data-dll key={dllName} src={`/${dllName}.dll.js`}></script>
      )}

      <script type="text/javascript" src={assets.main.js}></script>

      </body>
      </html>
    );
  }

Webpack

module.exports = {
  name: 'server',
  target: 'web',
  externals: [
    nodeExternals(),
  ],
  entry: [
    path.join(process.cwd(), 'app/server.js'),
  ],
  output: {
    path: outputPath,
    filename: 'server.js',
    publicPath: '/',
    libraryTarget: 'commonjs2',
  },

Webpack - Server side

      new webpack.DefinePlugin({
        'process.env': {
          NODE_ENV: JSON.stringify(process.env.NODE_ENV),
        },
        '__SERVER__': false,
        '__CLIENT__': true,
      }),
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV),
      },
      '__CLIENT__': false,
      '__SERVER__': true,
    }),

Webpack - Frontend side

So you could..

if (__SERVER__) {
    //do server-specific code
}


if (__CLIENT__) {
    //do client-specific code
    //i.e. window.location = 'http://google.com'
}

Problems and Optimizations

We have a problem with routing, when using /en, /pt and so on

App dispatches the redux actions on Backend and Frontend

The code deserves a refactoring

Currently hosted at https://github.com/cpBurn/redux-saga-ssr

SSR -devtalk

By Michał Przyszczypkowski

SSR -devtalk

  • 432