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

Server side dynamic imports will not split client modules in multiple chunks #54935

Open
1 task done
leo-cheron opened this issue Sep 2, 2023 · 8 comments
Open
1 task done
Labels
bug Issue was opened via the bug report template.

Comments

@leo-cheron
Copy link

leo-cheron commented Sep 2, 2023

Link to the code that reproduces this issue or a replay of the bug

https://github.com/leo-cheron/nextjs-issue-dynamic

To Reproduce

Dynamically multiple server of client components from a server page / component:

import dynamic from 'next/dynamic'

const ServerComponentA = dynamic(() => import('./ServerComponentA'))
const ServerComponentB = dynamic(() => import('./ServerComponentB'))

export default () => {
	return (
		<div>
			<ServerComponentA />
			<ServerComponentB />
		</div>
	)
}

Where ServerComponentA & B will import respectively ClientComponentA & ClientComponentB like below:

import ClientComponentA from './ClientComponentA'

export default () => {
	return <ClientComponentA />
}

ClientComponentA being just a large SVG:

'use client'

export default () => {
	return <svg>[...]</svg>
}

Current vs. Expected behavior

According to the documentation, If you dynamically import a Server Component, only the Client Components that are children of the Server Component will be lazy-loaded - not the Server Component itself.

By running bundle analyze, we see that both ClientComponentA & ClientComponentB are added to same page chunk, where we'd expect them to be split in two lazy loaded separated chunks. This issue defeats the purpose of dynamic loading and will prevent any client module from being loaded on demand.

I also tried to dynamically import ClientComponentA from ServerComponentA without success. Chunk splitting would only work when dynamic import is used from a client component (which we don't want here).

image

Build prod demo can be found here

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.6.0: Wed Jul  5 22:22:05 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T6000
    Binaries:
      Node: 19.8.1
      npm: 9.5.1
      Yarn: 1.22.17
      pnpm: 8.7.0
    Relevant Packages:
      next: 13.4.20-canary.15
      eslint-config-next: N/A
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 5.2.2
    Next.js Config:
      output: N/A

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

App Router

Additional context

Any production build is concerned.

@leo-cheron leo-cheron added the bug Issue was opened via the bug report template. label Sep 2, 2023
@leo-cheron leo-cheron changed the title Dynamic import from server module will not be split client modules in multiple chunks Dynamic import from server module will not split client modules in multiple chunks Sep 3, 2023
@leo-cheron leo-cheron changed the title Dynamic import from server module will not split client modules in multiple chunks Server side Dynamic imports will not split client modules in multiple chunks Sep 4, 2023
@leo-cheron leo-cheron changed the title Server side Dynamic imports will not split client modules in multiple chunks Server side dynamic imports will not split client modules in multiple chunks Sep 4, 2023
@alex-krasikau
Copy link

Duplicate of the - #49454

@poorscousertommy8
Copy link

Expected behavior: When server components are imported via next/dynamic, shouldn't the components be split into chunks on the server? At least the CSS?

The issue is that the CSS of all components is bundled into a single CSS file. This naturally leads to performance losses in PageSpeed Insights, especially in larger projects.

page.js:

import dynamic from 'next/dynamic';

const Page1 = dynamic(() => import('@/components/Page1'));
const Page2 = dynamic(() => import('@/components/Page2'));

const Page = ({ params: { pagename: pagenameArr } }) => {
  const pagename = pagenameArr?.[0];

  if (pagename === 'page1') return <Page1 />;
  if (pagename === 'page2') return <Page2 />;
};

export default Page;

component Page1:

import styles from "./Page1.module.css";

const Page1 = () => {
  return <div className={styles.page1}>Page 1</div>;
};

export default Page1;

Page1.module.css

.page1 {
  background-color: #00ffff;
}

component Page2:

import styles from "./Page2.module.css";

const Page2 = () => {
  return <div className={styles.page2}>Page 2</div>;
};

export default Page2;

Page2.module.css

.page1 {
  background-color: #ff00ee;
}

When I open Page1 in the browser, a CSS file (efe31bd8f307aac7.css) is loaded that contains styling from both components:

.Page2_page2__PYcEf{background-color:#f0e}
.Page1_page1__9tvyj{background-color:#0ff}

@JohnRPB
Copy link

JohnRPB commented Dec 20, 2023

I've been experiencing a similar issue and read the duplicate, but it was closed, and nothing was done about it. It makes it difficult to understand how to code-split in NextJS 14.

The claim was that it would be "automatic" and that any client component imported inside a server component that suspends and streams progressively would not increase FirstLoadJS, but I'm finding that this isn't true at all.

Instead, all client code is sent to the browser at the same time, regardless of whether it renders initially, unless you code-split inside a client component, which introduces an unnecessary round-trip. Nothing you can do on the server, whether using lazy() or dynamic() (with ssr: false or true), can stop this.

@poorscousertommy8
Copy link

Our biggest problem is not necessarily the splitting of JS files (that works as long as you integrate another component that then loads again via next/dynamic), but rather CSS files that become far too large and cannot be split. It would be sensational if an intelligent solution were sought for this.

@BleddP
Copy link

BleddP commented Jan 16, 2024

I am experiencing exactly the same problem.

Using Next 14 (canary), I have a catch-all route in /src/app/[...slug] as we are using a CMS where we render a page based on the slug. I've already wrapped my dynamic components in a client component, as mentioned in duplicate #49454, but the CSS is just being bundled in a few big CSS files.

This means that dedicated client-components (such as form inputs, checkboxes, modals, etc), which are dynamically imported from a client wrapper, still have their CSS bundled into the big CSS files, even though those components are not used on the page at all. Next.js is supposed to be all about performance, but how can you benefit from reduced client-side javascript if you have a huge render-blocking CSS destroying your performance metrics.

@poorscousertommy8
Copy link

Progress on this topic would be great. I think there must be the option to split CSS files (rendering blocking). We have big losses in Pagespeed Insights because of this.

@BleddP
Copy link

BleddP commented Feb 6, 2024

So I've done a bit more research on this, and managed to get the CSS and JS code-splitting working, but only when used in very specific circumstances.

First and foremost, I have realised that if you have a /components folder and in that folder an /index.ts where you export your components (for example export * from './Button', so you can use import { Button } from '@components', code splitting does not work. Maybe this is by design, as the compiler is not sure which components you might need from the components dir and therefore just includes them all? This was a bit of a bummer, because organising your code like this works really well in larger projects.

So the JS file and CSS is lazy loaded, only if the following conditions are met:

  1. You are using a dynamic import from within a client component ('use client') - So essentially you are going to end up creating endless extra client wrappers to lazy load components, described as a 'solution' in React lazy or next dynamic don't reduce an app route's "First Load JS" #49454
  2. You are importing from the exact file (i.e. import { Button } from '@components/Button/Button'. , you cannot import from something like an index.ts in your components folder
  3. You are not importing this component anywhere else in your page in server component, even if it's loaded conditionally

In relation to point 3, I've found that if you'd have a server component like this:

const TextinputWithHeavyAnimationLib = dynamic(() => import('./path/to/lib))

const FormWrapper = ({ showForm }) => {
 return (
    <div>
      {showForm && <TextinputWithHeavyAnimationLib /> }
    </div>
  )
}

Then it just loads the Textinput component, including CSS which will be bundled in the main CSS file.
You have to wrap the Textinput component with another client wrapper, which then in turn imports the actual component.

Tbh it's a real pain to have to create these workarounds, but if performance is absolutely paramount for your project then I hope these steps might help someone who's stuck like I was.

@chahatbahl
Copy link

@leerob any update here?
Are we planning to do anything to solve the code-splitting issue on server-side components inside the APP Directory?

Also anything on an option to split CSS files (rendering blocking)

jahirfiquitiva added a commit to jahirfiquitiva/jahir.dev that referenced this issue Mar 14, 2024
dynamic only works properly in client components. More info: vercel/next.js#54935 (comment)
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.
Projects
None yet
Development

No branches or pull requests

7 participants
@leo-cheron @JohnRPB @chahatbahl @poorscousertommy8 @BleddP @alex-krasikau and others