“The Fetch as Google tool enables you to test how Google crawls or renders a URL on your site. You can use Fetch as Google to see whether Googlebot can access a page on your site, how it renders the page, and whether any page resources (such as images or scripts) are blocked to Googlebot (via robots.txt.) This tool simulates a crawl and render execution as done in Google’s normal crawling and rendering process, and is useful for debugging crawl issues on your site.”
The process I’m suggesting here is before going to solution mode we must first see how Google is crawling the client-rendered SPA (no isomorphism or server side templating, just a pure client app) and if Google is failing to crawl it (Google has been investing in their ability to crawl SPAs since 2009 and by 2014 they announced that they were able to crawl pretty much any SPA) then we would use the Headless Chrome approach.
First I want to clarify that this post is not about Static (build time) Server Side Rendering (SSR) where server is an exclusive resource, as promoted by frameworks like Gatsby.JS. It is about Dynamic (runtime) SSR where ‘server’ is a shared resource, as promoted by frameworks like Next.JS (but excluding the combination of Next.JS and Electron where the server is once again an exclusive resource.)
Let’s start with my simple use case. I have a React app that I wish to expose to search bots (for SEO) and speed up it’s initial render. I don’t believe in using SSR because it negates the scalability we get with Single Page Apps (SPAs) where we do all the CPU-bound work of rendering the page on the client as opposed to doing it on a shared server, where the most common scenario involves NodeJS as the server, a single-threaded, cooperative multitasking environment that is designed for I/O-bound work.
One of the alternatives I‘ve considered is as follows:
Upon receiving a request for a given URL, the server loads the index.html file into a string and inject into the data for initial requested route (which is must be set (by user and SPA) in URL params since hash fragment is not forwarded by the browser, and including any query params) as follows:
This way when the SPA is loaded by the browser the initial route will already have the data it needs, and will start rendering on the client without having to wait for the data, so the initial render will feel fast since the browser won’t open a blank page then wait then render. Instead, it will open a blank page then immediately render all content at once, so the user won’t even see the blank page that gets assembled in piecemeal fashion. After the initial requested route is served, the SPA takes over and can make sure that subsequent routes are rendered only after their data dependencies are fetched, thus achieving the same effect.
As to how we would pre-fetch the data on the server, we would use GraphQL as the data layer which allows us to use the same GraphQL schema and resolvers to execute a single query per route, without having to build imperative reducer logic per route just for data preloading.
When it comes to SEO (see Update to this post at the top) the best approach I’ve seen is by Sam Li of the Google Chrome team. Sam’s approach is to detect the user-agent and if it’s a bot then queue the request to a pool of Headless Chrome instances that will load the SPA like any user browser would, run it and return the HTML. The advantage is that my React app setup remains super simple and does not carry any SSR baggage.
Another (additional) way to speed up initial render:
As far as React application bloat problem where developers pile up loads of NPM modules and do not use dead code elimination or tree shaking (learn the difference between the two), one solution, as Kayle Mathews, creator of GatsbyJS, suggested is to use the caching feature in Service Workers and code splitting (since as of the time of this writing, we still don’t have fully working dynamic imports) to cache all vendor modules that our code depends on and load them separately so that we’re not downloading megabytes of dependencies.
Yet another way to speed up initial page render (in ES proposal stage, so it may or may not come to fruition) is this proposal for over-the-wire format for JS, as binary encoding of the abstract syntax tree:
Create your free account to unlock your custom reading experience.