Fast initial load of dynamic pages with Meteor and React

With the changes in the new version of Lighthouse, we saw our pages drastically dropped in score as compared to the last version (version 6 vs version 5)

This forced us to take a look again at how we are loading our pages and worked hard during the last couple of weeks to change the structure of our meteor pages.

Here are some learnings that might help other users of Meteor and React

  1. Server Side Rendering
  1. Inline CSS
  • @wildhart technique of inline css: Pre-rendered landing pages with Critical CSS
  • react-router to identify and cache the inline-css of different routes (although the pages are dynamic, each route normally displays the same page structure (i.e. the same critical css); used staticContext for cases where a page structure changes depending on some variable)
  1. Async load CSS
  1. Proper React hydration without re-rendering (for many, no white-screen flicker)
  • react-router for the staticrouter context: https://reacttraining.com/react-router/web/example/static-router
  • fast-render-like data handling: we did not use fast-render in this case but the idea is the same. When generating the SSR page, we attached the data from the static context to the generated HTML page. During react hydration, the data is being used to hydrate the page and not query from the server

Sample pages:

Feedback welcome so we can further improve our pages

10 Likes

“Inline CSS”

—> i always use styled-components for react-apps. it can collect style sheets to send along with the initial page server side render.

As a pro-tip: in chromes network tab, find the request of the html document, select it and click on preview. Does it look right?

if yes, stylesheets are correctly sent along,

1 Like

Would sending the main CSS bundle with HTTP/2 push headers be just as good?

I am personally wary of using CSS in JS, because of the runtime overhead, even if it speeds up page load. (Even though I do use it on my main app - mostly because Material-UI uses JSS.)

I guess that would be just as good

but with respect to “performance last”: css in js is usually much cleaner, easier, more flexible and maintainable then pure css (or even scss). I would value that much higher than some runtime overhead (unless you are some tech giant where every milisecond matters)

We did a lot of tests with HTTP/2 multiplexing (although with JS and not CSS): No JS files concatenation for production bundle

But for all tests that we did, for the smaller packages, the best it can do is just around the performance of one single bundle.

We do not adhere to CSS in JS. It does not fit how we look at how CSS works

Just learned something new. Thanks :+1:

3 Likes

My pattern is still very modular - I have an SCSS file right next to my component definitions in the file hierarchy, it’s just included in a main SCSS bundle instead of the JS, and all delivered at once instead of piecemeal through JS. Every file has a “root” class/id to keep things modular. CSS in JS always seemed like overkill to me.

@rjdavid Do you use Apollo? Can you share your SSR code? The front-end source code for the web page looks great

We are not using Apollo.

Cannot share our SSR-code for now. Will require some cleaning as it includes some app-specific code. Although the structure is as I have shared above

@rjdavid meteor1.10.2 + apollo 3 + ssr

Check it out. The only problem with reloading and blinking is that it’s a perfect starter kit

From experience, the blinking problem (white-screen flicker) is caused by the following in a component:

React render -> componentDidMount -> query data -> callback with results -> setState with the results -> re-render

We solved this issue by ensuring that componentDidMount will not run any data query function because the state already has the data from the SSR page. Without data query, no setState, no re-render. Therefore, it is just hydration in the initial load.

2 Likes

That’s how we did as well.

You need to make sure the SSRed page looks exactly the same as what the client will initially render which is done by injecting the data in the page and using it for the initial load.

@rjdavid Thank you very much. I will study it again, according to your idea

Thank you very much, maybe my project is a little different, if it is a single template will not find this problem

Updated our handling to use PWA + AMP

Well done @rjdavid!

As you know, I do something similar with Vue - just for sales landing pages, not product pages. What’s the trick in removing the flash once Vue/React take over the DOM? In my case I don’t think I need data-hydration because these are effectively static pages.

@wildhart the reason for the flashing in react is the call to setState() after mounting the component which causes a re-render.

Hi, I am updating a Meteor app we’ve been maintaining for years, thanks a lot for this summary with recent sources!
One question I have with Loadable: code-splitting in Meteor seems to be very tied to dynamic loading. I guess this make sense from a technical standpoint, in the scenario where you navigate through links in the SPA.

Yet for a big application, I hit some limitations. For instance, I’d like some public facing pages to have their own separate bundle. Currently, this would be dynamically importing 99% of the app so this public facing page is not “polluted”. Is that the sign that I need 2 apps? Is there a solution with one app to handle this scenario?

To give a concrete setting: we have a backoffice to setup forms, and a page to display the form for end users. Both are deeply tied, so one application is a good fit, yet both have very different performance requirement. Should I dynamically the whole backoffice just to boost the page that displays the form?

In our case, all our “admin stuff” is separated into its own app.

For the app above, it has huge members-only modules that use code-splitting. Previously, it was separated into its own app but maintainance becomes a headache and we ended just joining the guest and members only into one app with separate admin-only app

But in terms of code-splitting, it can really make the decision easier to join modules into one app