Meteor Blog: First Load Optimization with Meteor

Hey, make sure you read this post.

In summary you will learn:

Measuring:

  • Lighthouse
  • Profiler
  • Meteor DevTools Evolved
  • Bundle Visualizer

Fixes:

  • Use a CDN
  • Serve data with SSR
  • Split your client bundle and delay JS evaluation
  • Reduce your bundle size
  • Scale as needed

Good optimizations :slight_smile:

5 Likes

Great post, I really wish I had this a month ago when I cut over our hosting to Galaxy and went through all of this myself.

There are a few other things that helped me a lot with improving the initial page load experience:

  • Using Meteor.defer() and this.unblock() in Meteor Methods to reduce response times. Since all DDP messages are executed in a queue, one slow method will make all data load slower for a user which can make the initial app loading feel slow. For example we have a logging endpoint, the user doesn’t need to know when the logging is done so I just do the entire method within a Meteor.defer() callback.
  • Using the staringatlights:inject-data package to take any data that was being loaded immediately by the client on every page load, and instead inject it right into the HTML so that it’s available without making a round trip call to the server (This is how the old meteorhacks:fast-render package worked).
  • Using the approach described by @banjerluke in this post to add a loading splash screen that’s included in the initial HTML so that it displays immediately while the app is loading to prevent initial render juttering. When the client bundle is loaded it can then remove the splash screen. This doesn’t make it load any faster but it does make it appear to be faster to the user since something is displayed the moment the HTML is downloaded.

Could you please post your server-side code for this? The code posted last year by @banjerluke seems not to work in the latest version of staringatlights:inject-data.

Is there a way to enable text compression on Galaxy? It is one of the things that came up on my Lighthouse report.

I don’t use staringatlights:inject-data anymore. Here’s my current code, working on Meteor 2.1:

const head = Assets.getText('initial-response/injected-head.html')
  .replace(/<!--.+?-->/g, '')    // these three lines are crude minifiers which are optional
  .replace(/\s*\n+\s*/g, '\n')
  .replace(/\s\s+/g, ' ');

const body = Assets.getText('initial-response/injected-body.html');
const script = Assets.getText('initial-response/injected-script.html')
  .replace(/\/\/ .+$/g, '')     // also unnecessary optional minifiers
  .replace(/\s*([;)(}{:=])\s*/g, '$1');

WebAppInternals.registerBoilerplateDataCallback('initial-response', (request, data, arch) => {
  // if ( request.browser.name == "ie" ) {}    // in case you want to send something different to ol' IE...
  if (arch.startsWith('web.browser')) {
    data.dynamicHead = (data.dynamicHead || '') + head;
    data.dynamicBody = (data.dynamicBody || '') + (body + script).replace(/\n\s+/g, '');
  }
});

1 Like

startingatlights:inject-data working code example

I use the startingatlights:inject-data package for other things and it works fine, but it’s not needed for adding a splash screen.

Anyways, this is the working code example for using the inject-data package

On the server

WebApp.rawConnectHandlers.use(function (req, res, next) {
  // for html requests, inject the variable
  if (req.headers.accept && req.headers.accept.indexOf('text/html') !== -1) {
    InjectData.pushData(req, 'my-variable-name', someDataObject)
  }
  next();
});

On the client

InjectData.getData('my-variable-name, function (myVariable) {
  // now have  got the variable on the client
});

loading splash screen code example

There’s no need for the inject-data package if you just want to add a splash screen. All I did was add the loading splash screen div right to my index.html so that it’s included in the HTML that’s sent to the client, then in the client JS when the app starts you can remove the splash screen.

<body>
  <div id="app-wrapper">
    <!-- this is the loading screen -->
    <div id="app-loading-splash-screen">
      <div class='circle-loading-spin'></div>
    </div>
    <div id="app">
      <!--app is rendered here-->
    </div>
  </div>
</body>

Then in the client startup code you can remove the splash screen

// client application startup
Meteor.startup(async () => {
  // the initial index.html file contains a splash screen to cover the page w/a loading spinner
  // while the full app is being loaded + parsed. now that app is ready we will remove it
  const loadingSplashScreen = document.getElementById('app-loading-splash-screen');
  loadingSplashScreen && loadingSplashScreen.classList.add('fadeout'); // this class animates opacity to zero

  setTimeout(() => {
    loadingSplashScreen && loadingSplashScreen.parentNode.removeChild(loadingSplashScreen);
  }, 1000);
});

This example just waits 1 second after the app is loaded before hiding & removing the splash screen, but you could also hide it after all your necessary data is loaded or after any other thing that you need to wait for.

Hi, why did you move away from Galaxy?

These improvements are not related to Galaxy, just optimizations that Meteor developers need to be aware.

I already replied a few support tickets about this, check here for example Using gzip on Galaxy - #4 by vikr00001

What exactly was the feedback from Lighthouse? Maybe it was about something else.

1 Like

Moving to, not away :slight_smile: we’ve been on Galaxy for a month now and it’s been going great after some performance tuning (although I do still prefer MontiAPM over MeteorAPM)

The link on the particular item links to this page:

Possibly a wrong identification like in the post you linked.

Thanks, @efrancis and @banjerluke for these great examples.

I added some splash screen features. Here’s the approach I’m trying out.

In the <head> section of client/main.html:

  • css styles needed

In the <body> section of `client/main.html - before adding splash screen:

<div id="app">
</div>

…i.e. the div React will put its component in.

After adding splash screen:

<div id="app">
<!--  HTML used for my splash screen-->
</div>

Then when React loads it removes everything inside <div id="app"> and replaces it with React components.

1 Like

In addition to the above, I also followed the instructions here to reduce my Lighthouse render blocking time, particularly hosting my own font files, and moving script calls to the bottom of the body tag for scripts called in my client/main.html file.

I’m having a big problem with the Zendesk chat that is on my main landing page.

As you can see, it blocks the loading even though I load the initial script already with async but as that one is very small and it then starts to load more than 500kb afterwards I’m running out of ideas how I can further improve it (other than taking it off my landing page completely)

Here’s a screenshot of my script already being at the bottom of my index.html

Any help is appreciated, the URL in question is Your DNA family

Do not load the chat script together with the page since it is most likely that using chat will not be the first thing in the mind of the user. Load the chat script only on a certain event (trigger) of the user

1 Like

It’s basically a single landing page. The next step is to click on the sign-up button which will trigger the start of the actual Meteor app which is under a different subdomain.

What kind of trigger do you have in mind?

Something like loading it only after 5 seconds? How can this be done without blocking the whole loading process? The solutions here all seem to block.

What I do is load it after X seconds. You just need to create the script tag on meteor.startup and add it to the head or the body, as you prefer.

I use something like this

Meteor.startup(() => {
  setTimeout(() => {
    // create and add tag to head
  }, 7000);
})

Thanks for showing an example. My landing page is a vanilla HTML though and not a Meteor one (we split that only load the Meteor app when the prospect wants to create a new user).

This is the current script code in index.html:

<!-- Start of yourdnafamily Zendesk Widget script -->
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=abcdefghij0123456789" async></script>
<!-- End of yourdnafamily Zendesk Widget script -->

How do I load this after 7 seconds, like in your example?

Ill do something like this

<script>
setTimeout(() => {
  // Init zendesk chat plugin
    var zendeskScript = document.createElement('script');
    zendeskScript.setAttribute('id','ze-snippet');
    zendeskScript.setAttribute('src','https://static.zdassets.com/ekr/snippet.js?key=XXXXXXX');
    document.head.appendChild(zendeskScript);
    // End init zendesk chat plugin
}, 7000)
</script>
1 Like

Thank you so much, this works very well

1 Like