🔥 Make your meteor site load FAST!

in JavaScript, Meteor

Meteor does not shine when it is serving static pages. And often, you switch to another solution. Check out their documentation , it is generated with hexo . Hexo is great, good choice. But when you are building “the fastest way to build JavaScript apps” with an amazing build tool and that you end up using another generator to build your documentation, there is room for improvement !

Of course, most of what we build with Meteor are single page applications behind a login wall. And most of the time, SEO is not a priority. But there is also often a static part in the things we ship. And we need to make the first full paint happen as fast as we can for users and crawlers. I’m going to explain you what I did for a particular app and why. But this is not a tutorial. Your app has specifc issues and you need to figure them out or to find someone who will.

📈 Why even bother?

The basic internet user is now used to web applications like Gmail. That kind of app can take up to a full minute to load, we don’t care. It will stay open all day in a tab of our browser and we are going to use it that way. But your product exists also outside of your app. And you want it fast for two reasons: the users and the search engines.

Let me show you the main reason why you should bother. What happened in the search console the day I made a speed update to a Meteor app:

Google console : big improvement

Yes, that’s it. One single update and the organic trafic went up to more than 300%; and so did the impressions. And that single update was doing only ONE thing: make the site load faster. I cannot promise that you are going to get the same results. Mainly because your situation is probably less crappy that this application situation was. But I’m confident that you should see a significant improvement. Go check yours. You have at least an FAQ page and a few landings. You want them to appear on the search engines pretty high.

The second reason is the user:

google analytics : mobile users share is 30%

Roughly a third of that website users are using a mobile device. And most of them are on mobile networks. Since the speed update, there is finally a match between the number of ads clicked and the number of visitors from those clicks and the overall CPC dropped. It means that a significant amount of mobile users (I’d say 25%) used to leave the page before it had a chance to load. That was sad.

To do that, you will need to do a few things to reduce the wait time for the first paint:

Making things faster

💾 Cache all the things!

The application I was just writing about is divided in two parts. A pure front / website for end client and the actual application hidden behind a login wall. The application part is a CRM for the company clients and in-house salespersons. The website is there to acquire end clients for the company clients and the app is there to manage and invoice them. SEO matters only for the website part.

The app is built with React and FlowRouter. Initially the server used to render the pages on the fly with Flow Router SSR and serve them with a one hour cache. The company clients can (and do) update their profiles and products all the time so the architecture needs to stay dynamic because the website for the endclient is.

You can do whatever you want, React Rendering server side sucks. When I render a full page on a brand new MacBook pro, it takes up to 300 ms for heavy content pages. This is way too long. On a galaxy “quad” container, this is even longer. So rendering on the fly is not an option. For the same reason, prerender.io is not that good for SEO. When google ask it a page for the first time, it needs to fully load and render it before serving it. So, the speed score for the first load is a disaster and if this is a deep page, the next time google comes, prerender will need to update its cache, so, same disaster. Plus it does nothing for the users.

If you think you can render on the fly and cache for the next use, you are forgetting how google bots work. They are crawling your website continually. So, 99% of the pages it visits are not going to be cached. Let me show you what happened with my update in the search console:

google console: load time drop

Of course, the homepage was always in the cache of FlowRouter SSR, but the average load time of all pages was stupid high. After the update, it dropped from 1200ms to 180ms. This is actually what triggered the SEO up ranking.

So yes, pre-rendering everything, caching the result and serving from that cache is the best option.

I started to write a tutorial, but it was too long, so here is a live code to show a way it can be done. This is not the absolute and only way. The code is available on that git repo and here is the video:

Bonus point: if you pre-render that way, you can check you data integrity and that all your pages render like they should.

You can notice that there is a glitch when the first page loads: the loader view is mounted then data are called, then final view is mounted => so the initial view is rendered twice, server and client. Which makes no sense. In a real-world application, you have a general method for the client route action; and on the first load, you do not re-render.

PS: I do not know how people do it, but I cannot talk and code at the same time, so I coded, recorded, accelerated time 2 and commented over the video 🤓.

Serving all static assets from the outside world

Node suck at serving static assets. But you do not care, you will serve them from elsewhere.

The first thing to do is to simply remove your public folder. Just do it.

Even the favicon; do not forget them. If you did not define a link for it, the browsers are going to ask for one anyway. This is not much, but you can save your server one request for each visitor. Here is how the code in your header may look like:

`<link rel='apple-touch-icon' sizes='57x57' href='${ cdnPath }/favicons/apple-touch-icon-57x57.png'>` +
`<link rel='apple-touch-icon' sizes='114x114' href='${ cdnPath }/favicons/apple-touch-icon-114x114.png'>` +
`<link rel='apple-touch-icon' sizes='72x72' href='${ cdnPath }/favicons/apple-touch-icon-72x72.png'>` +
`<link rel='apple-touch-icon' sizes='144x144' href='${ cdnPath }/favicons/apple-touch-icon-144x144.png'>` +
`<link rel='apple-touch-icon' sizes='60x60' href='${ cdnPath }/favicons/apple-touch-icon-60x60.png'>` +
`<link rel='apple-touch-icon' sizes='120x120' href='${ cdnPath }/favicons/apple-touch-icon-120x120.png'>` +
`<link rel='apple-touch-icon' sizes='76x76' href='${ cdnPath }/favicons/apple-touch-icon-76x76.png'>` +
`<link rel='apple-touch-icon' sizes='152x152' href='${ cdnPath }/favicons/apple-touch-icon-152x152.png'>` +
`<link rel='apple-touch-icon' sizes='180x180' href='${ cdnPath }/favicons/apple-touch-icon-180x180.png'>` +
`<link rel='icon' type='image/png' href='${ cdnPath }/favicons/favicon-192x192.png' sizes='192x192'>` +
`<link rel='icon' type='image/png' href='${ cdnPath }/favicons/favicon-160x160.png' sizes='160x160'>` +
`<link rel='icon' type='image/png' href='${ cdnPath }/favicons/favicon-96x96.png' sizes='96x96'>` +
`<link rel='icon' type='image/png' href='${ cdnPath }/favicons/favicon-16x16.png' sizes='16x16'>` +
`<link rel='icon' type='image/png' href='${ cdnPath }/favicons/favicon-32x32.png' sizes='32x32'>` +

Use a CDN for the JavaScript. This is actually easy to do and a big improvement. Simply setup a CDN distribution (I use AWS cloudfront, but it will work with any decent CDN) and then in your meteor settings, add the cdnPrefix like that:

{
    …
    "ENV": "PROD",
    "cdnPrefix": "https://xxxxdistributionaddressxxxx.cloudfront.net",
    "public": {
        …
}

Optimizing the pictures

Re-optimize everything! Use offline software like ImageOptim for macOs, FileOptimizer for Windows, and trimage for Linux before you upload your images. For user generated content, resize client side with the canvas before uploading and then optimize server-side with ImageMagick . If you use Galaxy to deploy your Meteor app, the binary is already included.

Of course, put everything on a CDN. I usually use AWS S3 for the storage and CloudFront for the delivery. But I’ve recently tried Google and CloudFlare and they are really good too. Do not forget the cache expire: CacheControl: 'max-age=31536000'.

Removing external CSS

On mobile network the latency is often high. Your external CSS may be super light and properly CDN served, it still requires and complete back and forth. And it delays the first paint.

You are going to have a lot of users within the 100-500 ms latency range . If the user already waited a full second for your page, don’t make him wait 25 ot 50 % time more!

My take on that one is that I have a short general / reset CSS embeded at the top of my html file and all the other styles are inlined with Radium or put into small scoped style tags with styled jsx .

Going 1.5 for “code splitting” (dynamic imports)

Your user is going to have to load all your bundle on the first page. Even if he won’t use most of it. Meteor 1.5 introduced a clever way to avoid that: dynamic imports. It does not work at all like webpack code splitting under the hood (it actually sends the code over the DDP) but it will look like it is the same.

It is super easy to setup. From the code in the video above, just take your client route:

import React from 'react';
import {
    mount
} from 'react-mounter';
import PostView from './post_view.js';
import Loader from '/imports/components/loader.js';

FlowRouter.route(
    '/post/:postID', {
        name: 'Post',
        action( param ) {

            mount( Loader );

            return Meteor.call(
                'getPostData',
                param.postID,
                ( err, postData ) => mount(
                    PostView, {
                        ...postData
                    }
                )
            )

        }
    }
);

Remove the unconditional import for the PostView, and import it in the action:

import React from 'react';
import {
    mount
} from 'react-mounter';
import Loader from '/imports/components/loader.js';

FlowRouter.route(
    '/post/:postID', {
        name: 'Post',
        action( param ) {

            mount( Loader );

            import ( './post_view.js' ).then(
                PostView => Meteor.call(
                    'getPostData',
                    param.postID,
                    ( err, postData ) => mount(
                        PostView, {
                            ...postData
                        }
                    )
                )
            );

        }
    }
);

If the module have already been imported, the promise will resolve instantly; otherwise it will load the code and cache it before going further.

Since the entire page is already rendered, the user won’t see the difference.

Even faster: meteor static site generator

Nothing is faster than a static site served over a CDN. Always been, always will be. No shit Sherlock, when the server has nothing to do, it does it faster than when it has something to do!

As written in the introduction, the MDG uses Hexo to generate their documentation. And I think they should not for two reasons:

We need to introduce more developers to the Meteor community. Things like the user accounts packages are awesome because we can say: “Hey look, two lines of code and it works!”. It is a lie, but a white one; and people buy it. Being able to say: “Hey look, two lines of code and you made your static blog!”; or “Hey look, two lines of code and you are ready to write a clean doc for your project!”. Would be awesome because once you have a foot in the door, you can start selling the rest of it.

MDG made an awesome build tool and they do not use it! That is a huge downside. I remember a few years ago, I was trying to sell Angular to a CTO. And it went like this:

The fact that Google did not use Angular was a deal breaker. The NOT dogfooding IS a red flag. So, I think we should have things like packages to generate static websites and the MDG should use them. 99% of the work is already done with the build tool.

This is something I have wanted to do for a long time. As soon as I find a client that needs static stuff or a CMS, I’ll build it for real. In the meantime, as previously, let’s make a live code on how to build a static generator with Meteor! This will be just a proof of concept of course. The important things for adoption are a good documentation, a few good templates and well-designed CLI and tutorial.

The code is here : https://github.com/fabien-h/static