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

App router: Content flickering with React.createContext and next/dynamic for SSG/SSR #64060

Open
KirillEvtushenko opened this issue Apr 4, 2024 · 18 comments · May be fixed by #65766
Open

App router: Content flickering with React.createContext and next/dynamic for SSG/SSR #64060

KirillEvtushenko opened this issue Apr 4, 2024 · 18 comments · May be fixed by #65766
Labels
bug Issue was opened via the bug report template. Lazy Loading Related to Next.js Lazy Loading (e.g., `next/dynamic` or `React.lazy`). linear: next Confirmed issue that is tracked by the Next.js team.

Comments

@KirillEvtushenko
Copy link

KirillEvtushenko commented Apr 4, 2024

Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/interesting-aryabhata-ckyy6f

To Reproduce

  1. Refresh the page
  2. Notice that the right block with Client component and RSC inside noticeably flickers

Current vs. Expected behavior

When using next/dynamic with React.createContext, I expect that it will work the same way as without React.createContext.
Please pay attention that initial request with html correctly contains all the needed html. Provider's rerender (recreating of the value object prop) then causes that flickering.

issue.mov

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.4.0: Wed Feb 21 21:44:43 PST 2024; root:xnu-10063.101.15~2/RELEASE_ARM64_T6000
Binaries:
  Node: 20.10.0
  npm: 10.2.3
  Yarn: 1.22.18
  pnpm: 8.6.5
Relevant Packages:
  next: 14.1.4
  eslint-config-next: 13.5.4
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.4.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

App Router, Dynamic imports (next/dynamic)

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), Vercel (Deployed), Other (Deployed)

Additional context

No response

NEXT-3074

@KirillEvtushenko KirillEvtushenko added the bug Issue was opened via the bug report template. label Apr 4, 2024
@github-actions github-actions bot added the Lazy Loading Related to Next.js Lazy Loading (e.g., `next/dynamic` or `React.lazy`). label Apr 4, 2024
@KirillEvtushenko KirillEvtushenko changed the title Content flickering with React.createContext and next/dynamic for SSG/SSR App router: Content flickering with React.createContext and next/dynamic for SSG/SSR Apr 4, 2024
@huozhi
Copy link
Member

huozhi commented Apr 11, 2024

Can you try switching dynamic() call to React.lazy() if you're not using ssr: false before we get it fixed? So far there's a suspense boundary warpped there, it probably can be removed in the future if there's no loading specified.

@huozhi huozhi added the linear: next Confirmed issue that is tracked by the Next.js team. label Apr 11, 2024
@KirillEvtushenko
Copy link
Author

Hey @huozhi ! Thanks for the answer.
I've tried to switch dynamic() calls to lazy():

'use client';

import { lazy } from 'react';

export const ClientComponent = lazy(() => import('./ClientComponent'));
export const ClientComponent2 = lazy(() => import('./ClientComponent2'));
// Other client components imports

However, I get this error (have never seen this kind of error before lol):
Error: Element type is invalid. Received a promise that resolves to: [object Object]. Lazy element type must resolve to a class or function. Did you wrap a component in React.lazy() more than once?

I found the only way to fix it: remove "use client" directive from the registry. But this breaks code-splitting and puts everything to the First load JS

Am I missing something?

@joshwcomeau
Copy link

Hi @huozhi!

I'm running into this issue as well, and also thought to try React.lazy(). This does indeed solve the flickering issue, but it causes a regression around the CSS being included on SSR; the awesome work you did to close #62940 isn't applied with React.lazy().

But I'm glad to hear that there's a potential fix, with removing the Suspense boundary when no loading state is provided! I believe this would solve the issue 👍.

@bluebeel
Copy link

I can confirm the next/dynamic doesn't work even if done in a "use client" file.
The solution we did for now was using React.lazy where we could. But yeah quite the regression compared to Pages Router ^^'

@KirillEvtushenko
Copy link
Author

KirillEvtushenko commented Apr 26, 2024

Moving aside the issue with React.ContextAPI breaking next/dynamic, there is one more “issue” with next/dynamic I want to show. 



We have a number of JAMStack apps built with Next.js, that we are currently moving from Pages router to App router. In CMS we have a content that describes a page -> sections, their order, props etc. Nothing fancy there, quite common approach. To map CMS content to actual components, we use this kind of registry (simplified):


// registry.ts
const ClientComponentLvl1 = dynamic(() => import('./atoms/ClientComponentLvl1'));
const ClientComponentLvl2 = dynamic(() => import('./molecules/ClientComponentLvl2'));
const ClientComponentLvl3 = dynamic(() => import('./organisms/ClientComponentLvl3'));

const components = {
  ClientComponentLvl1,
  ClientComponentLvl2,
  ClientComponentLvl3
}
export const getComponent = (name: keyof typeof components) => components[name]


// page.tsx
import {getComponent}  from "@/components/registry";

const getPageProps = async () => {
  const props = await fetch(`content from CMS`);
  return props;
}

export default async function Page() {
  const props = await getPageProps();

  const Section = getComponent(props.sectionName);

  return (
    <main>
      <Section />
    </main>
  );
}

All pages are built as SSG. If we open some page, HTML loads as it supposed to, we immediately see the content, no CLS. The problem appears if we NAVIGATE to this page using built-in <Link /> component. We don’t see any HTML for "use client" components then and we suffer from noticeable CLS as dynamic components start actually to load dynamically.

Opened directly (looks good): 


direct.mov

Internal navigation (CLS):

navigated.mov

So again: we can directly open the SSG generated page and see the content, but we CAN’T navigate to this page through the app without seeing how it dynamically loads. And this applies only to the ‘use client’ components. RSC is good.
“funny” thing is that the workaround would be to change <Link /> to <a /> tag. Full redirect with reloading of html makes it look much better and smoother, ha.
This behaviour seems wrong. Or do we miss some other way of utilizing JAMStack in Next.js.

huozhi added a commit that referenced this issue Apr 30, 2024
### What

Removing the Suspense boundary on top of `next/dynamic` by default, make
it as `React.lazy` component with preloading CSS feature.

### Why

Extra Suspense boundary is causing extra useless rendering. For SSR, it
shouldn't render `loading` by default

Related: #64060
Related: #64687
Closes
[NEXT-3074](https://linear.app/vercel/issue/NEXT-3074/app-router-content-flickering-with-reactcreatecontext-and-nextdynamic)

This is sort of a breaking change, since removing the Suspense boundary
on top of `next/dynamic` by default. If there's error happening in side
the dynamic component you need to wrap an extra Suspense boundary on top
of it
@huozhi
Copy link
Member

huozhi commented May 7, 2024

In the repro seems there's a setState call in React context Provider when on mount, which will trigger the re-render of Suspense boundary in next/dynamic that lead to the flicking you saw. We recommend to remove the setState in the context provider.
The reproduction demostrates the issue well but I wonder what's the real-world case as trigger re-render in context Provider will trigger all the re-render in the Suspense boundaries below.

@KirillEvtushenko
Copy link
Author

KirillEvtushenko commented May 8, 2024

Hi! The real-world use case is loading some initial data from localStorage and setting it as a state on mount. We need it, unfortunately.

Please take a look at my previous comment as well. The fix you had initially pushed, fixed both of our issues (I tested it on a canary version). Both flickering on mount and flickering on redirect. I know you had the reason to revert it, I just want to stress out that we can't finish our migration to the App router in a number of JAMStack apps. Using "registry with dynamic imports" approach proved well on Pages router and we want to keep it in the App router.
If you want to mark this behaviour as "intended", I would like to ask you to help us find any alternatives to "next/dynamic" as we weren't able to achieve the same level of tree shaking with static imports.
I hope my previous message describes our use cases well. Thanks!

@snehakhobragade90
Copy link

snehakhobragade90 commented May 8, 2024

@huozhi Removing of extra suspense boundary fixed the loading state to flash before rendering the next/dynamic - #64687
Without this fix we are completely blocked to proceed with our app migration. For the initial launch we ended up statically importing the components which are above the pageFold to mask CWV degradation, but we cannot continue doing this as we cannot treeshake this and it abundantly increases the app size.

If you want to mark this behaviour as "intended", I would like to ask you to help us find any alternatives to "next/dynamic" as we weren't able to achieve the same level of tree shaking with static imports.

💯 💯 💯
I also wanted to add, in my sandbox example I have created a minimal code to reproduce the bug. The setState call is not in any React contextProvider which you have mentioned in the comment above. But instead in a simple react component which renders a next/dynamic imports. I have more details in added in the issue if it helps to debug it better. Thanks.

@huozhi
Copy link
Member

huozhi commented May 10, 2024

Hi @snehakhobragade90 the setState in provider was the original reproduction of this issue. In your repro there's a setState call above the next/dynamic component which will also trigger the Suspense re-rendering

useEffect(() => {
    setsomeState("bar");
  });

@huozhi
Copy link
Member

huozhi commented May 10, 2024

@KirillEvtushenko I patched an improvement in #65486 that will preload the chunks earlier, and seemed to work for me with the initial reproduction I downloaded before. Could you help verify with latest canary again to see if it helps since I don't have the source code of the demo from the video?

Also regarding to the read of localStorage, is that possible to finish in callback of useState like useState(() => { .. }) so you don't need to set it again to trigger the re-render?

@nmiddendorff
Copy link

nmiddendorff commented May 10, 2024

Hi @snehakhobragade90 the setState in provider was the original reproduction of this issue. In your repro there's a setState call above the next/dynamic component which will also trigger the Suspense re-rendering

@huozhi, that's correct. Calling useState directly in the parent OR setState in a provider will trigger next/dynamic's baked-in suspense boundary. I don't know if it's realistic to recommend that parents and siblings to a dynamic component should not call useState or setState. The baked in suspense boundary does have me looking at using React.lazy() instead to avoid the unwanted suspense boundary but as @joshwcomeau mentioned this reintroduces the style flashing issue #62940 that was patched for next/dynamic. Is a potential path forward to apply a similar fix to React.lazy so CSS is collected before rendering client side?

I have a sandbox that demonstrates the React.lazy() + CSS Module flashing issue.

@KirillEvtushenko
Copy link
Author

@KirillEvtushenko I patched an improvement in #65486 that will preload the chunks earlier, and seemed to work for me with the initial reproduction I downloaded before. Could you help verify with latest canary again to see if it helps since I don't have the source code of the demo from the video?

Also regarding to the read of localStorage, is that possible to finish in callback of useState like useState(() => { .. }) so you don't need to set it again to trigger the re-render?

@huozhi, tested on 14.3.0-canary.57. Both issues (on direct opening and on redirect) are still there, unfortunately. I clearly see that waterfall improved and components appear faster, but it's still visually visible. And thats for the direct opening process.

As for redirect to the page using Link component, dynamic loading is still consequent:

Screen.Recording.2024-05-11.at.11.56.26.mov

@huozhi
Copy link
Member

huozhi commented May 11, 2024

@KirillEvtushenko could you share the codebase of this video to let us dig into it? 🙏

@KirillEvtushenko
Copy link
Author

Of course! Updated the repo and readme.
https://github.com/KirillEvtushenko/app-router-dynamic-imports-react-context-issue

@snehakhobragade90
Copy link

@huozhi quick proposal, thoughts if we want to add the extra suspense as a conditional element ? We can probably use the suspense prop (which I see was deprecated, or we can introduce a new prop to handle this logic)
The default behavior can be to render with the Suspense, but the consumers can opt out of it if required. Thoughts ?

@snehakhobragade90 snehakhobragade90 linked a pull request May 14, 2024 that will close this issue
@brian123zx
Copy link

One more repro case to pile on here. When the initial fix PR was reverted, I dug into the history of next/dynamic and saw others reporting similar issues as us on the PR which introduced its current implementation. The suggested fix was to wrap state changes in startTransition. One response to that in the thread suggests a problem when the state updates are within a 3rd party hook out of your control. We're now running into exactly this, so I've set up an isolated repro here to illustrate the problem.

@navin-moorthy
Copy link

navin-moorthy commented May 16, 2024

Might be related to this issues - #65796

Sandbox - https://codesandbox.io/p/github/navin-moorthy/next.js-layout-shift-from-14.2.2/main?checkout=true&workspaceId=51b6ee02-0790-44a1-b61c-2e7846c65016

The flickering can be seen by doing the below steps, -

  1. Go to HomePage /
  2. Go to AboutPage /about
  3. Hard refresh
  4. Go to Homepage '`'
  5. To reproduce again, Hard refresh
  6. Go to AboutPage /about

@snehakhobragade90
Copy link

@huozhi are there any updates on this ? We are completely blocked to launch our Nextjs project due to the lack of treeshaking support coupled with these unresolved issues. Any updates would be helpful to plan next steps at our end. Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template. Lazy Loading Related to Next.js Lazy Loading (e.g., `next/dynamic` or `React.lazy`). linear: next Confirmed issue that is tracked by the Next.js team.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants