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

All routes are chunked, causing hydration of prerendered pages to flicker #281

Closed
ajoslin opened this issue Aug 3, 2017 · 33 comments
Closed
Labels

Comments

@ajoslin
Copy link

ajoslin commented Aug 3, 2017

It appears that all routes are being chunked and async loaded, even without the presence of the async! prefix.

I've reproduced this using preact-cli 1.4.1 here: https://github.com/ajoslin/preact-cli-issue-all-routes-chunked. Just build and serve and check the network tab, it loads both bundle and route-home.chunk. And it does a render before the home chunk is loaded.

Details

I have a prerendered homepage. Here's the process when the user visits the site:

  1. Index.html loads with prerendered content
  2. Main JS bundle loads
    • Initial render is triggered without the home component downloaded
    • Bundle begins downloading route-home.chunk.js
  3. Wait for route-home.chunk.js is loaded
  4. Home route chunk loads and the home content renders

Between (2) and (4), a version of the app is rendered that does not include the home route's content, because the home route's JS is still loading.

This results in a noticeable "flicker" of the home content being removed then re-appearing as soon as the initial bundle loads.

I'm not using the async! prefix when importing my home route, but it looks like the home route is being chunked anyway.

@prateekbh
Copy link
Member

also related #274

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

Let me know how I can help. I might take a stab at seeing what's wrong with the preact-cli Webpack config if I have some time later.

@thangngoc89
Copy link
Collaborator

@ajoslin The async loader was implemented in a way that during prerendering , it act as a normal require so the above behavior is correct (maybe?)

But anyway, we should fix this !

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

@thangngoc89 The problem is that it's happening whether or not I use the async! prefix. I would definitely expect this behavior if async! is given, but it happens with or without it.

I've fixed this problem in my app for now by moving my routes into src/components/pages/.

@thangngoc89
Copy link
Collaborator

thangngoc89 commented Aug 3, 2017

@ajoslin ah. So it's because of route spliting behavior of preact-cli

@lukeed
Copy link
Member

lukeed commented Aug 3, 2017

This is supposed to happen.

We also state this in the README:

Fully automatic code splitting for routes

The async! prefix is primarily designed for lazy-loading large components. For example, a REPL component or a WYSIWYG editor.

As for your issue, you're seeing a content flash, correct? If so then we need to fix that part 😄

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

Got it. I certainly love this feature minus the flicker.

And yes, the problem is just the content flash and that it's rendering before the current route's bundle is ready.

@lukeed
Copy link
Member

lukeed commented Aug 3, 2017

Cool 😃 Do you mind refreshing me by sharing what your <script> tags look like? I don't have a CLI project in front of me atm.

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

Sure, here's what they look like (from the repository in the OP): http://ajoslin.co/UzAy0g/4Us7rtzG

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

And here's a movie of what the flicker looks like and the network tab: http://ajoslin.co/GIH2Ts/5kzN1wTO

@lukeed
Copy link
Member

lukeed commented Aug 3, 2017

Thanks, helpful! I see Network throttling, but do you have CPU throttling on too?

It seems to be just a really slow render, because the "flash"/re-render only happens once the route-home bundle has been downloaded.

@ajoslin
Copy link
Author

ajoslin commented Aug 3, 2017

I don't think that's the issue. It does one render right when bundle.js has loaded (3.04s) and then does another after home bundle has loaded (698ms later). You see all of the content (prerendered), then app content only (bundle), then all content again (bundle + home chunk).

@lukeed
Copy link
Member

lukeed commented Aug 3, 2017

Oh, silly. I didn't pay attention to the Pending status of the route-home request. 😅

@lukeed
Copy link
Member

lukeed commented Aug 3, 2017

My first guess is issue with AsyncComponent. I think it gets passed an empty string while the route is being fetched. That empty string becomes its child, which renders & replaces the current content.

I think we can add a falsey check to inside the AsyncComponent? Just need to ignore the first re-render (production & prerender only).

@ajoslin
Copy link
Author

ajoslin commented Aug 10, 2017

I don't think the problem is the first re-render, it's the initial render itself.

I tried:

  • Add shouldComponentUpdate to AsyncComponent that allows update only if state.child is truthy
  • Return null from render if state.child is falsy

Both of these still result in the Async component's content being erased immediately on initial render. Is there an idiomatic way to tell the component render to "keep whatever is inside this element and don't do a vdom diff?"

The "hacky" way would be something like this (though even this wouldn't exactly work):

render () {
  if (!state.child) return <div dangerouslySetInnerHTML={{__html: this.base.innerHTML}}>
}

@ajoslin
Copy link
Author

ajoslin commented Aug 15, 2017

Ping @lukeed, if you have a second would you mind commenting on the above?

@lukeed
Copy link
Member

lukeed commented Aug 15, 2017

Ooh, sorry, hectic week for me -- I'm in the middle of moving. 🚚

I think the truthy state.child should be enough.

As of right now, my theory focuses around the initial javascript render. After reviewing your video clip a few more times (thank you 🙏), the pre-rendered content is, of course, fine. It is only once the app begins to boot up that a full reset + full re-render happens.

That points back to this. So, my theory is that:

  1. HTML Content
  2. Bundle: Pending...
  3. Bundle: Loaded
  4. Bundle: Webpack dispatches route.home.js request
  5. Route: Pending...; AsyncComponent triggered
  6. AsyncComponent returns '' while pending...
  7. Main Bundle receives ''
    let app = interopDefault(require('preact-cli-entrypoint'));
    root = render(h(app), document.body, root);
    // at this point, replaces `document.body` with `''`
  8. Route: Loaded
  9. Application re-renders, returning a truthy HTML string (the expected content)
  10. Bundle re-renders, re-replacing document.body with the correct content

Inserting a truthy check at (6) or (7) should do the job.

@ajoslin
Copy link
Author

ajoslin commented Aug 16, 2017

@lukeed I appreciate the in-depth explanation. That all makes sense.

However, I'm just not sure how to accomplish "defer initial render until the current route's chunk is loaded." It doesn't seem like it's as simple as a "truthy check" somewhere.

It seems like entry.js would need to reach into the router, find the current path's component, and wait for that component's chunk to load before doing initial render. How would you go about doing that? Or am I completely off-base here?

@ajoslin
Copy link
Author

ajoslin commented Aug 20, 2017

Ping @lukeed if you have a sec could you take a look again at my above comment?

@lukeed
Copy link
Member

lukeed commented Aug 21, 2017

@ajoslin 🙈

Yes, I'm playing with the templates now. I'll try to come up with a solution & post it here!

@ajoslin
Copy link
Author

ajoslin commented Aug 26, 2017

Alright. I appreciate it. I'm still playing with this too, I'll post here if I come up with anything.

@ajoslin
Copy link
Author

ajoslin commented Aug 26, 2017

Hacky solution as POC: AsyncComponent emits "asyncComponentLoading" in its constructor, and "asyncComponentLoaded" in its done callback.

Then the app startup listens for those.

entry.js

  let root = document.body.firstElementChild;

  let firstInit = () => {
    let app = require('preact-cli-entrypoint');

    let isFetchingChunk = false
    window.addEventListener('asyncComponentLoading', () => {
      isFetchingChunk = true
    })

    let detachedApp = preact.render(preact.h(app), document.createElement('div'));
    if (isFetchingChunk) {
      window.addEventListener('asyncComponentLoaded', init);
    } else {
      init();
    }
  }

  let init = () => {
    let app = require('preact-cli-entrypoint');
    root = preact.render)(preact.h(app), document.body, root);
  }

  if (module.hot) module.hot.accept('preact-cli-entrypoint', init);
  firstInit();
}

The biggest problem with this is that we're mounting the app twice. This means that if any components in the app have side effects on mount, those side effects will happen twice.. we can't really tolerate that.

Is there a way to render the app detached, and then attach it later, once the async chunk loads?

The alternative to this method (determining a route is going to be async loaded at runtime) is to determine it at compile time with static analysis. Doing that is beyond my scope of knowledge.

@lukeed
Copy link
Member

lukeed commented Aug 26, 2017

I was putting my efforts into #329 -- now that that's done I can actually put effort into this guy. Been on my mind & want to hack at it 😈

@ajoslin
Copy link
Author

ajoslin commented Sep 5, 2017

@lukeed I had to go into production without route chunking (I moved all of my routes into src/components/pages). I'd like to fix that as soon as this is fixed.

I know your time is limited & valuable, so I put a $100 bounty on this issue to hopefully enable you to prioritize it: https://www.bountysource.com/issues/47899502-all-routes-are-chunked-causing-hydration-of-prerendered-pages-to-flicker

@reznord
Copy link
Member

reznord commented Sep 5, 2017

Hahaha! Clever move!

@lukeed
Copy link
Member

lukeed commented Sep 9, 2017

I think I solved it 😃 I'll post later tonight. Time for a walk!

@lukeed
Copy link
Member

lukeed commented Sep 9, 2017

Yup! Here's a quick video recording.

PR underway~

@lukeed lukeed added the has-fix label Sep 9, 2017
lukeed added a commit that referenced this issue Feb 8, 2018
lukeed added a commit that referenced this issue Feb 19, 2018
* FIX~! prevent route flickers 🎉

closes #364, #281

* fix linter / copy-paste error
@ajoslin
Copy link
Author

ajoslin commented Feb 19, 2018

@lukeed this can be closed now.

Also, please claim the bounty: https://www.bountysource.com/issues/47899502-all-routes-are-chunked-causing-hydration-of-prerendered-pages-to-flicker

💰

@FDiskas
Copy link

FDiskas commented Dec 8, 2020

Just for reference - none of discussed solutions worked for me. Got it working like so preact.config.js

export default {
    webpack(config, env, helpers, options) {
        if (env.isProd) {
            config.devtool = false;
        }
        if (!env.ssr) {
            config.output.filename = 'scripts/[name].js';
            config.output.chunkFilename = 'scripts/[name].js';
            config.optimization.splitChunks.cacheGroups = {
                default: false,
                vendors: false,
                route: false,
                chunks: false,
                styles: {
                    name: 'styles',
                    test: /\.s?css$/,
                    enforce: true,
                },
            };
        }
    },
};

Build result is:

                index.html ⏤  589 B (+81 B)
                  200.html ⏤  587 B
          bundle.d6a05.css ⏤  444 B
    styles.chunk.44761.css ⏤  124 B
         scripts/styles.js ⏤  162 B
         scripts/bundle.js ⏤  8.42 kB
      scripts/polyfills.js ⏤  2.13 kB
     scripts/route-home.js ⏤  224 B
 scripts/route-notfound.js ⏤  267 B
  scripts/route-profile.js ⏤  1.06 kB

@FDiskas

This comment has been minimized.

@developit
Copy link
Member

This issue is 3 years old and the related modules have been rewritten since then, it's exceptionally unlikely you're seeing something unrelated. The current version of preact-cli (3.x) includes asynchronous hydration by default that prevents flickering.

@FDiskas
Copy link

FDiskas commented Dec 11, 2020

I got this problem with a latest version

@vignesarul
Copy link

vignesarul commented Mar 8, 2021

The issue still occurs in latest version

"preact-cli": "^3.0.0",
"preact": "^10.3.2",

As @lukeed explained here, the flickering happens at step 6 ad 7.

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

Successfully merging a pull request may close this issue.

8 participants