stereobooster / react-snap

👻 Zero-configuration framework-agnostic static prerendering for SPAs
MIT License
5.05k stars 394 forks source link

React.lazy chunk not deferred? #455

Open Daniel15 opened 4 years ago

Daniel15 commented 4 years ago

Bug Report

Current Behavior I have a create-react-app based site that contains a page that lazily loads a library:

const Chart = React.lazy(() => import('react-google-charts'));

This is so the page loads immediately when navigated to (via React Router), but the library can be deferred as it's not critical to the page.

However, when I run react-snap, it inserts a <script> tag for the lazy library directly in the <head> of the resulting HTML:

<script src="/static/js/3.e475cbac.chunk.js" charset="utf-8"></script>

which results in the "lazy" chunk actually blocking render and loading before all the other JS. This seems unexpected, particularly given it doesn't have an async attribute, and kinda defeats the purpose of prerendering, as rendering of the HTML is now blocked by the loading of this JS file.

Is that expected?

Reproducible demo Can provide if you say that this is unexpected behaviour

Expected behavior/code Looking for confirmation as to what should happen here.

Daniel15 commented 4 years ago

The workaround I ended up using is to not render the lazy component at all during snapping. I created a custom useIsPrerendering hook that returns true for react-snap, true for the first render from a regular user (to ensure the HTML is identical so it can properly rehydrate), and false for subsequent renders:

import {useEffect, useState} from 'react';

export const isPrerendering = navigator.userAgent === 'ReactSnap';

/**
 * React hook that returns `true` if prerendering or on initial render (to allow rehydration,
 * or `false` otherwise).
 */
export function useIsPrerendering(): boolean {
  const [isPrerender, setIsPrerender] = useState(true);
  useEffect(() => {
    if (!isPrerendering) {
      setIsPrerender(false);
    }
  }, []);

  return isPrerender;
}

Then in my code I just always return the loading state in that case:

const Chart = React.lazy(() => import('react-google-charts'));

export default function LazyChart(props: Props) {
  const placeholder = <LoadingPlaceholder />;
  const isPrerendering = useIsPrerendering();
  if (isPrerendering) {
    return placeholder;
  }
  return (
    <React.Suspense fallback={placeholder}>
      <Chart foo="bar" />
    </React.Suspense>
  );
}

function LoadingPlaceholder() {
  return (
    <div>
      Loading...
    </div>
  );
}