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:
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:
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:
- Cache all the public users and crawlers facing routes that make sense.
- Make sure you serve zero static assets from your node server; including your JavaScript.
- Optimize your pictures.
- Donāt use an external CSS file.
- Take advantage on Meteor 1.5 to split all your code.
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:
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:
- me: This is a really good framework; I will work much faster using it than backbones.
- him: Do you have benchmarks, use cases and are there real world big applications to show?
- me: Benchmarks yes. Prod application, not that much. But this is the google framework for the web!
- him: Oh nice! Is google made with Angular?
- me: Wellā¦ no.
- him: Youtube? Gmail? Google maps? ā¦
- ā¦
- Ok, backbone it is.
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