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

Vite not tree-shaking dynamic imports #14145

Closed
7 tasks done
benlind opened this issue Aug 17, 2023 · 10 comments · Fixed by #14221
Closed
7 tasks done

Vite not tree-shaking dynamic imports #14145

benlind opened this issue Aug 17, 2023 · 10 comments · Fixed by #14221
Labels
has workaround p3-significant High priority enhancement (priority)

Comments

@benlind
Copy link

benlind commented Aug 17, 2023

Describe the bug

Vite 4.4.9 is not tree-shaking the contents of dynamically-imported modules:

// utils.js
export function export1() {
  console.log('export1')
}
export function export2() {
  console.log('export2')
}

// main.js
async function main() {
  const { export1 } = await import('./utils.js')
  export1()
}
main()

In the above example, even though only export1 is imported, both export1 and export2 will be bundled by Vite.

If you use a static import, export2 is correctly tree-shaken from the final bundle:

import { export1 } from './utils.ts'
export1()

Rollup has supported dynamic import tree-shaking since 3.21.0 (description of feature, PR), and Vite 4.4.9 is using Rollup 3.28.0, so Vite should also support it.

Vite #11080 was closed saying "rollup now supports this," but the repro link was not a Vite repro, but a vanilla Rollup repro. I attached a vanilla Vite repro, and here is a Vite Vue TS repro.

Finally, here is a repro of Rollup correctly tree-shaking the dynamic import.

Reproduction

https://stackblitz.com/edit/vitejs-vite-z6x1lz?file=main.js

Steps to reproduce

  1. Open the repro stackblitz
  2. npm run dev
  3. Look at the dist/ output for "export4". It will appear even though it shouldn't.

System Info

Stackblitz

Used Package Manager

npm

Logs

No response

Validations

@benlind benlind changed the title Vite not tree-shaking dynamic import Vite not tree-shaking dynamic imports Aug 17, 2023
@sapphi-red
Copy link
Member

Ah, I guess this is happening because Vite replaces

const { export3 } = await import('./not-tree-shaken.js');
// into
const { export3 } = await __vitePreload(() => import('./not-tree-shaken.js'), []);

Then, rollup fails to analyze this.

@sapphi-red sapphi-red added has workaround p3-significant High priority enhancement (priority) labels Aug 18, 2023
@bdezso

This comment was marked as duplicate.

@benlind
Copy link
Author

benlind commented Aug 18, 2023

@sapphi-red , I see you added the "has workaround" label. Are you aware of a workaround?

@sapphi-red
Copy link
Member

This workaround (#11080 (comment)) should still work.

@benlind
Copy link
Author

benlind commented Aug 22, 2023

Thanks for the link.

Unfortunately that workaround is too unwieldy for my use case. I'm using a "main": "index.ts" file in my package.json in a monorepo, and I want to support dynamic importing from that main file (I'm also using Webpack 4, which doesn't support the "exports" field, which would have let me use multiple exports). I would have to create facade files in every other package that uses this package for every one of the many exports.

@sapphi-red
Copy link
Member

(I'm also using Webpack 4, which doesn't support the "exports" field, which would have let me use multiple exports)

You don't need to use exports field for multiple exports. If you don't declare exports field, all files are exposed. For example, if you have a package named foo and have bar.ts in the root directory of the package, you can import that file with foo/bar.ts.

@benlind
Copy link
Author

benlind commented Aug 23, 2023

Certainly true, though in our monorepo we enforce importing from the canonical main files for structure/API hygiene.

@ccreusat
Copy link

ccreusat commented Jan 12, 2024

Hi,

I'm coming from this issue rollup/rollup#4951 and I cannot import dynamic component from a react library:

const OnboardingModal = lazy(async () => {
  const module = await import("@edifice-ui/react");
  return { default: module.OnboardingModal };
});

This will break tree-shaking and imports whole library..

I have to import and re-export my component to use it without importing whole library.

// Example
import { OnboardingModal } from "@edifice-ui/react";

export default OnboardingModal;

Then I can use lazy / await import

const OnboardingModal = lazy(async () => await import("./Re-export"));

Any idea if it's possible since this PR was merged ?

I don't know if there is any point in re-exporting to use Suspense/Lazy instead of a static import.

@Taewoong1378
Copy link

Taewoong1378 commented Jun 6, 2024

I faced a similar issue where Vite wasn't tree-shaking dynamically imported modules, particularly when using a design system with a barrel file that imported all components. Even if only one component was used, all components were included in the bundle. I solved this by creating a custom Vite plugin to handle the imports more efficiently.

Here’s how I resolved the issue:

Problem

Using a barrel file for components resulted in all components being included in the final bundle, even if only one component was used. For example, with the following barrel file:

// packages/design-system/src/components/index.ts
export * from './Badge';
export * from './Button';
export * from './Checkbox';
export * from './Chip';
export * from './Divider';
// ...other components

Even if only one component was imported and used, all components were bundled.

// only one component is used
import { Button } from 'design-system'

// vite bundle was loaded all components exported like below
export * from "/@fs/path/design-system/src/components/Badge/index.ts";
export * from "/@fs/path/design-system/src/components/Button/index.ts";
export * from "/@fs/path/design-system/src/components/Checkbox/index.ts";
export * from "/@fs/path/design-system/src/components/Chip/index.ts";
export * from "/@fs/path/design-system/src/components/Divider/index.ts";
// ...other components

Solution

To resolve this, I created a custom Vite plugin in the vite.config.ts file. This plugin dynamically imports only the necessary components.

Here’s the full configuration:

// vite.config.ts
import legacy from '@vitejs/plugin-legacy';
import react from '@vitejs/plugin-react-swc';
import fs from 'node:fs';
import path from 'node:path';
import { defineConfig, type Plugin } from 'vite';
import svgr from 'vite-plugin-svgr';

// Read all component directories
const componentsDir = path.resolve(__dirname, '../../packages/design-system/src/components');
const componentNames = fs
  .readdirSync(componentsDir)
  .filter((name) => fs.lstatSync(path.join(componentsDir, name)).isDirectory());

const componentAlias = componentNames.reduce((acc, name) => {
  acc[`design-system/${name}`] = path.join(componentsDir, name, `${name}.tsx`);
  return acc;
}, {} as Record<string, string>);

function designSystemPlugin(): Plugin {
  return {
    name: 'design-system-plugin',
    enforce: 'pre',
    resolveId(source: string) {
      if (source === 'design-system') {
        return source;
      }
      return null;
    },
    // I used 'lazy' import for React, but you can use 'defineAsyncComponent' if you are using Vue.js
    load(id: string) {
      if (id === 'design-system') {
        const imports = componentNames.map(
          (name) => `
          export const ${name} = lazy(() => import('${componentsDir}/${name}/${name}').then((module) => ({ default: module.${name} })));\n`,
        );
        return `import { lazy } from 'react';\n${imports.join('\n')}`;
      }
      return null;
    },
  };
}

export default defineConfig({
  plugins: [
    react(),
    designSystemPlugin(),
  ],
  resolve: {
    alias: {
      ...componentAlias,
    },
  },
  build: {
    commonjsOptions: {
      include: [/design-system/, /node_modules/],
    },
    target: 'esnext',
    rollupOptions: {
      plugins: [],
      output: {
        manualChunks(id) {
          if (id.includes('design-system')) {
            for (const component of componentNames) {
              if (id.includes(component)) {
                return `design-system/components/${component}/${component}.tsx`;
              }
            }
          }
        },
      },
    },
  },
});

Explanation

  • Component Alias: I created an alias for each component so they can be individually imported.
  • Custom Plugin: The designSystemPlugin dynamically imports components as needed. It uses React's lazy to load components only when required.
  • Manual Chunking: In the build.rollupOptions.output.manualChunks option, each component is put into its own chunk to ensure they are tree-shaken correctly.

This solution ensures that only the necessary components are included in the final bundle, achieving proper tree-shaking for dynamically imported modules in Vite.

Final Bundled Result

The final bundled result in Vite was as follows:

// final bundled result in vite was like below
import __vite__cjsImport0_react from "/node_modules/.vite/deps/react.js?v=fc7ff56c";
const lazy = __vite__cjsImport0_react["lazy"];

export const Badge = lazy(()=>import("/@fs/path/design-system/src/components/Badge/Badge.tsx").then((module)=>({
    default: module.Badge
})));

export const Button = lazy(()=>import("/@fs/path/design-system/src/components/Button/Button.tsx").then((module)=>({
    default: module.Button
})));

export const Checkbox = lazy(()=>import("/@fs/path/design-system/src/components/Checkbox/Checkbox.tsx").then((module)=>({
    default: module.Checkbox
})));

export const Chip = lazy(()=>import("/@fs/path/design-system/src/components/Chip/Chip.tsx").then((module)=>({
    default: module.Chip
})));

export const Divider = lazy(()=>import("/@fs/path/design-system/src/components/Divider/Divider.tsx").then((module)=>({
    default: module.Divider
})));
// ...other components

@github-actions github-actions bot locked and limited conversation to collaborators Jun 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
has workaround p3-significant High priority enhancement (priority)
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants