Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explore options to eliminate flicker with async components #1

Closed
stereobooster opened this issue Dec 27, 2017 · 10 comments
Closed

Explore options to eliminate flicker with async components #1

stereobooster opened this issue Dec 27, 2017 · 10 comments

Comments

@stereobooster
Copy link

To do this you need to be able to save state. If you can save state and pass it to window you can use, for example, loadable-components

import { loadComponents } from "loadable-components";
import { getState } from "loadable-components/snap";
// you need to save state of getState in the end of page render

// loadComponents will pickup this statet
loadComponents().then(() => {
  hydrate(AppWithRouter, rootElement);
});

Solution described here

@rakeshpai
Copy link
Member

Hey @stereobooster! Firstly, thanks for react-snap. react-snap and react-snapshot were my inspirations for this module.The idea of using puppeteer, and inlining the critical CSS (and general perf optimisations) came from react-snap. However, the minimalCSS package didn't do the trick for me - it seems to only work with external stylesheets, whereas I wanted something that works with CSS-in-JS.

While I was playing with react-snap, I tried loadable-components after seeing it suggested in your readme. (In fact, the loadScripts in snapshotify is inspired from loadable-components.) However, I was unable to get it to work for me. Details here: gregberge/loadable-components#25

I guess I need to dig deeper. However, I'm unclear about how it could eliminate the flicker. The fact that import(...) returns a promise makes it necessarily asynchronous. Meanwhile, since react has to render something in the hydration pass, it ends up clearing the DOM. So, the flicker only lasts for the shortest of times (doesn't wait for the bundle to load - that's already loaded). But it is noticeable enough to be slightly annoying.

My current cop-out excuse is that the flicker only lasts for the shortest possible time, the preloading of scripts that this module does makes it work across any async loading mechanism (including react-loadable), and the flicker only happens once during the initial load. Not entirely happy about this anyway, but such is. :)

As far as I can tell, the only way to prevent the flicker is to somehow prevent react from rendering an async component if the DOM already has stuff in it. But as far as I know, this isn't possible. (I could be wrong though - I'd love to stand corrected.)

@stereobooster
Copy link
Author

reactSnap and CSS-in-JS

There are a lot of types of CSS-in-JS solutions. If you are talking about a solution which injects CSS as style tag in the document. You already have "critical CSS" problem solved. When a snapshot is taken CSS-in-JS will inject the only CSS required for the current page and html-minifier will compress CSS inside inline tags. For CSS-in-JS with "style tag approach" you do not need inlineCss flag.

reactSnap and loadable-components

As far as I can tell, the only way to prevent the flicker is to somehow prevent react from rendering an async component if the DOM already has stuff in it. But as far as I know, this isn't possible. (

This is exactly what reactSnap and loadable-components do. Full explanation on how it works:

import { hydrate, render } from 'react-dom';
import { loadComponents } from "loadable-components";
import { getState } from "loadable-components/snap";
window.snapSaveState = () => getState();

const rootElement = document.getElementById('root');
if (rootElement.hasChildNodes()) {
  loadComponents().then(() => {
    hydrate(AppWithRouter, rootElement);
  });
} else {
  render(<App />, rootElement);
}
  1. reactSnap visits page, there are no child nodes in the root, React will render document.
  2. while render happens loadable-components collects information about loaded components
  3. after render finished reactSnap calls snapSaveState function
  4. loadable-componentss getState returns information about loaded components (which were tacked in step 2)
  5. reactSnap saves this information in window object
  6. reactSnap saves a snapshot (e.g. html)

Now when real user visits the prerendered page (e.g. snapshot)

  1. user visits page, there are child nodes in the root, so the browser will execute loadComponents()...
  2. loadComponents() will check if there is information about async components in window object
  3. Information about async components present in window, so loadable-components will wait while all components get loaded
  4. as soon as all components loaded hydrate function is called

If something above is confusing or unclear let me know. Maybe I need to improve documentation of reactSnap

@stereobooster
Copy link
Author

Also worth to note that most (not saying all) CSS-in-JS solutions are anti load performance solutions out of the box. See this example - gained ~20kb by removing styled-components

@rakeshpai
Copy link
Member

Thanks for the detailed explanation, @stereobooster. snapshotify already does all of these things you mentioned to eliminate flicker, in one way or another. That's not the reason for the flicker. Sorry if I was unclear. The flicker occurs after step 4 in your explanation for hydration. So, I'll add to your list:

  1. When hydrate is running, at some point it encounters the import(...) call. This call is necessarily async. All the assets are already downloaded, but the nature of import(...) forces us to wait for the next tick.
  2. Meanwhile, react is synchronous, and has to render something. There's no way to cancel rendering of a component AFAIK. So, react clears the DOM. <-- This is what causes the flicker.
  3. The next tick comes around, the import is resolved, and the content shows up as expected.

The flicker isn't because bundles haven't loaded - they already have. It's because import is asynchronous and react is synchronous. The flicker lasts for exactly 1 tick, and not for the duration of downloading the bundle, because the bundle is already downloaded.

loadScripts in this module, similar to loadComponents in loadable-component, is responsible for delaying hydration until all required scripts are loaded. It works directly with import calls, and is independent of which async component you've used (react-loadable or loadable-component).

AFAIK, there's no way around this issue. I'll be happy to be corrected. shouldComponentUpdate false comes very close in helping fix this, but unfortunately, it doesn't fire during the hydration phase.

About CSS-in-JS, I'm using glamor, which I'm pretty sure uses the CSSOM to create styles. There are no style tags on the page as a result of using glamor. This module extracts style rules from the CSSOM, which works regardless of the CSS approach used (external CSS file or any CSS-in-JS flavour). Thanks for bringing up the issue about libs that insert style tags - snapshotify would effectively duplicate the style tag in this scenario. I'll see what I can do to fix it.

@stereobooster
Copy link
Author

It's because import is asynchronous and react is synchronous. The flicker lasts for exactly 1 tick, and not for the duration of downloading the bundle, because the bundle is already downloaded.

I see. Thanks

AFAIK, there's no way around this issue. I'll be happy to be corrected

We can break async nature of promise, for example set some additional field with the source of module and read it directly in synchronous manner. Yes I know this is hack.

I'm using glamor

I haven't worked with it, but this is quote from documentation:

there are two methods by which the library adds styles to the document -

  • by appending css 'rules' to a browser backed stylesheet. This is really fast, but has the disadvantage of making the styles uneditable in the devtools sidebar.
  • by appending text nodes to a style tag. This is fairly slow, but doesn't have the editing drawback.

@stereobooster
Copy link
Author

stereobooster commented Dec 27, 2017

But wait a second

The flicker lasts for exactly 1 tick, and not for the duration of downloading the bundle, because the bundle is already downloaded.

It means that any async solution (loadable-components or react-loadable etc) with prerender (snapshot or SSR) has this problem.

@rakeshpai
Copy link
Member

I haven't explored SSR, so I can't speak to that. But yes, no matter the async solution, and no matter the snapshot tool, as long as react isn't going to wait for the import, it's going to flicker.

@stereobooster
Copy link
Author

I haven't explored SSR, so I can't speak to that

It doesn't matter - issue happens in the browser. It doesn't matter how you generate HTML on the server. I wonder, how nobody else has noticed this before. 👏 👏 👏

@rakeshpai
Copy link
Member

:) People probably haven't noticed it because it's just exactly 1 tick - blink and you'll miss it. The worst 'flicker' happens when the bundle isn't available in the first place, as you've documented in the react-snap readme. That would definitely be noticeable - the duration for which the DOM is empty would depend on things like bandwidth and latency for downloading the bundle. However, that's already a solved problem, either with react-snap+loadable-component or with snapshotify.

@stereobooster
Copy link
Author

we discussed this in another issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants