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

Client bundle contains unnecessary components with [[...slug]] catch-all route #74756

Open
1 task done
dia-loghmari opened this issue Jan 10, 2025 · 1 comment
Open
1 task done
Labels
bug Issue was opened via the bug report template.

Comments

@dia-loghmari
Copy link

Verify canary release

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

Provide environment information

Node.js v20.18.1

Operating System:
  Platform: win32
  Arch: x64
  Version: Windows 10 Enterprise
  Available memory (MB): 32440
  Available CPU cores: 20
Binaries:
  Node: 20.18.1
  npm: 10.8.2
  Yarn: 1.22.21
  pnpm: N/A
Relevant Packages:
  next: 15.1.4 // Latest available version is detected (15.1.4).
  eslint-config-next: 15.1.4
  react: 19.0.0
  react-dom: 19.0.0
  typescript: 5.7.3
Next.js Config:
  output: N/A

Which example does this report relate to?

https://github.com/dia-loghmari/next-issue-demo

What browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

Vercel

Describe the Bug

When using a single [[...slug]] catch-all route in the src/app directory, the client bundle contains all imported components, even if they are not used on a specific route.

For instance:

  • URL /about uses the Carousel component.
  • URL /contact uses the Accordion component.

However, when navigating to /about, the client bundle includes JavaScript for both Carousel and Accordion, leading to larger-than-necessary client bundles.

Screenshots

Capture d’écran 2025-01-10 115642

System Info

  • Next.js Version: 15 (same for 14)
  • Node.js Version: 20lts

Additional Context

The dynamic imports are wrapped with dynamic and ssr: true, but the generated client bundle still includes all unused components. This could indicate that the [[...slug]] route is not optimized for selective client-side bundling.

Expected Behavior

The client bundle for each route should only include JavaScript for the components used on that specific route. For example:

  • /about should include Carousel only.
  • /contact should include Accordion only.

To Reproduce

I’ve prepared a minimal reproduction example. Here's the key setup:

  1. A catch-all route src/app/[[...slug]]/page.tsx.
  2. Dynamically resolved components using the following:
// resolveComponent.tsx
import React from "react";
import dynamic from "next/dynamic";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const componentMap: Record<string, React.ComponentType<any>> = {
  Header: dynamic(() => import("./components/Header"), { ssr: true }),
  Carousel: dynamic(() => import("./components/Carousel"), { ssr: true }),
  Accordion: dynamic(() => import("./components/AccordionDemo"), { ssr: true }),
};

export default function resolveComponent(name: string) {
  return componentMap[name] || null;
}
  1. The page setup:
// [[...slug]]/page.tsx
import React from "react";
import { allPages } from "../all-pages";
import resolveComponent from "../resolveComponent";

type PageProps = {
  params: Promise<{ slug?: string[] | string }>;
};

export async function generateStaticParams() {
  return allPages.map((onePage) => ({
    slug: Array.isArray(onePage.slug) ? onePage.slug : [onePage.slug],
  }));
}

export default async function Page({ params }: PageProps) {
  const slug = (await params).slug || ["/"];
  const page = allPages.find((p) =>
    p.slug === (Array.isArray(slug) ? slug.join("/") : slug)
  );
  if (!page) {
    return <div>Page not found</div>;
  }
  return (
    <div>
      <h1>{page.title}</h1>
      {page.components.map((component, index) => {
        const Component = resolveComponent(component.name);
        return Component ? <Component key={index} {...component.props} /> : null;
      })}
    </div>
  );
}

The file that contains the definition of each page.

export type PageParams = {
  slug: string|Array<string>;
  title: string;
  components: Array<{
    name: string;
    props: Record<string, unknown>;
  }>;
};

export const allPages: PageParams[] = [
  {
    slug: '/',
    title: "Home",
    components: [
      {
        name: "Header",
        props: {},
      },
    ],
  },
  {
    slug: "about",
    title: "About",
    components: [
      {
        name: "Header",
        props: {},
      },
      {
        name: "Carousel",
        props: {
          title: "this is a carousel",
        },
      },
    ],
  },
  {
    slug: "contact",
    title: "Contact",
    components: [
      {
        name: "Header",
        props: {},
      },
      {
        name: "Accordion",
        props: {
          title: "this is a an accordion",
        },
      },
    ],
  },

];
  1. Generated static/chunk/...js bundle (as shown in Webpack Bundle Analyzer) contains JavaScript for all components, regardless of the route being accessed.

Here is a link to a minimal repository that demonstrates the issue: https://github.com/dia-loghmari/next-issue-demo

  • npm run build (webpack analyzer will be executed)
  • go to any page and open coverage tool in devtool to see how much unsued js is loaded
    Capture d’écran 2025-01-10 120506
@dia-loghmari dia-loghmari added the examples Issue was opened via the examples template. label Jan 10, 2025
@samcx samcx added bug Issue was opened via the bug report template. and removed examples Issue was opened via the examples template. labels Jan 13, 2025
@dia-loghmari
Copy link
Author

dia-loghmari commented Jan 15, 2025

Hello, I got this response from Customer Success Engineer at Vercel:

The Engineering team investigated the issue and confirmed that you can resolve it by moving the page into a Client boundary by adding use client to the top, or rather just the part contains the dynamic import calls. Essentially, you should move the routing of route to client component in the catch-all route into a Client boundary. Note that import() has no effect on the server.
We understand that docs aren't very clear about that so we'll update https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading to make it clear what "Lazy loading applies to Client Components." means.

Thank you team for this response. It may help some users here.

That means you can add use client to the component mapping:

// componentMap.tsx
"use client";  // <- here
import dynamic from "next/dynamic";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const componentMap: Record<string, React.ComponentType<any>> = {
  Header: dynamic(() => import("../components/Header"), { ssr: true }),
  Carousel: dynamic(() => import("../components/Carousel"), { ssr: true }),
  Accordion: dynamic(() => import("../components/AccordionDemo"), {
    ssr: true,
  }),
};

or you can you can add use client directive to the src\app\[[...slug]]\page.tsx file.

Unfortunately, this only applies to client routes. It doesn't work with RSC components. I mean, it's not possible to nest client and server components in the tree. 😥

To illustrate two use cases that limit us, I've created this sample on github. With a RSC component (the Accordion) In the first I added 'use client' to the page.tsx and in the second branch I added 'use client' to the component mapping (componentMap.tsx).

First use case: https://github.com/dia-loghmari/next-issue-demo/tree/feature/client-page
Second use case: https://github.com/dia-loghmari/next-issue-demo/tree/feature/client-component-map

Please see the readme.md.

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

2 participants