vitejs / vite

Next generation frontend tooling. It's fast!
http://vite.dev
MIT License
68.52k stars 6.19k forks source link

Treeshaking with compile-time constants #17898

Open LorisSigrist opened 2 months ago

LorisSigrist commented 2 months ago

Description

I've been trying to get treeshaking to work with compile-time computed keys. Consider the following:

// src/index.js
import * as m from "./my-module.js"
console.log(m['hello_' + __defined_value__])

// vite.config.js
const config = {
  define: {
    __defined_value__: JSON.stringify("world")
  }
}

In this example, the call to m['hello_' + __defined_value__] could (should) be reduced to m['hello_world'] before treeshaking. That way only the hello_world export from ./my-module.js is used and everything else is treeshaken.

Currently all exports from ./my-module.js would be included in the build-output.

Suggested solution

AFAIK Rollup currently doesn't do this kind of transform, however, EsBuild does during it's minification step. It would output the following code

import * as m from "./my-module.js"

const thing = "world"; 
m["hello_world"] // the string get's computed and inlined

The issue is timing.

This transform would need to happen after the vite:define plugin, but before the build plugins. Currently EsBuild's minification only runs after the build has already completed.

This results in a weird output where the key is pre-computed in the output, but no treeshaking has taken place:

// dist/assets/index-[hash].js
const e = Object.freeze(
  Object.defineProperty(
    {
      __proto__: null,
      hello_world: "Hello World",
      extra: "I should have been treeshaken",
    },
    Symbol.toStringTag,
    { value: "Module" }
  )
);
console.log(e.hello_world()); // correctly computed & inlined

Vite could achieve the desired optimization by running esbuild on *.{js|ts|jsx|tsx} files before running rollup. This wouldn't need to be full-blown minification, just the constant-folding step.

Alternatively this optimization could be added to rollup itself.

Alternative

Users could manually provide a plugin that runs EsBuild minification after Vite's built-in plugins but before the build plugins. I believe enforce: undefined gives that behavior.

Additional context

I work on the Paraglide i18n library, which could benefit greatly from this. See: https://github.com/opral/inlang-paraglide-js/issues/164

Validations

sapphi-red commented 2 months ago

It worked with this example. Would you create a reproduction? https://stackblitz.com/edit/vitejs-vite-rapg5b?file=vite.config.js,main.js&terminal=dev

LorisSigrist commented 2 months ago

Of course, here is a minimal repo: https://github.com/LorisSigrist/vite-17898-reproduction

sapphi-red commented 2 months ago

Ah, I found that the tree-shake works with console.log(m['new_year_' + __YEAR__]()) but not with console.log(m[`new_year_${__YEAR__}`]()).

It seems it's because esbuild replaces 'new_year_' + __YEAR__ with "new_year_2024" and 'new_year_' + __YEAR__ with new_year_${"2024"}. (When minify is not enabled)