Server Side Render SPAs with Puppeteer

By Yongzhi Huang

In this tutorial, I would like to demonstrate how we can use Puppeteer to render contents generated by client side script.

The problem almost every SPA faces is that contents are generated from client side, this means the data (such as list of names in a table) are generated the following way:

1. Page loads, client side javascript is executed

2. The script goes to fetch the data from elsewhere

3. Data is then inserted into a container div

If you view page source on a typical single page app, you will not see any of that content within the div because they were loaded from elsewhere during client side execution, this is bad for SEO mainly becaue crawlers  cannot see what’s on your page unless they execute the scripts themselves.  Even then, the rendering engine google uses for its crawler is many versions behind the latest release of Chrome, so it’s not guaranteed that the crawler sees your page the way you intended.

Puppeteer is a project created by the Chrome Dev Team that enabled you to access Chrome headlessly.  This is extremely powerful, because you can now programmatically control chrome via code, and perform tasks such as crawling, screenshots, end to end tests and other automations all from one single tool.

Let’s get started, for this tutorial, we’re going to create a very simple express app with express generator:

npm i -g express-generator

this will place a global express command in your terminal.  Next we can create a simple starter project with

express ssrspaPuppeteer

This will generate a boilerplate project with express, we can then finish the project set up process with

cd ssrspaPuppeteer; npm i; npm start

The server should now be running on localhost:3000

Next we need to add a static HTML file to the public folder, let’s call this comments.html

<html> <body> <ul class="comments-list"> </ul> </body> <script> (async () => { const getComments = () => { return fetch('') .then(response => response.json()); } function renderComments(comments, container) { let commentsListHTML = ''; comments.forEach(comment => commentsListHTML += `<li><b>${}</b>: ${comment.body})</li>`); container.innerHTML = commentsListHTML; } const comments = await getComments(); const container = document.querySelector('.comments-list'); renderComments(comments, container); })(); </script> </html>

This client side script does two things: first, it fetches a list of comments from our dummy api from jsonplaceholder, next it pieces together each comment to a single string and then it inserts the string into the body of .comments-list.  If async await or ES6 is unfamiliar to you, I have some tutorials on the site about them.

If we go to http://localhost:3000/comments.html we should see a simple page with about 200 comments on it.  To see the problem with client side rendered page, we can view source on the page, and you’ll notice that the content within .comments-list is empty.  Don’t worry, we’re going to fix that with Puppeteer.

Install the Puppeteer library with

Create a helper file call ssr.js in the root path of your express project:

var puppeteer = require('puppeteer'); async function ssr(url) {'rendering the page in ssr mode'); const browser = await puppeteer.launch(); const page = await browser.newPage(); try { await page.goto(url, {waitUntil: 'networkidle0'}); await page.waitForSelector('.comments-list'); } catch (err) { console.error(err); throw new Error('page.goto/waitForSelector timed out.'); } const html = await page.content(); await browser.close(); return {html}; } module.exports = ssr;

This script initiates Puppeteer, tells it to visit the url specified, and have the browser wait for the existence of particular DOM element (.comments-list) and return the html representation of the content of the page.

the last thing we need to do is to bring this helper into express.  Open app.js and include ssr.js

var ssr = require('./ssr.js'); 

then create a handler for the route /comments

app.use('/comments', async(req, res) => { const { html } = await ssr(`${req.protocol}://${req.get('host')}/comments.html`); return res.status(200).send(html); });

This essentially proxies /comments to /comments.html by running the content of comments.html (after client side javascript execution) through puppeteer, and returns the content and renders to the user.

Head over to http://localhost/comments and you should see the same page you saw before, but the difference is when you view the page source, you’ll notice each <li> within .comments-list are now present.

Of course there are a few optimizations you can do here, such as caching the result so you don’t have to run through puppeteer every page load.  I challenge you to implement that yourself.  I highly recommend the documentation page for Puppeteer on google’s web dev central.  I hope this tutorial was useful to help you speed up your websites.