vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
67.16k stars 6.04k forks source link

Tree-Shaking: Everything disappears when moduleSideEffects: false #17911

Open senpro-ingwersenk opened 3 weeks ago

senpro-ingwersenk commented 3 weeks ago

Documentation is

Explain in Detail

So I am developing this SPA for my company and noticed that my bundle contains a lot of un-shaken components after analyzing the build output. After a while, I found the rollup option modulesideeffects which seemed to do what I was after. But, to quote South Park: "Aaaaaand it's gone!"

Before:

> vite build --emptyOutDir true

vite v5.4.1 building for production...                                                                                                                                                                 09:31:38
✓ 102 modules transformed.                                                                                                                                                                             09:31:40
../build/index.html                   0.49 kB │ gzip:  0.32 kB                                                                                                                                         09:31:40
../build/assets/index-DWSGYTYA.css   50.06 kB │ gzip:  8.76 kB                                                                                                                                         09:31:40
../build/assets/index-Ct1EDd8J.js   198.68 kB │ gzip: 64.10 kB                                                                                                                                         09:31:40
✓ built in 2.42s 

After:

> vite build --emptyOutDir true

vite v5.4.1 building for production...                                                                                                                                                                 09:32:00
✓ 102 modules transformed.                                                                                                                                                                             09:32:02
../build/index.html                  0.49 kB │ gzip: 0.32 kB                                                                                                                                           09:32:03
../build/assets/index-DWSGYTYA.css  50.06 kB │ gzip: 8.76 kB                                                                                                                                           09:32:03
../build/assets/index-rmTNly__.js    0.71 kB │ gzip: 0.40 kB                                                                                                                                           09:32:03
✓ built in 2.08s  

The only thing now contained in the JS file is the preloader. Here is the full config:

import { defineConfig, loadEnv } from 'vite'
import { createHtmlPlugin } from 'vite-plugin-html';
import { viteSingleFile } from "vite-plugin-singlefile"
import preact from "@preact/preset-vite";
import { resolve } from "path";

export default defineConfig({
    root: "frontend",
    plugins: [
      preact({
        previewMiddlewareEnabled: true
      }),
      /*
      viteSingleFile(),
      createHtmlPlugin({
        minify: true,
      }),
      */
    ],
    resolve: {
      alias: {
        'react': 'preact/compat',
        'react-dom': 'preact/compat',
        // shadcn
        '@': resolve(__dirname, "frontend/"),
      }
    },
    css: {
      postcss: resolve(__dirname, "postcss.config.js")
    },
    build: {
      emptyOutDir: true,
      rollupOptions: {
        input: resolve(__dirname, "frontend/index.html"),
        treeshake: {
          moduleSideEffects: false
        }
      },
      minify: 'esbuild',
      outDir: '../build',
      assetsInlineLimit: Infinity,
      //cssMinify: 'lightningcss'
    },
  });

In the documentation, it is said that tree-shaking is enabled by default - but apparently not really? For instance, it is including the entirity of modules that I am not even using and even the whole bulk of Preact and Preact Compat although I only use a few hooks (namely useState() and useEffect()). Whilst I am aware that this rollup option is probably the brute-force method, I really wish there was better documentation about tree-shaking - it's behaviour seems ... weird.

Your Suggestion for Changes

Reproduction

Heavy WIP, company internal.

Steps to reproduce

Unless I did something else wrong, this should reproduce the issue I have had whereby you will see both components and both their dependencies show up, whilst only half of that is neccessary. And thus, experience the pitfall that I wish was documented more. :)

Kind regards!

sapphi-red commented 3 weeks ago

it is said that tree-shaking is enabled by default - but apparently not really?

It is enabled by default.

For instance, it is including the entirity of modules that I am not even using and even the whole bulk of Preact and Preact Compat although I only use a few hooks (namely useState() and useEffect()).

For tree-shaking to work efficiently, it requires the code to be written carefully as the bundler has to be conservative. It seems there's some non-analyzable sideeffect-free code in preact/compat (https://github.com/preactjs/preact/issues/3295).

Document the various options that affect tree shaking

I'd say I recommend not to change any tree-shaking options unless you know the impact because it will easily break the output code. Therefore, I don't think we should add any additional documentation for tree-shaking related options.

Document the proper baseline setup ("type": "module", "sideEffects": false in the package.json)

Unless you're making a library "sideEffects": false won't affect that much since you won't add unused imports in your project directly.

Clarify under which conditions dead code is or is not removed

This is difficult to write because it's a moving target. If it's fine to write it simple, "statements that are considered by Rollup to be safely removed would be removed". Putting that aside, I think this should be in Rollup's docs rather than Vite's docs.

Possibly link out to what the purity annotations are and how to use them.

It's written in the rollup docs. I don't think you don't need to add these unless you're making a library.

Steps to reproduce

I tried to reproduce it here: https://stackblitz.com/edit/vitejs-vite-sth3yd?file=src%2Fapp.tsx&terminal=dev But wasn't able to. When I comment out import { AlertDialogOverlay } from '@/components/ui/alert-dialog';, the bundle size doesn't change. (That means AlertDialog is not included in the bundle even when it's imported.)

senpro-ingwersenk commented 3 weeks ago

For tree-shaking to work efficiently, it requires the code to be written carefully as the bundler has to be conservative. It seems there's some non-analyzable sideeffect-free code in preact/compat (https://github.com/preactjs/preact/issues/3295).

Oh wow, looking at the references made to this ticket, this is an old and well-known issue. Thanks for that pointer, I'll see where exactly preact/compat is pulled in, so I can course-correct where I can!

I will see if I can make the project's core public as to showcase a proper reproduction - but I have a hunch where the problem lies now thanks to your previous pointer. Chances are that the (few) dependencies that I use are heavily affected.

After reading Rollup's documentation in this recard, I still believe it should at least be linked to, if one wants to explore deeper. However, as you had stated, it certainly breaks things. Still, I'd rather have it there than not. I'd rather have the gun to shoot myself in the foot than to never know it existed in the first place - basically...

Anyway I will go and travel down that route, it may lead me to what I need find.

Here is the last report that I generated after looking into the imports and exports more: report.json