\n\n","SSR function","Next, take the ","ssr()"," function from earlier and beef it up a bit:","ssr.mjs","import puppeteer from 'puppeteer';\n\n// In-memory cache of rendered pages. Note: this will be cleared whenever the\n// server process stops. If you need true persistence, use something like\n// Google Cloud Storage (https://firebase.google.com/docs/storage/web/start).\nconst RENDER_CACHE = new Map();\n\nasync function ssr(url) {\n if (RENDER_CACHE.has(url)) {\n return {html: RENDER_CACHE.get(url), ttRenderMs: 0};\n }\n\n const start = Date.now();\n\n const browser = await puppeteer.launch();\n const page = await browser.newPage();\n try {\n // networkidle0 waits for the network to be idle (no requests for 500ms).\n // The page's JS has likely produced markup by this point, but wait longer\n // if your site lazy loads, etc.\n await page.goto(url, {waitUntil: 'networkidle0'});\n await page.waitForSelector('#posts'); // ensure #posts exists in the DOM.\n } catch (err) {\n console.error(err);\n throw new Error('page.goto/waitForSelector timed out.');\n }\n\n const html = await page.content(); // serialized HTML of page DOM.\n await browser.close();\n\n const ttRenderMs = Date.now() - start;\n console.info(`Headless rendered page in: ${ttRenderMs}ms`);\n\n RENDER_CACHE.set(url, html); // cache rendered page.\n\n return {html, ttRenderMs};\n}\n\nexport {ssr as default};\n","The major changes:","Added caching. Caching the rendered HTML is the biggest win to speed up the\nresponse times. When the page gets re-requested, you avoid running headless Chrome\naltogether. I discuss other optimizations later on.","Add basic error handling if loading the page times out.","Add a call to ","page.waitForSelector('#posts')",". This ensures that the posts\nexist in the DOM before we dump the serialized page.","Add science. Log how long headless takes to render the page and return the\nrendering time along with the HTML.","Stick the code in a module named ",".","Example web server","Finally, here's the small express server that brings it all together. The main\nhandler prerenders the URL ","http://localhost/index.html"," (the homepage)\nand serves the result as its response. Users immediately see posts when\nthey hit the page because the static markup is now part of the response.","server.mjs","import express from 'express';\nimport ssr from './ssr.mjs';\n\nconst app = express();\n\napp.get('/', async (req, res, next) => {\n const {html, ttRenderMs} = await ssr(`${req.protocol}://${req.get('host')}/index.html`);\n // Add Server-Timing! See https://w3c.github.io/server-timing/.\n res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc=\"Headless render time (ms)\"`);\n return res.status(200).send(html); // Serve prerendered page as response.\n});\n\napp.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));\n","To run this example, install the dependencies (","npm i --save puppeteer express",")\nand run the server using Node 8.5.0+ and the "," flag:\n","Here's what an example of the response sent back by this server:","\n\n
\n \n
\n\n\n\n","A perfect use case for the new Server-Timing API","The Server-Timing API communicates\nserver performance metrics (such as request and response times or database\nlookups) back to the browser. Client code can use this information to track\noverall performance of a web app.","A perfect use case for Server-Timing is to report how long it takes for headless\nChrome to prerender a page. To do that, just add the ","Server-Timing"," header to\nthe server response:","res.set('Server-Timing', `Prerender;dur=1000;desc=\"Headless render time (ms)\"`);\n","On the client, the Performance API\nand PerformanceObserver\ncan be used to access these metrics:","const entry = performance.getEntriesByType('navigation').find(\n e => e.name === location.href);\nconsole.log(entry.serverTiming[0].toJSON());\n\n{\n \"name\": \"Prerender\",\n \"duration\": 3808,\n \"description\": \"Headless render time (ms)\"\n}\n","Performance results","The following results incorporate most of the performance\noptimizations discussed later.","On an example app, headless\nChrome takes about one second to render the page on the server. Once the page is\ncached, DevTools 3G Slow emulation puts\nFCP at\n8.37s faster than the client-side version.","First Paint (FP)","First Contentful Paint (FCP)","Client-side app","4s"," 11s","SSR version","2.3s","~2.3s","These results are promising. Users see meaningful content much quicker because\nthe server-side rendered page no longer relies on JavaScript to load + shows\nposts.","Prevent re-hydration","Remember when I said \"we didn't make any code changes to the\nclient-side app\"? That was a lie.","Our Express app takes a request, uses Puppeteer to load the page into\nheadless, and serves the result as a response. But this setup has a\nproblem.","The same JavaScript that executes in headless Chrome on the server runs again\nwhen the user's browser loads the page on the frontend. We have two places\ngenerating markup. #doublerender!\n","To fix this, tell the page its HTML is already in place.\nOne solution is to have the page JavaScript check if ","