vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.87k stars 26.97k forks source link

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

Open leo-cheron opened 1 year ago

leo-cheron commented 1 year ago

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

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.

alex-krasikau commented 1 year ago

Duplicate of the - https://github.com/vercel/next.js/issues/49454

poorscousertommy8 commented 1 year ago

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 commented 10 months ago

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 commented 10 months ago

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 commented 9 months ago

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 https://github.com/vercel/next.js/issues/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 commented 9 months ago

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 commented 9 months ago

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 https://github.com/vercel/next.js/issues/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 commented 8 months ago

@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)