Google Core update and the very bad impact on meteor apps (FCP/LCP/CLS) - Desperate call for help

@ivo, I’ve recently implemented SSR for my entire site (https://bibbase.org) using Meteor’s server-render package and it works fine. I’m not using Apollo though. What was the issue you ran into when trying to use that package?

1 Like

To be honest, Apollo is the main issue…

I mean Apollo is great but plugging it without SSR and getting used to it is already a bit painful, but checking some documentation about SSR with Apollo is really not clear. Most (all?) of them are outdated, no real example or integration with Meteor… Gotta say that’s a reason why I would probably not use it in another project.

@chfritz How do you handle cache without Apollo, any alternative you recommend? We are using react-query on a project at my company and it is much easier to implement than Apollo, however we do not have SSR need for this project (pure B2B, no SEO needed) so couldn’t say if that’s doable.

Do you have a codebase you’d be willing to share regarding your implementation of SSR? It is just plain Meteor/React using tracker and methods ?

SSR with apollo-client is not hard and you can do it with meteor in the exact same way is with a plain create-react-app.

Part 1: server

you should carefully read the official documentation: https://www.apollographql.com/docs/react/performance/server-side-rendering/

there where you see the getDataFromTree call, thats roughly the code that will go into onPageLoad (server) described here: https://www.apollographql.com/docs/react/performance/server-side-rendering/

SSR also affects routing, so you should use react-router and read some tutorials for that as well, see for example https://www.digitalocean.com/community/tutorials/react-react-router-ssr

If you set this up, ignore the client for a moment and check if it works as you intended. You can e.g. use curl to fetch the raw html source or use the “Preview” function in chrome’s network tab to see what the server actually returned. Verify that it returns what you expect.

Also make sure that you write this __APOLLO_STATE__ variable in the markup as described in the documentation above. It does not affect what the server returns, but its important that the client already has something in the cache. Without it, the client first needs to fetch all data again. This usually leads to a flicker after ssr, because all components go back into “loading:true”

Also watch for errors on the server. A typical problem is that you use browser-only api (like window) which will either throw errors or have unexpected behaviour.

Part 2 client:

if every route works on the server, you can now switch to the client.

Read the tutorials again above. You will create a different ApolloClient instance that will read its cache from the __APOLLO_STATE__ variable (see https://www.apollographql.com/docs/react/performance/server-side-rendering/#rehydrating-the-client-side-cache). Also the router setup is slightly different.

as described here https://docs.meteor.com/packages/server-render.html, on the client you will use React.hydrate. This function renders your client side app inside the dom-node where it already finds the existing markup from the server, Instead of replacing it, it will try to “hydrate” it, “bringing it back to live”.

5 Likes

In complement of what macrozone said, you should :

  • prepare your client code to be fully compatible with SSR : move client only API calls (such as window, event listeners, etc.) into componentDidMount or useEffect.

  • Because Meteor server cannot compile scss and css imports you need to remove them from your react components : either replace them by a global import in the head from your public directory or move to an in-JS solution.
    The best solution would be the styled-components library which you can fully implement incrementally.

  • When initializing the ApolloClient, you should set SSR: Meteor.isServer.

  • If your using the react-router-dom library, dont forget to use a StaticRouter on the Server and Router on the client.

  • If you’re using the react-helmet library to interact with the document head you should move to react-helmet-async (which is the same API but it will work with async stuff such as Apollo).

3 Likes

@macrozone @nschwarz Thanks to both of you for the leads, I’m indeed using scss, so quite a lot of work to change it but will check all this. Any of you have by chance a working example of github of implementation of meteor/react/apollo ssr with the latest libraries? That would be huge help as well but will check all the doc.

Hello there,

I cannot give you a working example from my apps since it’s really embed deeply with my company needs, but I can give you advice on yours if you wish.

I would suggest to do this incrementally. Forget about Apollo on the server for now, since it’s the last step (and the apollo ssr documentation is not the clearest).

  • Make the transition for your styling solution.
  • Make your React Components compatible with SSR.
  • Make the server render the App.

If all is working, making the changes to Apollo will be really easy.

I did convert all my apps too to be SSR compatible, I know it seems really overwhelming (I used the exact same stack as yours); but I really did it in 2-3 days of work and I cannot be happier :wink: .

1 Like

@nschwarz

If I would start a public repo with a boilerplate for Meteor / React / SSR with actual stack (updated libraries, hooks, styled components), would you be in to help me on it?

my time for side project is limited (whisky-hamster I mention on this post is already a side project) But you see to know what you’re doing and I am motivated to switch to ssr in the first months of 2021 so could be good match to set is up and keep it public for others to enjoy.

Yes ! No issue at all, a boiler plate is a good idea !

Not sure how well this will fly here on the forums… :eyes: but from first glance I’d question if Meteor is the best choice of stack for this. Of course, I do not know the details of your project, but it seems to be a largely static ecommerce site. For something like this I would personally opt for something like Gatsby or Next.js, which give you static rendering out of the box.

Here’s a store I built with Next.js on top of the Shopify API. Note the speed (it’s actually even faster than it looks, because of the page transition animations), page loads are near instant. The Lighthouse score is almost 100 on all fronts, only thing dragging it down are the external scripts (Klaviyo, Facebook, etc).

I love Meteor but I also think every platform has its use case. Static sites is not where it shines.

1 Like

I kinda agree with you actually and you’re in full right to say so. I’m considering alternatives but always supported Meteor and think it could also work with it as well.

I intend to check a NextJs/Mongo solution to replace it as well but first would like to considered the Meteor possibilities.

I’m using the same stack as your for an e-commerce website and it’s been a wonder. Next is great but it has its limitation !

If you want a more flexible option, you could do Next for the front and Meteor as a GraphQL headless API.

actually there is : https://docs.meteor.com/packages/server-render.html

1 Like

I started working on the boilerplate, wanna try some stuff before I put it live and then ask for opinions / advice. I’m optimistic I can stay with Meteor.

1 Like

@macrozone @nschwarz

That’s as far as I could go so far with starting from the apollo skeleton and tried to build on top of it. Right now it’s not working, but it’s something :smiley:

I don’t think the link in the ssr file is good, found an alternative online on a repo but seems big, so wanted to have your opinion if that’s any good:

   const client = new ApolloClient({
     // simple local interface to query graphql directly
     link: new ApolloLink(({ query, variables, operationName }) => {
       return new Observable(obs => {
         graphql(schema, print(query), {}, {}, variables, operationName)
           .then(result => {
             obs.next(result);
             obs.complete();
           })
           .catch(obs.error.bind(obs));
       });
     }),
     cache: new InMemoryCache(),
     ssrMode: true,
   });

I know I’m also missing the last piece, the rendering part which should look like something:

 await getDataFromTree(App);

  sink.renderIntoElementById("app", html);
  sink.appendToBody(`
    <script>
      window.__APOLLO_STATE__=${JSON.stringify(client.cache.extract())};
      window.__CSS__=${JSON.stringify(ids)}
    </script>
  `);

But here as well I’m unsure where to get the html from.

Also I still would like to include react helmet and styled-components, but that’s gonna be later, first I want the original skeleton to work.

@ivo, I’ve been looking through your code and what I can tell is :

As an advice :
instead of creating both server and client config and App you should do everything as an isomorphic import (both compatible with the server and the client), it will make it easier to debug and understand.

on your client :

  • you should use ReactDOM.hydrate not ReactDOM.render in main.jsx
  • you should set ssr: false and ssrForceFetchDelay: value (value should be at least 100) in your ApolloClient config.
  • why do you use BatchHttpLink instead of createHttpLink ?

on your server :

You’re using getDataFromTree wrong : as the doc says it returns a promise so you should use it like this :

getDataFromTree(App).then(html => {
  sink.appendToHead(`
  <script>
      window.__APOLLO_STATE__=${JSON.stringify(client.cache.extract()).replace(/</g, '\\u003c')}
      window.__CSS__=${JSON.stringify(ids)}
    </script>
  `)
  sink.renderIntoElementById("app", html)
})

as the doc says getDataFromTree render static Markup (I don’t think you want that).
To understand the difference you should read :

If static Markup is not what you want you should use renderToStringWithData which works the same way as getDataFromTree.

Hi @nschwarz,

Thanks for the feedback. I tried to implement them but getting a bit confused how to fully handle the isomorphic part. Where should what be rendered. It’s not working right now still not having anything render from the server.

Also not sure where to get the ids from this part:

window.__CSS__=${JSON.stringify(ids)}

in your getDataFromTee app for the styling? Also should window.CSS and window.APOLLO_STATE be initiated somewhere on client ?

No error on server side but getting this on client side:

Warning: Expected server HTML to contain a matching <p> in <div>.
    in p (created by Info)
    in Info (created by Context.Consumer)
    in Route (created by Routes)
    in Routes
    in Router (created by BrowserRouter)
    in BrowserRouter
    in ApolloProvider

Updated repo is here:

@ivo,

I made a pull request on the repo with the needed patch to make SSR working with apollo.

As of window.__CSS__=${JSON.stringify(ids)} I don’t understand what you want to make with this.
If you want to pass the css created through styled components, please read the doc here : https://styled-components.com/docs/advanced#server-side-rendering

Initializing the apollo cache can sometimes be necessary if you have locale states to manage. for now you should be good.

Thanks a lot, I had a few mistakes here and there and I really appreciate you checking it up and clarifying the code.

I implemented the styled component and it seems to be working. I tried to implement Helmet (using react-helmet-async as you adviced) which I think is the last step before “releasing” our repo, but somehow I’m getting an empty title through the ssr. I updated the repo and would appreciate if you could tell me what you think is the issue, The SSR code is now:

import { onPageLoad } from "meteor/server-render";
import React from "react";
import { renderToString } from "react-dom/server";
import { getMarkupFromTree } from "@apollo/client/react/ssr";
import App from "../imports/both/App";
import apolloClient from "../imports/both/apolloClient";
import { ServerStyleSheet } from "styled-components";
import { Helmet } from "react-helmet";

onPageLoad(async (sink) => {
  const sheet = new ServerStyleSheet();
  const client = apolloClient;
  const tree = sheet.collectStyles(
    <App client={client} location={sink.request.url} />
  );

  return getMarkupFromTree({
    tree,
    context: {},
    renderFunction: renderToString,
  }).then((html) => {
    sink.renderIntoElementById("app", html);

    sink.appendToHead(sheet.getStyleTags());

    const helmet = Helmet.renderStatic();
    sink.appendToHead(helmet.meta.toString());
    sink.appendToHead(helmet.title.toString());

    sink.appendToHead(`
    <script>
    window.__APOLLO_STATE__=${JSON.stringify(client.cache.extract()).replace(
      /</g,
      "\\u003c"
    )}
      </script>
      `);
  });
});

and I added this in App in the both folder:

import React from "react";
import { ApolloProvider } from "@apollo/client";
import Routes from "./../ui/Routes";
import Router from "./Router";
import { HelmetProvider } from "react-helmet-async";

export default function App({ client, location }) {
  return (
    <ApolloProvider client={client}>
      <HelmetProvider>
        <Router location={location}>
          <Routes />
        </Router>
      </HelmetProvider>
    </ApolloProvider>
  );
}

In the Info file I have basic Helmet title and meta description tag and they’re only visible on client-side. On server I get this for title and nothing for description:

<title data-react-helmet="true"></title>

It was in your previous post that’s why I ask but it seems not needed, I think you copied one of my line of code somewhere, so nevermind :).

Thanks a lot again, I think we’re very close.

@ivo,

you’re using the wrong helmet library:

use import { Helmet } from "react-helmet-async"
not import { Helmet } from "react-helmet"

please read the documentation of react-helmet-async, you’re currently using the react-helmet api.

window.CSS=${JSON.stringify(ids)}

was in your post… : Google Core update and the very bad impact on meteor apps (FCP/LCP/CLS) - Desperate call for help - #34 by ivo

@nschwarz

Yes my bad, I copied the text before changing the library…

Anyway everything seems to be working so far:

You mentioned Initializing the Apollo Cache, is it something hard to do? Could it be worth it to add in the repo too? Not sure in which case it’s needed and what’s the implication, but if it’s not too complicated and doesn’t have any possible “bad” impact I think it could be great to add as well, if you have the relevant doc for our case or a small code example somewhere I can add it too.

Otherwise I think we’re good to go. Tell me if you agree too or if you see anything to be modify?