mantine icon indicating copy to clipboard operation
mantine copied to clipboard

Cannot Modularize Imports for Next.js

Open anthonyalayo opened this issue 2 years ago • 23 comments

What package has an issue

@mantine/core

Describe the bug

Hey there,

With Next.js >= 13.1, you can make use of their Modularize Imports feature.

Right now there is an open PR on material-ui mentioning it: https://github.com/mui/material-ui/pull/35457/files

Could mantine support that as well? I attempted it, and it fails compilation at import { useMantineTheme } from '@mantine/core';

What version of @mantine/hooks page do you have in package.json?

N/A

If possible, please include a link to a codesandbox with the reproduced problem

No response

Do you know how to fix the issue

None

Are you willing to participate in fixing this issue and create a pull request with the fix

None

Possible fix

No response

anthonyalayo avatar Jan 10 '23 20:01 anthonyalayo

Why would you need that? Tree shaking should remove all unused code.

rtivital avatar Jan 10 '23 20:01 rtivital

@rtivital my understanding is that with SWC (the new default in Next.js), there is no tree shaking in development mode: https://github.com/vercel/next.js/discussions/17977#discussioncomment-3939189

anthonyalayo avatar Jan 10 '23 20:01 anthonyalayo

So, why do you need tree shaking in development mode?

rtivital avatar Jan 10 '23 21:01 rtivital

@rtivital in order to increase page load times, since they can become very slow. @tabler/icons, the dependency for mantine, is also in the middle of it's v2 branch for the same support: https://github.com/tabler/tabler-icons/issues/359

anthonyalayo avatar Jan 10 '23 21:01 anthonyalayo

Do you experience any performance issues?

rtivital avatar Jan 10 '23 21:01 rtivital

@rtivital I am fiddling with a setup forked from https://github.com/mantinedev/mantine-next-template but with upgraded dependencies (including Next >= 13), and it is slow on startup and initial page loads. The latency could be solely coming from @tabler/icons, but I don't know if there's a mechanism to easily tell (I couldn't find one at least). But I would assume @mantine/core is suffering from the same issue, considering @mui added support for it.

anthonyalayo avatar Jan 10 '23 21:01 anthonyalayo

@tabler/icons is not a dependency of @mantine/core, if you do not use @mantine/tiptap you can uninstall it.

Can provide some measurable metrics on performance?

rtivital avatar Jan 10 '23 21:01 rtivital

@tabler/icons is not a dependency of @mantine/core, if you do not use @mantine/tiptap you can uninstall it.

Figured I might have used the wrong choice of words there, "recommended via documentation" is more accurate 😄

Can provide some measurable metrics on performance?

After digging through several threads on the issue, I don't see an obvious way to get performance metrics. Next.js only tells you X modules were loaded, but it doesn't tell you which modules were loaded.

I will run an experiment where I only import a single mantine component from a single page and see if I can get a convincing delta and report back.

anthonyalayo avatar Jan 10 '23 21:01 anthonyalayo

@rtivital I made a demo app using create-react-app to profile this.

Here's the demo app:

function App() {
  return (
      <MantineProvider withGlobalStyles withNormalizeCSS>
          <Button>Click me!</Button>
      </MantineProvider>
  );
}

export default App;

I will showcase the modules loaded by webpack in each import scenario.

Scenario 1: Recommended by docs

import { MantineProvider } from '@mantine/core';
import { Button } from '@mantine/core';
orphan modules 1.06 MiB [orphan] 454 modules
runtime modules 28.5 KiB 14 modules

Scenario 2: Reaching into the ESM directory manually

import { MantineProvider } from '@mantine/styles/esm/theme/MantineProvider';
import { Button } from '@mantine/core/esm/Button/Button';
orphan modules 12.1 KiB [orphan] 20 modules
runtime modules 28.5 KiB 14 modules

Notice 454 modules were loaded with the current approach, and only 20 modules were loaded with the desired approach.

While tree shaking will occur during the minification process, webpack still needs to traverse all of these modules that get imported. If @mantinedev's build can be restructured so that:

  1. "module": "esm/index.js", is changed into "module": "index.js",
  2. All top level imports can be retrieved using path imports

Then transformations like Modularize Imports or babel-plugin-import will work, and we could import as we currently are:

import { Button, MantineProvider } from '@mantine/core';

while still only pulling in 20 modules.

anthonyalayo avatar Jan 17 '23 05:01 anthonyalayo

Just an update here that @tabler/tabler-icons just merged their support for this as well: https://github.com/tabler/tabler-icons/issues/359

anthonyalayo avatar Jan 26 '23 01:01 anthonyalayo

Do you experience any performance issues?

Yes, I have experienced performance issues with Mantine in development (with Next.js) because it bundles all of Mantine instead of only using what's imported. This causes significantly slower dev-mode page load times, even for very simple pages. There are no issues in production as Next.js does tree shaking there, but it doesn't do any tree shaking in development mode.

For comparison, antd does allow modullarizeImports for most components with something like this:

modularizeImports: {
  antd: {
    transform: "antd/lib/{{lowerCase member}}",
  }
}

mgreenw avatar Feb 26 '23 19:02 mgreenw

Disclaimer: I haven't worked with SWC yet, so please take the following with a pinch of salt.

I'm using esbuild and it treeshakes Mantine just fine. And it "only" treeshakes on module level so unused modules will be discarded but unused exports within modules not (if I understand it correctly). And the build performance for dev/production is excellent.

So if I understand it correctly these Next.js transform rules require the package to follow some specific file convention that the user can configure. Therefore the internal package/export structure becomes part of the public API. Is this really good?

What exactly do you require? As I see it Mantine already has a logicial file structure that may be described with such a configuration. Where does it fall short? On every module that exports multiple things? On every export that doesn't match the filename? Or because it doesn't use default exports?

cyantree avatar Feb 26 '23 21:02 cyantree

@cyantree

I'm using esbuild and it treeshakes Mantine just fine. And it "only" treeshakes on module level so unused modules will be discarded but unused exports within modules not (if I understand it correctly). And the build performance for dev/production is excellent.

Treeshaking vs module loading is different. Everyone agrees here that treeshaking is working, but all modules are being loaded during development. That is what we are looking to fix.

So if I understand it correctly these Next.js transform rules require the package to follow some specific file convention that the user can configure. Therefore the internal package/export structure becomes part of the public API. Is this really good?

We are already using the public API by using the components the library publicizes. The only request is that the public API becomes consistent in the way that the file layout is structured so that we don't need to load all modules.

What exactly do you require?

I posted this in the issue description:

Could mantine support that as well? I attempted it, and it fails compilation at import { useMantineTheme } from > '@mantine/core';

anthonyalayo avatar Feb 26 '23 22:02 anthonyalayo

Thanks for the clarification @anthonyalayo .

We are already using the public API by using the components the library publicizes. The only request is that the public API becomes consistent in the way that the file layout is structured so that we don't need to load all modules.

IMHO that's two different things.

  1. You're using the public API described in the docs and made public by the root exports from each package. That's the expected way to use the library.
  2. You want to access the same things via sub imports that follow a specific schema. So:
  • import {Button} from '@mantine/core'; may become import Button from '@mantine/core/Button';
  • import {useMantineTheme} from '@mantine/core'; may become import useMantineTheme from '@mantine/core/useMantineTheme';
  • As I read it using default exports isn't mandatory, so it might be possible to stick with named exports if wanted.

Is my understanding correct? So to get this working and stable the file structure must become part of the public API - in my understanding it's currently not part of it. And it must be consistent with the export namings.


Another idea: Because next.config.js is a JS file and not a JSON (or might it be a JSON file?) it may also be possible to utilize some require statements and fetch the transformation configuration directly from the package:

module.exports = {
  modularizeImports: {
    ...require('@mantine/core/nextJsModularizeImports.js'),
  },
}

This opens up the possibility to keep the file structure out of the public API.


And what about using package.json exports? https://webpack.js.org/guides/package-exports/ https://nodejs.org/api/packages.html#subpath-exports

Is this already usable? For me this looks like the better and more standardized approach.

cyantree avatar Feb 27 '23 22:02 cyantree

We are really suffering from this issue.

Screenshot 2023-05-04 at 13 44 08

soylemezali42 avatar May 04 '23 10:05 soylemezali42

I'll put my experience here too: today it is not much about "dev" speed and module loading, but also about "React Server Components" experience, because when I'll use @mantine/core, I'll get a lot of stuff where Next.js complains that I should use "use client"; even when it's not necessary: image

Just because of emotion cache from Mantine I've to put "use client"; in a place where it does not make much sense...

marek-hanzal avatar Jun 05 '23 15:06 marek-hanzal

Here is my config:

modularizeImports: {
      "@tabler/icons-react": {
        transform: "@tabler/icons-react/dist/esm/icons/{{member}}",
      },
      "@mantine/core": {
        transform: "@mantine/core/esm/{{member}}/{{member}}.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/AppShell": {
        transform: "@mantine/core/esm/AppShell/{{member}}/{{member}}.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Grid": {
        transform: "@mantine/core/esm/Grid/{{member}}/{{member}}.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Timeline": {
        transform: "@mantine/core/esm/Timeline/{{member}}/{{member}}.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Burger": {
        transform: "@mantine/core/esm/Burger/Burger.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Menu/MenuDivider/MenuDivider": {
        transform: "@mantine/core/esm/Menu/MenuDivider/MenuDivider.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Menu/MenuItem/MenuItem": {
        transform: "@mantine/core/esm/Menu/MenuItem/MenuItem.js",
        skipDefaultConversion: true,
        preventFullImport: true,
      },
      "@mantine/core/lib/Menu/MenuTarget/MenuTarget": {
        transform: "@mantine/core/esm/Menu/MenuTarget/MenuTarget.js",
        skipDefaultConversion: true,
      },
      "@mantine/core/lib/Menu/MenuDropdown/MenuDropdown": {
        transform: "@mantine/core/esm/Menu/MenuDropdown/MenuDropdown.js",
        skipDefaultConversion: true,
      },
    },

It worked, but I unfortunately didn't notice a great perfomance gain on dev mode.

DenisBessa avatar Jun 05 '23 15:06 DenisBessa

We have the "slow getServerSideProps" issue and while for MaterialUI, working with modularizeImports/importing the default exports of the specific components seems to ease the pain, I struggle finding a solution Mantine.

  • @mantine/core/lib/[Component] can't be resolved (even tho the types seem to have that path)
  • modularizeImports isn't really working, because paths aren't consistent, some things (MantineProvider, useMantineTheme) don't seem to be available on a sub-paths at all (at least not for typescript).

Has anyone found a working solution for NextJS SSR + Mantine imports?

boredland avatar Aug 25 '23 09:08 boredland

hey guys thats a realy big problem. and also tabler ui has same problem

Devisione avatar Sep 19 '23 13:09 Devisione

hey guys thats a realy big problem. and also tabler ui has same problem

I think Next.js 13.5 will address it with a new config option in next.config.js file. The recent commits on the Next.js project indicates that there will be an optimizePackageImports experimental option, and it will work automatically.

DenisBessa avatar Sep 19 '23 13:09 DenisBessa

I tried the experimental.optimizePackageImports - by just adding "@mantine/core" our ssr pages got SO much faster!

boredland avatar Sep 19 '23 22:09 boredland

in my case optimizePackageImports does not help me. i have ben add @mantine/core and also @tabler/icons-react and have the same modules in dev mode. (and same build ms)

Compiled /in 9.2s (6432 modules) (4000+ modules only @tabler/icons-react) but i am use only 10 icons. that a very big problem.... modularizeImports have same problem.

Devisione avatar Sep 20 '23 06:09 Devisione

How did you get those numbers?

boredland avatar Sep 20 '23 08:09 boredland

This is no longer an issue starting from Next.js 13.5 with optimizePackageImports. I've validated that it works correctly with app router:

With optimizePackageImports:

✓ Compiled /page in 1141ms (412 modules)
✓ Compiled /not-found in 166ms (403 modules)

Without optimizePackageImports:

✓ Compiled /page in 4.4s (2125 modules)
✓ Compiled /not-found in 382ms (2107 modules)

rtivital avatar Feb 26 '24 16:02 rtivital

This is no longer an issue starting from Next.js 13.5 with optimizePackageImports. I've validated that it works correctly with app router:

With optimizePackageImports:


✓ Compiled /page in 1141ms (412 modules)

✓ Compiled /not-found in 166ms (403 modules)

Without optimizePackageImports:


✓ Compiled /page in 4.4s (2125 modules)

✓ Compiled /not-found in 382ms (2107 modules)

I saw that, it works great. You might want to add a section about this on the installation docs. It doesn't seem to have any downsides

ajnart avatar Feb 26 '24 17:02 ajnart