SSR/Apollo Loadable Components - Performance Leak somewhere

Hello everyone,

I tried to add Loadable Components from @captainn on top of your Apollo/SSR boilerplate (GitHub - Gorbus/meteor-react-apollo-ssr) we did with @nschwarz. The speed result after deployment are very good, max 1s LCP on mobile, 0.4s on desktop, just what I was aiming at with the work done in the last weeks, but, (there is a but) there seems to be a leak somewhere that causes CPU and memory usage to increase overtime on the server and therefore the fast performance to slow down over time (after 2h LCP is already at 3s, overnight CPU was at 130%…). I think I screwed up something when I added the LoadableComponents as I didn’t have leak issues before that (but a non optimized app).

here is the SSR code on the server:

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 {
  LoadableCaptureProvider,
  preloadAllLoadables,
} from "meteor/npdev:react-loadable";

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

preloadAllLoadables().then(() => {
  onPageLoad(async (sink) => {
    const sheet = new ServerStyleSheet();
    const client = apolloClient;
    const helmetContext = {};
    const loadableHandle = {};

    const tree = sheet.collectStyles(
      <LoadableCaptureProvider handle={loadableHandle}>
        <App
          client={client}
          location={sink.request.url}
          context={helmetContext}
        />
      </LoadableCaptureProvider>
    );

    return getMarkupFromTree({
      tree,
      context: {},
      renderFunction: renderToString,
    }).then((html) => {
      sink.appendToBody(loadableHandle.toScriptTag());
      const style = sheet.getStyleTags();
      sheet.seal();
      const { helmet } = helmetContext;
      const clientData = getClientData(client);
      sink.appendToHead(style);
      sink.appendToHead(helmet.meta.toString());
      sink.appendToHead(helmet.title.toString());
      sink.appendToHead(clientData);
      sink.renderIntoElementById("app", html);
    });
  });
});

and the hydratation:

import React from "react";
import App from "../imports/both/App";
import { onPageLoad } from "meteor/server-render";
import apolloClient from "../imports/both/apolloClient";
import { hydrate } from "react-dom";
import { preloadLoadables } from "meteor/npdev:react-loadable";

preloadLoadables().then(() => {
  onPageLoad((sink) =>
    hydrate(<App client={apolloClient} />, document.getElementById("app"))
  );
});

Any idea ?

Here are the metrics:

Memory going up slowly and CPU using more and more for the same requests

Search how to get a memory dump from a node server and then use chrome dev tools to figure out where the memory leak is happening

I’ve had the same problem on pixstoriplus.com, and have had to disable SSR completely in order to prevent it. I think the problem has something to do with the interplay between react’s renderToString and Meteor’s Fibers. There are various issues on this, and React’s official position is that they don’t support async operations in renderToString.

I haven’t been able to grab a memory dump from the server to validate this assumption, mostly due to time constraints. That’s probably the starting point.

Oh, if you are using Apollo exclusively (no fiber based Meteor APIs), it might be easy to simply opt out of Fibers support. I’ve done that in various attempts to solve this issue, but it breaks Meteor data sources and wasn’t useful for me.

Thanks @captainn.

I’m not using any pub/sub and just one Meteor methods in my project that I could get rid of, is this enough to be able to get rid of Fiber ? got to say I don’t have a full understanding of Fibers and how they work. If so, what’s the procedure ?

I would be very sad to learn that all my work turns out useless. Is it a problem linked to Loadable components ? removing this part could be enough ? (I loose some speed but avoid a memory leak at least…)

could this be of help:

GitHub - overlookmotel/react-async-ssr: Render React Suspense on server ?

The starting point really should be to get a memory dump, and see what’s leaking.

If you wanted to try something a little blindly, you could try something like wrapping your render in bindEnvironment - this isn’t technically “opting out” of fibers, but it does seem to lock you to a single fiber, at least.

This is not an area I have a great deal of confidence in, just some observations to offer from previous quick attempts at workarounds.

Meteor.bindEnvironment(() => {
  // render here
});

Best to get a memory dump, and see where your real leak is coming from.

The annoying thing about this leak is that basically everything works - I get regular rendering in my app, and all the output is correct. It just leaks memory, and it’s hard to figure out where that’s coming from.

Thanks, will try to investigate.

Indeed the most frustrating is that it works…

What the easiest way to do memory dump with meteor ? classical node memory dump libraries ?

I believe there is a way to hook up a local chrome debugger session to a running galaxy instance, and get a dump from there, but I didn’t see how to do that in the Meteor guide.

Just for my understanding,

is the loadable component library the problem ?

I don’t think I had the memory leak before I tried to implement the Loadable component, and afaik @nschwarz is using the same setup in production without any issue (don’t think he uses the loadable component)

I seem to have the leak even without Loadable component so I don’t think that’s causing it then.