How to Implement Server-Side Rendering in React with Rails Using Puppeteer

Server-side rendering (SSR) is a powerful technique for optimizing the performance and improving the search engine visibility of single-page applications (SPAs). By rendering the initial HTML of an app on the server and sending it as the response to the browser, SSR can dramatically improve metrics like time to first byte (TTFB) and first contentful paint (FCP). This is especially important for users on slow networks or low-powered devices, who may otherwise have to wait several seconds before seeing any content on the page.

SSR is also critical for SEO, since search engine crawlers generally do not execute JavaScript and therefore cannot index content that is only rendered on the client-side. By serving fully-formed HTML on the initial request, SSR ensures that crawlers can properly index and rank your app‘s pages.

In this guide, we‘ll take a deep dive into implementing SSR in a React app with a Rails backend using Puppeteer, Google‘s powerful Node library for controlling a headless Chrome instance. We‘ll cover everything from configuring your Rails app to setting up a Node server for rendering React components, as well as best practices for caching, performance optimization, and testing.

By the end of this guide, you‘ll have a fully functional SSR implementation that can significantly improve the performance and SEO of your React app. Let‘s get started!

Why Puppeteer?

There are several different approaches to implementing SSR in a React app, including using a custom Node server, leveraging frameworks like Next.js or Gatsby, or even using a serverless solution like AWS Lambda. However, using Puppeteer with a headless browser has several advantages:

  1. Flexibility: With Puppeteer, you have full control over the browser environment, including the ability to set custom headers, cookies, and other request parameters. This makes it easy to implement features like authentication and localization.

  2. Fidelity: Since Puppeteer uses a real browser under the hood, you can be confident that the rendered HTML will be identical to what users see in their own browsers. This is especially important for visual testing and debugging.

  3. Performance: Puppeteer is highly optimized and can render pages much faster than a traditional headless browser like PhantomJS. It also supports features like parallelization and request interception for even greater performance.

  4. Ecosystem: Puppeteer has a large and active community, with excellent documentation and a wide range of plugins and extensions available. This makes it easy to find support and extend its functionality to meet your specific needs.

Step 1: Set Up a Rails App with React

Before we can implement SSR, we need a working Rails app with React on the frontend. If you already have an app set up, you can skip ahead to step 2.

First, create a new Rails app and add the necessary dependencies:

rails new ssr-react-app
cd ssr-react-app
yarn add react react-dom
bundle add webpacker react-rails

Next, generate the React installation:

rails webpacker:install
rails webpacker:install:react
rails generate react:install

Create a new controller and view for the app‘s home page:

rails generate controller Home index

Replace the contents of app/views/home/index.html.erb with:

<%= react_component("App") %>

Create a new React component at app/javascript/components/App.js:

import React from ‘react‘;

function App() {
  return (
    <div>

      <p>Edit this component in app/javascript/components/App.js</p>
    </div>
  );
}

export default App;

Start the Rails server with rails s and navigate to http://localhost:3000. You should see your React component rendered in the browser.

Step 2: Set Up Puppeteer

With our Rails app up and running, let‘s set up a Node server to handle the actual server-side rendering. We‘ll use Puppeteer to launch a headless Chrome instance and render our React components.

Create a new directory called ssr in the root of your Rails project:

mkdir ssr
cd ssr
yarn init -y

Install the necessary dependencies:

yarn add puppeteer react react-dom @babel/register @babel/preset-env @babel/preset-react ignore-styles

Create a .babelrc file in the ssr directory to transpile JSX:

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

Now create a new file called ssr.js in the ssr directory:

require("ignore-styles");
require("@babel/register")({
  ignore: [/\/(build|node_modules)\//],
  presets: ["@babel/preset-env", "@babel/preset-react"],
});

const puppeteer = require("puppeteer");
const ReactDOMServer = require("react-dom/server");
const App = require("../app/javascript/components/App").default;

async function ssr(url) {
  const browser = await puppeteer.launch({
    headless: true,
    args: ["--no-sandbox", "--disable-setuid-sandbox"],
  });

  const page = await browser.newPage();
  await page.goto(url, { waitUntil: "networkidle0" });

  await page.evaluate(async () => {
    const html = ReactDOMServer.renderToString(<App />);
    document.querySelector("#root").innerHTML = html;
  });

  const html = await page.content();
  await browser.close();

  return html;
}

module.exports = ssr;

Let‘s break this down:

  1. We require ignore-styles and @babel/register to transpile our JavaScript and ignore CSS imports.
  2. We launch a new headless browser instance with Puppeteer. Note the args option, which is necessary to run Puppeteer in a sandboxed environment like Docker.
  3. We create a new page and navigate to the specified URL, waiting for the network to be idle.
  4. We use page.evaluate to execute code in the context of the page. Here, we render our App component to a string using ReactDOMServer.renderToString and inject it into the DOM.
  5. Finally, we return the full HTML of the page and close the browser.

Step 3: Set Up an API Route in Rails

Now we need a way to trigger the server-side rendering from our Rails app. We‘ll set up an API route that will make a request to the Node server and return the rendered HTML.

Create a new controller called PagesController:

rails generate controller Pages

Replace the contents of app/controllers/pages_controller.rb with:

class PagesController < ApplicationController
  def index
    render html: ssr(request.original_url)
  end

  private

  def ssr(url)
    HTMLParser.new(ssr_html(url)).html.html_safe
  end

  def ssr_html(url)
    Net::HTTP.post(URI("http://localhost:3000/ssr"), { url: url }.to_json, "Content-Type" => "application/json").body
  end
end

Here‘s what‘s happening:

  1. We define an index action that renders the result of calling the ssr method with the current URL.
  2. The ssr method sends a POST request to the Node server at http://localhost:3000/ssr with the URL as a JSON payload.
  3. The Node server returns the server-side rendered HTML, which we parse and mark as safe to render.

Finally, update config/routes.rb to route requests to the new controller:

Rails.application.routes.draw do
  root "pages#index"
  get "*path", to: "pages#index", constraints: { path: /(?!.*\.(png|jpg|jpeg|svg|js|css|json)$).*/ }
end

This will route all requests to the index action of the PagesController, except for requests for static assets like images and CSS files.

Step 4: Render the App

We‘re almost ready to test our SSR implementation! First, let‘s update app/views/layouts/application.html.erb to include the necessary HTML and JavaScript:

<!DOCTYPE html>
<html>
  <head>
    <title>My SSR App</title>
    <%= csrf_meta_tags %>
    <%= stylesheet_pack_tag ‘application‘ %>
  </head>
  <body>
    <div id="root">
      <%= yield %>
    </div>
    <%= javascript_pack_tag ‘application‘ %>
  </body>
</html>

Notice the <div id="root"> element – this is where our server-rendered HTML will be injected.

Next, update app/javascript/packs/application.js to hydrate the React app on the client-side:

import React from ‘react‘;
import ReactDOM from ‘react-dom‘;
import App from ‘../components/App‘;

ReactDOM.hydrate(<App />, document.getElementById(‘root‘));

The ReactDOM.hydrate method is similar to ReactDOM.render, but it expects that the HTML for the app already exists in the DOM and will attach event listeners to the existing markup rather than re-rendering it.

Step 5: Test It Out

We‘re now ready to test our SSR implementation! Start the Rails server in one terminal window:

rails s

And in another window, start the Node server:

node ssr/server.js

Navigate to http://localhost:3000 in your browser. You should see your React app rendered on the server and sent as HTML in the initial response. To verify, view the page source – you should see the server-rendered HTML.

Congratulations! You now have a working SSR implementation in your React app using Rails and Puppeteer.

Performance Considerations

While SSR can significantly improve the performance of your app, there are a few key considerations to keep in mind:

Caching

Rendering pages on the server can be CPU-intensive, especially for complex apps with a lot of dynamic data. To reduce server load and improve response times, it‘s important to cache rendered pages whenever possible.

One approach is to use a caching server like Redis or Memcached to store rendered HTML fragments. When a request comes in, you can check the cache first and only render the page if it‘s not already cached.

Another option is to use a CDN like Cloudflare or Fastly to cache entire pages at the edge. This can significantly reduce the load on your servers and improve performance for users around the world.

Streaming Responses

By default, Puppeteer will wait for the entire page to render before sending the HTML back to the client. This can lead to slower response times, especially for pages with a lot of content.

To improve performance, you can use a technique called streaming SSR. With streaming, the server sends the HTML back to the client in chunks as it becomes available, rather than waiting for the entire page to render.

To implement streaming SSR with Puppeteer, you can use the page.evaluate method to inject rendered chunks into the DOM as they become available. This can significantly improve TTFB and perceived performance for users.

Optimizing Time to First Byte

Time to first byte (TTFB) is a key metric for measuring the performance of SSR apps. It refers to the time between when the server receives a request and when it sends the first byte of the response back to the client.

To optimize TTFB, there are a few key tactics you can use:

  • Minimize the amount of data sent in the initial response. This might involve inlining critical CSS or JavaScript, or splitting your app into smaller chunks that can be loaded on-demand.
  • Use server-side caching to avoid rendering pages from scratch on every request.
  • Optimize your server‘s response times by minimizing the amount of work done in the request-response cycle. This might involve optimizing database queries, minimizing the number of API calls, or using a faster web server like Nginx.

By focusing on these key areas, you can significantly improve the performance of your SSR app and deliver a better experience to your users.

Conclusion

Server-side rendering is a powerful technique for improving the performance and SEO of React applications. By rendering the initial HTML on the server and sending it to the client, SSR can dramatically improve metrics like time to first byte and first contentful paint, especially for users on slower networks or devices.

In this guide, we‘ve seen how to implement SSR in a React app with a Rails backend using Puppeteer. We‘ve also explored some of the key performance considerations and optimization techniques for SSR apps.

While SSR does add some complexity to your application, the benefits in terms of performance and search engine visibility can be significant. By following the techniques outlined in this guide, you can build fast, SEO-friendly React apps that deliver a great experience to your users.

So what are you waiting for? Get started with SSR today and take your React apps to the next level!

Similar Posts