loading...
Cover image for Server Side Render on React / Express

Server Side Render on React / Express

gbarreradev profile image Gonzalo Barrera Updated on ・6 min read

Server Side Render, tal como su nombre lo dice, es poder renderizar nuestra app en el Servidor, en vez de cliente, que quiere decir esto?

En el renderizado en el cliente, el servidor envía la respuesta hacia el browser como un documento HTML, y es este el que se encarga de descargar los archivos Javascript y luego (mediante React por ejemplo) renderizar la aplicación, luego de esto el sitio esta disponible para ser visualizado y disponible para interactuar con el.

En el renderizado en el Servidor, es este quien se encarga de compilar la data y devolver al browser el HTML listo para ser visualizado, para posteriormente descargar sus componentes JS y disponibilizar sus interacciones

Ventajas del uso de Server Side Render:

Time to First Byte (TTFB)

La performance de nuestro sitio web, se ve incrementada, ya que el browser recibe directamente el DOM estructurado, y evitar realizar nuevos ciclos para obtener información faltante, por lo que tenemos un TTFB mas inmediato.

SEO (Search Engine Optimization)

En tecnologías como React, Vue o Angular la página se construye del lado del cliente, lo cual es un lio para los Crawlers (Los robots que leen las páginas y entregan la información a los buscadores), lo cual no les permite identificar los contenidos correctamente.

Mediante SSR esto no ocurre, ya que nuestro DOM ya viene construido desde el servidor, y los Crawlers no tienen problemas para leerlo.

En la práctica

Los archivos para poder ejecutar estos ejemplos se encuentran en mi repositorio de Github
https://github.com/gbarreradev/ssr-example

Lo primero que vamos a hacer, es crear una app en React, para esto vamos a hacer uso de la herramienta de ejecución de paquetes que incluye npm llamada npx

npx create-react-app my-app

Esto nos generará una aplicación básica en React, con la cual podemos trabajar cualquier aplicación frontend.

* En este ejemplo de eliminaron algunos archivos del demo generado, para poder tener un área de trabajo mas limpia

hydrate()

El primer paso es reemplazar el método render() de nuestra aplicación, por hydrate(), el cual nos permite tomar un contenedor HTML renderizado por ReactDOMServer y agregar sus eventos (event listeners).

Entonces nuestro src/index.js quedaría de la siguiente forma:


ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

Para poder levantar nuestro servidor, necesitamos contar con una librería que nos entregue métodos HTTP y middleware, para esto instalaremos el web framework express que corre sobre Nodejs

npm install express

Además necesitamos poder transpilar nuestro código JSX y omitir los estilos en el ambiente de servidor (Node)

npm install @babel/register @babel/preset-env @babel/preset-react ignore-styles

Ahora podemos estructurar nuestro servidor, para esto crearemos dos archivos nuevos "index.js" y "server.js" dentro de una carpeta "server"

En nuestro server.js vamos a crear nuestro servidor (revisar los comentarios entre código)

const path=require('path');
const fs=require('fs');
const express=require('express');
const React=require('react');
const ReactDOMServer=require('react-dom/server');
// Importamos nuestra App
import App from '../src/App'

// Al usar express, ya tenemos listo el ambiente de Node
// Solo debemos setear nuestro puerto, ejecutar express()
// y usar el router para decirle a la App que escuche el puerto

const PORT = 8080
const app = express()
const router = express.Router()

// Creamos un middleware que nos permitirá crear un archivo estático
// HTML dentro de nuestro build para servir ese archivo mediante
// nuestra aplicación express.

const serverRenderer = (req, res, next) => {
  // Leemos nuestro archivo generado en el build
  fs.readFile(path.resolve('./build/index.html'), 'utf8', (err, data) => {
    if (err) {
      console.error(err)
      return res.status(500).send('An error occurred')
    }
    // Enviamos de respuesta un nuevo archivo
    // Reemplazamos el contenido de nuestro div root
    // invocando al método renderToString que nos permite
    // devolver el HTML en su estado inicial como cadena de texto
    return res.send(
      data.replace(
        '<div id="root"></div>',
        `<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
      )
    )
  })
}
// creamos una ruta para que toda la aplicación pase por este middleware
router.use('^/$', serverRenderer)

// le decimos a express que use el middleware static
// para permitirnos servir los archivos estaticos
router.use(
  express.static(path.resolve(__dirname, '..', 'build'))
)

// le informamos a nuestra app que use nuestro router
app.use(router)

// Levantamos nuestro servidor en el puerto solicitado
app.listen(PORT, () => {
  console.log(`SSR running on port ${PORT}`)
})

El punto de entrada a nuestra aplicación será nuestro "server/index.js" al cual debemos agregarle un par de configuraciones faltantes, para la correcta transpilación de babel y la exclusión de archivos de estilos

require('ignore-styles')

require('@babel/register')({
  ignore: [/(node_modules)/],
  presets: ['@babel/preset-env', '@babel/preset-react']
})

require('./server');

Ahora solo nos queda hacer el primer build para generar nuestros archivos.

npm run build

Luego de esto tendremos dos versiones de nuestra aplicación

npm run start  // Client side render

la cual podemos ver en http://localhost:3000/ y si vemos el código fuente, veremos algo como esto

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    
    <title>SSR Example</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  <script src="/static/js/bundle.js"></script><script src="/static/js/0.chunk.js"></script><script src="/static/js/main.chunk.js"></script></body>
</html>

Lo cual claramente no tiene una estructura valida para que un crawler pueda leer, pero ahora, si ejecutamos nuestro server

node server/index.js

Y vemos nuestro sitio, pero ahora en el puerto 8080 http://localhost:8080/ y nuevamente inspeccionamos el código fuente, ahora veremos algo así

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width,initial-scale=1"/>
        <title>SSR Example</title>
        <link href="/static/css/main.a567c94e.chunk.css" rel="stylesheet">
    </head>
    <body>
        <noscript>You need to enable JavaScript to run this app.</noscript>
        <div id="root">
            <div class="App" data-reactroot="">
                <p>Lorem ipsum dolor sit amet consectetur adipiscing elit cursus, nulla donec viverra duis nunc orci netus, placerat fringilla ridiculus ligula sollicitudin nostra</p>
                <img src="https://picsum.photos/seed/picsum/500/300"/>
                <div>
                    <br/>
                    <button>Click! (
                    <!-- -->
                    0
                    <!-- -->
                    times)</button>
                </div>
            </div>
        </div>
        <script src="/static/js/2.ce3a4288.chunk.js"></script>
        <script src="/static/js/main.917b27f0.chunk.js"></script>
    </body>
</html>

Esto tiene más sentido verdad?, y para validar que los eventos también funcionan, prueben revisando el comportamiento del botón!

Para facilitar la compilación y ejecución del server, les recomiendo agregar un run-script al package-json (server)

"scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "server": "npm run build && node server/index.js"
  },

Discussion

pic
Editor guide