next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Dynamic Import of Client Component from Server Component Not Code Split

Open ajwootto opened this issue 1 year ago • 12 comments

Link to the code that reproduces this issue

https://github.com/ajwootto/nextjs-dynamic-loading-repro

To Reproduce

  1. yarn and start the repro server with yarn dev
  2. observe that the "DynamicClientComponent" is only ever dynamically imported using the Next.js dynamic helper, and is never rendered.
  3. Visit the root page on localhost and check the network tab
  4. view the contents of page.js
  5. Observe that the implementation code for DynamicClientComponent is present in that bundle. Search for the words "Dynamic Component That Should Not Be Here" to see it.

Another case using Suspense

  1. Navigate to /suspend
  2. Observe that there's a suspense fallback rendered which is then replaced with the dynamically loaded client component
  3. Check the initial page.js file again for the same string. It is present in that bundle still.

Current vs. Expected behavior

Current: A client component that is dynamically imported by a server component is included in the initial client bundle, regardless of whether it is rendered or not during server rendering. Even if it is rendered inside a Suspense boundary it is still included in the initial bundle.

Expected: The client bundle should not contain any client component code which is dynamically imported but not rendered. It should also not include any client component code that is dynamically imported and rendered inside a Suspense boundary from a server component. The implementation of that component should be streamed later to the client.

The repro example is the simplest possible case where the imported component is never called no matter what, but I would also expect something like this to work:

const ConditionalComponent = dynamic(() => import('./DynamicClientComponent'))

export default function Home() {
    // some arbitrary condition which could be determined dynamically (think feature flags etc.)
    const shouldRender = false
    // ConditionalComponent should not be sent since on this server render pass it was never rendered
    return  shouldRender && <ConditionalComponent/>
}

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 23.2.0: Wed Nov 15 21:53:18 PST 2023; root:xnu-10002.61.3~2/RELEASE_ARM64_T6000
Binaries:
  Node: 20.10.0
  npm: 10.2.3
  Yarn: 3.6.2
  pnpm: 8.6.12
Relevant Packages:
  next: 14.1.1-canary.7 // Latest available version is detected (14.1.1-canary.7).
  eslint-config-next: N/A
  react: 18.2.0
  react-dom: 18.2.0
  typescript: 5.1.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), next start (local)

Additional context

Not sure if this is just a docs or understanding issue, or whether what I think should be the behaviour here is impossible, but it really seems intuitive to me that a dynamic import should not be included in the initial client bundle if its not called during render.

ajwootto avatar Jan 24 '24 04:01 ajwootto