vercel / vercel/next.js

dynamic() ignores <Suspense> hierarchy resulting in layout flicker

Open
#73,830 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://codesandbox.io/p/devbox/determined-firefly-9dq24w

To Reproduce

Open codesandbox on main page and observe the flicker

Current vs. Expected behavior

My app suffers from a rare (when on prod-optimized build) race condition, which results in layout flicker when navigating between pages.
It got more intense as I moved more pages out of SSR to improve performance, using dynamic.
I recalled an earlier issue in which the Router could make a full browser page refresh under some conditions resulting in a white-page flicker, but looks like that issue is long gone (with some NextJS update) and I wasn't able to race it that way.

With Chrome frame-by-frame recording, I finally raced it and seen that the flickered frame contains just the <Footer element without the webpage content.

I wrapped my whole layout code with <Suspend, but the issue remained

So the layout code was

const Layout: FC<AppProps> = ({ Component, pageProps }) => {
  return (
    <Suspense>
      <div>
        <Header />
        <Component {...pageProps} />
        <Footer />
      </div>
    </Suspense>
  );
};
export default Layout;

Why?
NextJS uses <Suspend in dynamic(), but it will always force fallback to be either a provided element, or it's own element which returns empty content when Suspended.
That means React can't see the parent <Suspend which only works when the child has fallback=null, which would avoid rendering incomplete component with just <Footer inside.

I don't see rendering "empty" as a default fallback making sense, IMO it would make sense to use no fallback= at all and refer the error handling, which is part of current "fallback" function, to the ErrorBoundary, and otherwise just properly Suspense.

Alternatively, I tried forcing loading to undefined which should override it properly, but there's another part of code that expects it to be valid and results in crash(it switches the implementation file between server and client, or something):

https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/loadable.shared-runtime.tsx#L142

EDIT: I double checked and what I wrote about <Suspend fallback={null} is not right because it seems have no special mechanics compared to a fallback function that itself returns a null component. So in that case, mechanics of lazy components seem enough without <Suspend, and if the crash when loading: undefined is fixed, and logic for hasSuspenseBoundary is extended to avoid forcing it without SSR, it can be used to workaround this problem.

https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/lazy-dynamic/loadable.tsx#L52C11-L52C30

Provide environment information
"next": "15.1.1-canary.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
Which area(s) are affected? (Select all that apply)

Not sure

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

next dev (local), next start (local)

Additional context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the layout flicker with the linked CodeSandbox, then read packages/next/src/shared/lib/loadable.shared-runtime.tsx around line 142 and packages/next/src/shared/lib/lazy-dynamic/loadable.tsx around line 52. Trace how dynamic() handles loading, Suspense, and SSR during navigation. Done means the reproduction no longer shows an incomplete layout or crash while preserving the existing dynamic-loading behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, next.js, react
Domain
frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.