Logic behind how Next.js builds dynamic pages using the app router system?

Background: dynamic pages

I have some large pages in my Next.js app currently (app router style), and am trying to debug why the pages are so large, and it’s hard to say the least.

Route (app)                               Size     First Load JS
┌ ○ /                                     3.29 kB         846 kB
├ ○ /_not-found                           142 B          86.5 kB
├ ○ /my/first/page                        1.56 kB         414 kB
├ λ /my/dynamic/page/[x]                  57.8 kB         636 kB
├ λ /my/other/page                        326 kB         1.32 MB
...
+ First Load JS shared by all             86.3 kB
  ├ chunks/4422-2c92fefc06002e18.js       30.3 kB
  ├ chunks/65b6c8ea-4ad97acde17233b9.js   53.6 kB
  └ other shared chunks (total)           2.38 kB

One of my pages, the /my/dynamic/page/[x], has this in it:

import dynamic from 'next/dynamic'

const ACTUAL_PAGE = {
  a: dynamic(() => import("~/components/page/A")),
  b: dynamic(() => import("~/components/page/B")),
  c: dynamic(() => import("~/components/page/C")),
  d: dynamic(() => import("~/components/page/D")),
  // .. up to t, 20 different styles of pages, under the same URL.
};

export default function Page({ params }) {
  const ActualPage = ACTUAL_PAGE[params.x]
  return <ActualPage />
}

Sidenote: URL structure

I need this because I have these routes:

/convert/ttf/otf
/convert/png/jpg
/convert/rgb/hex
/convert/other/stuff

So I have a FontConverter page, ImageConverter, etc., 20 different types of converters, using the same URL system. I could do this:

/convert/font/ttf/otf
/convert/image/png/jpg
/convert/color/rgb/hex
/convert/some/other/stuff

But I don’t want to do that… I like the simplicity of the URL structure I have.

Observation: dynamic subpages add to build size

When I comment out all but 1 ACTUAL_PAGE from above, it goes down to 400kb (still have more to figure out why my pages are so large). Each page I comment back in, it goes up by roughly 80kb:

  • 1 page: 400kb
  • 2 pages: 480kb
  • 3 pages: 560kb
  • 4 pages: 640kb

Assumption: dynamic pages aren’t included in the build?

I assumed that all my dynamic pages weren’t included in the build? But obviously they are somehow… Because commenting one in at a time slowly increases the final First Load JS in the top λ /my/dynamic/page/[x] log.

Question

How does dynamic work with the output bundles? How does it change First Load JS? Also, what exactly is the difference between Size and First Load JS? (I haven’t seen that in the docs yet).

I noticed that using const Component = await import("~/components/page/A"); in a switch statement, like:

switch (params.x) {
  case "a": {
    const Component = (await import("~/components/page/A")).default;
    return <Component />;
  }
  case "b": {
    const Component = (await import("~/components/page/B")).default;
    return <Component />;
  }
  // ...
}

Also resulted in the same behavior, of First Load JS incrementing every page I added. So somehow the build/bundle is including my sub-imports intelligently.

How does it work?