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

Flicker/layout shift with dynamic imports - Next.js #37

Closed
wjd3 opened this issue Oct 20, 2021 · 11 comments
Closed

Flicker/layout shift with dynamic imports - Next.js #37

wjd3 opened this issue Oct 20, 2021 · 11 comments

Comments

@wjd3
Copy link

wjd3 commented Oct 20, 2021

I'm following this guide from @ScriptedAlchemy (also on Stack Overflow) to implement lazy hydration in my SSG Next.js app.

It works, except the HTML of any dynamic import (using next/dynamic) gets destroyed during hydration. This causes flickering and huge layout shifts on larger websites.

The flicker/shift doesn't occur when directly importing a component using import - but then lazy hydration becomes redundant, because the component's JS is bundled with the initial page load.

Here's a link to a reproduction.

Any help would be greatly appreciated. I'm hoping to use this technique on a number of high-traffic websites. Lighthouse performance scores are up ~10-15 points and initial page load size is down over 50% for the site I'm testing it on.

@hadeeb
Copy link
Owner

hadeeb commented Oct 21, 2021

LazyHydrate expects the code to be loaded.
The solution is here to delay hydration until the code is loaded. Something like this might be what you are looking for.

The prop promise was added to support lazy loading. This works fine if you have control over code loading.

@wjd3
Copy link
Author

wjd3 commented Oct 21, 2021

Even when using your solution or promise, there's a flicker.

hydration-flicker.mov

It appears to happen right when hydrate is called. For emphasis, here's a recording of hydrate being called 2s after import.

hydration-flicker-settimeout.mp4

@ScriptedAlchemy
Copy link
Collaborator

ScriptedAlchemy commented Oct 30, 2021

That's because next dynamics loading state is null, youd need to set the loading state to be be something like danger inner html "" like we have done so while waiting for dynamic to load the js it doesn't destroy the markup with a null loading state

@benzizoo
Copy link

benzizoo commented Nov 4, 2021

@ScriptedAlchemy could you please share a working example of having the hydration to take place after dynamic imported component has been imported (using react-lazy-hydration)?
I wonder how to make it work with the promise prop you mentioned along with whenVisible / whenIdle.

@wjd3
Copy link
Author

wjd3 commented Nov 4, 2021

@ScriptedAlchemy As you can see in my updated repro, the flicker still happens when using the loading prop in next/dynamic like so:

const LazyComponent = dynamic(() => import('../components/LazyComponent'), {
  loading: () => (
    <div
      suppressHydrationWarning
      dangerouselySetInnerHtml={{
        __html: '',
      }}
    />
  ),
});

This occurs both when importing LazyHydration from react-lazy-hydration or using the custom implementation from @hadeeb. Is there something I'm missing? 🤔

@ScriptedAlchemy
Copy link
Collaborator

Try using a query selector all and add a class name to lazy hydrate props. You should be able to scrape the inner html off the DOM and reapply it on the fly. This is what I am currently doing. Could be improved tho but I've only done this for a handful of components where I can't have destructive DOM operations

@wjd3
Copy link
Author

wjd3 commented Nov 10, 2021

Thanks for the pointer - it works now! This is what I ended up with:

const LazyComponent = dynamic(() => import('../components/LazyComponent'), {
  loading: () => {
    // Grab the HTML from the DOM and use it as the loading component to prevent layout collapse/flickering
    const lazyEl = document.getElementById('lazy')?.outerHTML;

    return (
      <div
        style={{ display: 'contents' }}
        dangerouslySetInnerHTML={{
          __html: lazyEl ? lazyEl : '',
        }}
      />
    );
  },
});

Here's an alternative using the 1.5kb htmr library that avoids using a wrapper div with dangerouslySetInnerHTML:

const LazyComponent = dynamic(() => import('../components/LazyComponent'), {
  loading: () => {
    // Grab the HTML from the DOM and use it as the loading component to prevent layout collapse/flickering
    const lazyEl = document.getElementById('lazy')?.outerHTML;

    return (
      lazyEl ? htmr(lazyEl) : null
    );
  },
});

@wjd3 wjd3 closed this as completed Nov 10, 2021
@ScriptedAlchemy
Copy link
Collaborator

Just a warning, parsing html back into react browser side it's extremely expensive and like 120kb. Danger html is probably better especially since you only keep that markup for like 100ms or less.

@Sin1tar
Copy link

Sin1tar commented Feb 12, 2022

Just a warning, parsing html back into react browser side it's extremely expensive and like 120kb. Danger html is probably better especially since you only keep that markup for like 100ms or less.

thank you very much for your article, please, what about module styles (sass/less/etc)?
nextjs keep styles in .js, we havent access to the styles in components

is it possible to preload styles together with unhydrated static html?

now in the browser we have only unhydrated html without his styles,
but if we'll trigger rehydratation (for example, by "onClick"), after load .js file all styles are sync.

@ScriptedAlchemy
Copy link
Collaborator

youd need to ensure the styles are ssrd. if they are itll show up. server still executes and loads the css files for the file

@Sin1tar
Copy link

Sin1tar commented Feb 13, 2022

youd need to ensure the styles are ssrd. if they are itll show up. server still executes and loads the css files for the file

thank you, all works fine! 👍

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

5 participants