You write your unique new Meteor Application, but it takes time to load at first. What can you do? This issue has many causes, but it probably means you are loading too much or waiting for too long.
We will go through some basic optimizations here that, when applied, can get you a massive lift in first-page loading time and app performance.
But how can we optimize something that we can’t measure? No way. Let’s start by seeing ways to measure our performance and bottlenecks, and then we will proceed to optimize things.
Know your bottleneck
There are several tools for measuring performance, but there are some that I find essential when analyzing first load performance.
1- Lighthouse
Chrome’s feature for analyzing app performance is a massive ally in getting data. If you run it on your production website, or at least on a minified environment, most commonly in your staging env, you will have easily measurable numbers for performance.
If your new testing release performs worse than the current version in production, you may have to stop and optimize and analyze the latest PRs.
Golden tip: It’s also an excellent choice to create regression tests using lighthouse node API in your CI.
Lighthouse for Meteor Docs
2- Chrome Profiler
We know the app is slow, but why is it slow? You can check which javascript methods are taking more time with chrome profile. With this, you know if it’s your local DB, your UI, a startup script, or whatever is taking that much time. The most common culprit is that you are loading TOO MUCH CODE, and it’s incredible how slow things get when your bundle grows.
Chrome javascript profiler
If you see the example above, this app probably needs some love into the initial bundle, as the code evaluation is taking almost 1sec. If that is not blocking the initial render, that’s fine. Otherwise, we have many ways to overcome this, and we will get into more details in the next section.
3- Bundle Visualizer
Let’s suppose you are sending all React Components from Material UI in the initial bundle. You don’t know about that yet, but you know your app is slow to load.
How can we find that this is the issue? With bundle-visualizer.
If you do the following import, without tree-shaking, on meteor:
import { Button } from “@material-ui/core”
You will end up with all the library components in your bundle, which means MBs and MBs of unused files being sent to your user. You for sure don’t want that, right?
Bundle Visualizer view on Meteor, more details here
So, for generating the pretty chart above and identifying that your bundle has 5 megabytes of material-UI components, you can run your app locally as follows:
meteor --extra-packages bundle-visualizer --production
The solution for the issue varies from case to case, and we will explain it better in the next section.
4- Meteor DevTools Evolved
You can’t optimize if you can’t measure. That’s a rule of thumb.
Subscription monitoring with Meteor DevTools Evolved
With this chrome extension, you will be able to see how many subscriptions you are waiting for, at which time; see the number of documents you are loading on minimongo, which will directly impact your performance when searching for items with find(remember, minimongo don’t have indexes so that all searches will do a full scan in your collection — except for _id only queries)
Ok, now we know our numbers, and we know what is slow. I will point out in this article some standard optimizations that I think are easily implemented and can bring tremendous results, apart from the ones I’ve already mentioned earlier here. This is not an exhaustive list but a comprehensive one to help you get started.
Let’s start with easy wins then:
1- CDN
Do you know what a CDN is? Even if you don’t, you have for sure used one before. It is a way to distribute your static assets in several places, boosting the speed at which they can be served to users. It does this by caching and distributing your content over machines worldwide. You can read more about it here. But imagine you are hosting with Meteor Cloud at us-east-1, and a user in Brazil is accessing it. The RTT for this operation is vast, and your user will suffer from loading times.
Applying it to your galaxy-hosted app is easy, but it takes some steps depending on what you want to be served through it. We will be complete here, so if you want the maximum boost, follow all steps.
First, you will need to choose a CDN provider. Your CDN provider must have “origin” support, like Cloudfront. You need to create a new distribution in there and set origin support, like the following screenshot:
Create your CF distribution as follows. It’s ok to leave all settings as default, and optimize it further if needed
After that, we have to:
- Serve images with CDN:
Change all your URLs to point to your new URL showed in the dashboard, ex: https://d3ku71hz1f7ov0.cloudfront.net/.
From:
<img src="/instagram-icon.png" height="26" alt="Instagram" style="max-width: 30px; vertical-align: sub; display: inline-block;">
to
<img src="https://d3ku71hz1f7ov0.cloudfront.net/instagram-icon.png" height="26" alt="Instagram" style="max-width: 30px; vertical-align: sub; display: inline-block;">
If you want to automatically bust the cache for images on a new version, you can also append ?_g_app_v_={process.env.GALAXY_APP_VERSION_ID} to the URL. But keep in mind you will need to get this info back from the server, as it’s server only:
<img src="https://d3ku71hz1f7ov0.cloudfront.net/instagram-icon.png?_g_app_v_={process.env.GALAXY_APP_VERSION_ID}" height="26" alt="Instagram" style="max-width: 30px; vertical-align: sub; display: inline-block;">
2. Serve CSS/JS
Meteor does have a “hook” inside WebApp internals to change the URL we serve the JS/CSS assets, which is, by default, served from the server itself. We don’t want that, and we want that the CDN serves it, so we need to change the URL we send in the header that loads the assets.
It’s also important to note that Meteor already handles cache-busting by generating unique hashes on assets by bundle content, so you don’t need to worry about caching issues in your origin-supported CDN.
Rewrite the served JS/CSS URLs to use the CDN one, with the following call inside your server code:
WebAppInternals.setBundledJsCssUrlRewriteHook(
url => `https://d3ku71hz1f7ov0.cloudfront.net/${url}&_g_app_v_=${process.env.GALAXY_APP_VERSION_ID}`
);
3. Add CORS headers in your fonts requests
Serving fonts and other assets from another origin, i.e., your CDN URL, will cause access control origin issues. You have to set the header to allow it. Use your CDN URL for maximum security, or use a wildcard “*”.
WebApp.rawConnectHandlers.use((req, res, next) => {
if (
req._parsedUrl.pathname.match(
/\.(ttf|ttc|otf|eot|woff|woff2|font\.css|css|js|scss)$/
)
) {
res.setHeader('Access-Control-Allow-Origin', 'https://d3ku71hz1f7ov0.cloudfront.net');
}
next();
});
2- Initial Data SSR
If you have a dashboard or any dynamic system, you are probably building your screen based on data. And for this, you need data available to render your app at first.
You do have some options, like using skeleton, which is a great alternative. The solution I will present will make everything snappy and fast without sacrificing a meaningful first render.
Meteor Fast Methods is a library I made for Pathable Inc. It includes a way to preload data inside the HTML, which will populate the Minimongo and thus make your finds easy, as everything will be available as soon as possible. We remove the RTT needed when loading data after the app is loaded on the client.
The approach this library takes is sending with the initial HTML a JSON that contains all the data you need to render your app, and after that, it starts listening with Redis Vent, from the package redis-oplog changes on that cursors.
The usage is straightforward, just add the cursors you need available at the preloadData() method on the server, and use it after FastMethods.ready() is true.
If you want to use the redis-oplog part, also check the FastMethods.startListeningToChanges call.
This has dropped some seconds from the initial page loading at Pathable.
3- Split your client bundle and delay JS evaluation
You can use Meteor dynamic import to split your bundle into smaller parts. This helps significantly with first load performance, and it’s also an essential point on lighthouse scoreboard.
Using React and Meteor, it’s effortless to do. You will have to create a component that is not loaded in your app’s home screen and make it default exported(or create a wrapper). Then, just use react-router and React Suspense with Meteor dynamic import to load the route as needed.
AdminDashboardContainer file:
const AdminDashboard = () => <div>Heavy component here</div>
export default AdminDashboard;
In your route definition file, use react lazy() and Meteor import() to split your bundle and load as needed:
const AdminDashboardContainer = lazy(() =>
import('../../../app/admin/ui/AdminDashboardContainer')
);
<Route
path={`/adminDashboard`}
component={props => <AdminDashboardContainer {...props} />}
/>
You also have to set Suspense for showing a loading indicator while we fetch the bundle related to this screen. In your main app file, do the following:
<Suspense fallback={<Loading />}>
<ErrorBoundary history={history}>
<Routes />
</ErrorBoundary>
</Suspense>
And voilá! You are doing bundle splitting and saving a LOT of client loading time. Imagine your admin app, that some screens are rarely used, and you may have dozens of screens. The benefits are huge.
There is also a new concept applied in the above example, and it’s Nested Importing. You can read more details about it in our previous post, so we will only do some en passant observations.
Nested imports delay the evaluation of JS code until they are needed. This is extremely useful on Cordova, as most webviews implementations are slow to evaluate code. Do this if you see a high delay inside your profiling regarding “code evaluation.” How to do it? Simple, instead of importing everything at the header of the file, do it when you need the library. Example:
function doSomethingWithDates(){
import moment from 'moment'
return moment().add(2, 'days').toDate();
}
And, in another file, you have this:
export const DateComponent = () => {
const date = doSomethingWithDates();
return <div> {date.toString()} </div>
}
Moment won’t be evaluated until DateComponent is used, saving evaluation time at startup.
4- Reduce your bundle size
This might seem obvious, but there are subtle things you might found when analyzing your bundle with a bundle analyzer.
If you don’t use the entire library components, why are you serving it fully to your client?
To solve it, prefer libraries with ‘es’ implementations, so you can directly import the components and smaller libraries that can be measured with Bundlephobia.
Instead of including all lodash library in this way:
import _ from 'lodash';
Which is roughly 69.9kB minified, prefer to include only the methods you use:
import keyby from 'lodash.keyby';
or
import sumBykeybyfrom 'lodash-es/keyby';
5- Scale aggressively
It’s hard to be fast when your server is suffering. With Galaxy, this is easy to solve. You can set some triggers to upscale more aggressively while you fix your code or if you want maximum performance.
You can check how this is done with triggers here. I will share one that is a good starting point if you want to have more distributed connections:
With this trigger, we are saying that we will scale up one by one container if any of the following rules are matched:
- Cluster CPU usage is above 30%
- Cluster Memory usage is above 40%
With this, you will ramp your servers earlier than your load, and more containers will be spawn, hopefully meeting your demand. Wrapping up
Let’s summarize what we have learned today. The basic principle: If you can’t measure, you can’t optimize. With this in mind, we discovered some approaches for measuring bottlenecks and app performance, which were:
Measuring:
- Lighthouse
- Profiler
- Meteor DevTools Evolved
- Bundle Visualizer
We also learned some common easy wins in first load performance that can be applied today in your application.
Fixes:
- Use a CDN
- Serve data with SSR
- Split your client bundle and delay JS evaluation
- Reduce your bundle size
- Scale as needed
And that is it for today, I hope you have found this useful, and let me know in the comment section how all these things are helping you optimize your application and produce fast-and-furious Meteor apps.
First load optimization with Meteor was originally published in Meteor Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.