Google Page Speed 100/100 with React

Published by Dev Kordeš on 12/22/2017

How I reached 100/100 Google Page Speed score for this website with React/React-redux/Radium and server side rendering.

Google Page Speed 100/100

Here , check it out.

With every project I made, whether it was at work or a side project I've always been obsessed with getting page speed as high as possible. But getting over 80 seemed good and getting well over 90 was mostly impossible for various external reasons. This goes especially for projects at work.

But this blog is different. It's a learning project and I intentionally keep it simple. This way it's easy to completely rewrite parts of it. I started with Laravel for backend and Vue for frontend, just to completely rewrite, first backend to Go and then later frontend to React.

Libraries

Notable libraries:

Preprocessing

Lately I've defaulted to using Laravel Mix as my choice of preprocessing setup. Out of the box it really does work for majority of simple use cases. And extending it for most of my needs is easy as well.

This is basically all I need in webpack.mix.js file for the project.

      let mix = require('laravel-mix');
mix
   // To get around some problems I had once
  .setPublicPath('./')
  // Copies static resources
  .copy('src/static', './public') 
  // Compiles ES6 and jsx to vanilla
  .react('src/js/index.js', 'public/js/app.js'); 
    

And then I just run npm run production --production on server to compile my assets and minify my javascript.

Radium and inline styles

Since I was still very new to React I had to google how to properly do css in React. I decided on using Radium after reading this article on survive.js which gave it some praise.

And while I didn't like the idea of writing css in javascript at first, I quickly grew to love it. Without learning another language I can now use sass on steroids for writing css :)

Being able to do this is pretty nice:

      import {transition} from './vars';
export const li = function(selected = false) {
  const borderColor = selected ? color2 : color3;
  return {
    ...transition,
    'borderBottom': `1px solid ${borderColor}`,
  };
};
    

92/100 Mobile 95/100 Desktop

By using basic best practices and inlining styles I got to a nice 90+ score. Google only had 3 more problems with my site. Cache and Gzip which are really easy to take care of with Apache mods. More on setting those up by Elliotec . And...

Prioritize visible content

The forever pain for SPA frameworks, where you're fetching data from external sources. The only way to satisfy Prioritize Visible Content with SPA frameworks that rely on data from some API is to preload that data along with the first request - and render it on the server.

I didn't have a reason to even think about SSR. Sure the stack was a little messy, where Go handled both API routes and static content. But I was ok with that. And sure it would help with SEO. But I don't care for getting on any search page results with my blog, I don't even have a sitemap yet.

I didn't have any reason to think about SSR until Prioritize visible content was my last enemy.

Server side rendering

Change of stack

So now I had to introduce a new friend to my Go/MySQL/React stack. Say hi to Node js.

This of course caused a chain reaction of mess. First I had to separate API and static content. Go now only takes care of REST API on subdomain api.myprogramming.blog. And Node js takes care of static content, and HTML rendering.

Which brought problems with CORS. And since I wasn't automatically redirecting from http to https and Allow-Origin header only allows 1 parameter I spent quite some time debugging why the blog "randomly" just wouldn't work. It wasn't so much random as it was visiting http or https version. Former didn't work, latter did. But after I added auto redirect to https all was fine.

Library and js differences for server

Radium

Radium was simple. You wrap everything in StyleRoot component and pass it radiumConfig={{userAgent: req.headers['user-agent']}} parameter.

React redux

React redux was a little harder. In order to preload the store on the server, I implemented a static method static fetchData() on routes that needed to fetch some data from APIs. And simply checked if the matched route (React component) has fetchData method. If it does, execute it and wait for it to get results before rendering the app. This tutorial on codemancers helped a lot.

Then I inserted __PRELOADED_STATE__ as described in server rendering documentation into HTML. Then I had to create 2 separate stores, one for server, one for client, where the client one preloaded the state from the HTML, while the the server one didnt.

React router

This one proved to be quite more problematic than I assumed. Mostly because I cut some shortcuts and had a pretty big Layout component, routes included. It took me a while to realize that having an array you can iterate over as an export in a separate file is really good. It cleared the logic from Layout and it made it possible to find the matched route on server with a simple .find(matchRouteLogic). It also made including same routes with BrowserRouter on client and StaticRouter on server much more elegant.

Quick hacks

Then I had to find where I used globals like window and document in the app and implemented a quick hack around them with a simple if if (typeof window === 'undefined') return;

100/100 ... almost

2 weeks after I had 92 & 95 I finally managed to get server side rendering (SSR) to a decent state (admin part is to some degree still riddled with bugs) and installed the Apache mods and finally got that perfect 100/100! I kept checking the score a couple times a day just to be sure.

Screw you, Google

About 2 weeks ago I finally got around to adding Google Analytics as well. No problems there. Except that the score dropped to 99/100 due to a mysterious new resource not having enough cache time.

Well guess what. This is no longer about performance. It's about pride.

      if (navigator.userAgent.indexOf('Speed Insights') == -1) {
  ReactGA.initialize(client.analytics);
}
    

Shoutouts

Disclaimer

Not perfect

I didn't bother with optimizing previously uploaded images. There's one blog post with images that I didn't go back and optimize yet, so it's the only page with lower score.

Code

Source on github . This is the whole project with Go API included.

This website uses  Google Analytics  cookies. Beware.