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

[QUESTION] Why is this code not tree shaked? #756

Closed
jdvivar opened this issue Feb 4, 2021 · 6 comments
Closed

[QUESTION] Why is this code not tree shaked? #756

jdvivar opened this issue Feb 4, 2021 · 6 comments

Comments

@jdvivar
Copy link

jdvivar commented Feb 4, 2021

I've got a simple Web Component defined like this:

// Not using LitElement
import { LitElement } from 'lit-element'

window.customElements.define(
  'my-component',
  class extends HTMLElement {
    connectedCallback() {
      this.innerHTML = '<p>hello</p>'
    }
  }
)

And esbuild is building with this script:

esbuild ./src/my-component.js --bundle --outdir=dist

The class LitElement is not being used at all, however the output includes the whole of LitElement's class definition: https://esbuild-tree-shaking-example.glitch.me/my-component.js

You've got the example at Glitch here:
https://glitch.com/edit/#!/esbuild-tree-shaking-example

What am I missing? 😅

@evanw
Copy link
Owner

evanw commented Feb 4, 2021

It is retained because it contains code that can potentially have side effects. The two expressions retaining LitElement are these:

LitElement["finalized"] = true;
LitElement.render = render2;

For example, the superclass called UpdatingElement could contain a setter called finalized that has side effects. Tree shaking in esbuild will only remove code if it's 100% confident that it does not have side effects, which requires having proof. There are many edge cases here that could introduce side effects. For example, UpdatingElement extends HTMLElement. Does that have a setter for finalized? It's often impossible to prove that a given piece of JavaScript code has no side effects with 100% confidence since JavaScript is such a dynamic language.

If you would like for LitElement to be removed, you could consider advocating for the lit-element package to make use of the sideEffects annotation in package.json. This is a common convention respected by both Webpack and esbuild to mark entire files as having no side effects. Import statements to files marked as having no side effects are completely removed from the bundle if none of the imports are used.

@leeoniya
Copy link

leeoniya commented Feb 4, 2021

a opt-in "loose" mode would be quite useful. while many things are possible, the overwhelming majority are unusual/improbable.

@jdvivar
Copy link
Author

jdvivar commented Feb 5, 2021

Import statements to files marked as having no side effects are completely removed from the bundle if none of the imports are used.

I know it's super invasive doing it like this, but what's your opinion about the improvement?
Marking the packages with a sideEffects given value from within a sideEffectsPackages... I just made everything up on a whim.

The scripts now look like this:

  "scripts": {
    "mark-side-effects": "node ./mark-side-effects.js",
    "prebuild": "npm run mark-side-effects",
    "build": "esbuild ./src/my-component.js --bundle --outdir=dist",
    "prestart": "npm run build && cp src/*.html dist",
    "start": "npx servor dist"
  },
  ...
  "sideEffectsPackages": {
    "lit-element": false
  },
  ...

And I've written mark-side-effects.js in 10 min but for POC it's OK:

const fs = require("fs");
const sideEffectsPackages = require("./package.json").sideEffectsPackages;

if (sideEffectsPackages) {
  Object.keys(sideEffectsPackages).forEach(packageName => {
    const packagePath = `./node_modules/${packageName}/package.json`;
    const packageJSON = JSON.parse(fs.readFileSync(packagePath));
    packageJSON.sideEffects = sideEffectsPackages[packageName];
    fs.writeFileSync(packagePath, JSON.stringify(packageJSON));
  })
}

Now the bundle works as expected by your explanation @evanw , thank you for taking the time to clarify it for me.

@jdvivar
Copy link
Author

jdvivar commented Feb 5, 2021

@evanw Of course, instead of actually overwriting packages' package.json files... maybe there's a way to do it with an esbuild plugin (that I can work on myself)?

@jdvivar
Copy link
Author

jdvivar commented Feb 5, 2021

OK, I'm reading the code for https://github.com/evanw/esbuild/blob/master/internal/resolver/resolver.go and the plugins documentation and there's definitely no way to do it with a plugin. As described:

A plugin's job is to generate the individual files that end up being linked.

Would you consider adding an advanced option to mark sideEffects values from within esbuild?

require('esbuild').build({
  entryPoints: ['app.js'],
  bundle: true,
  outfile: 'out.js',
  sideEffects: {
    'lit-element': false
  },
}).catch(() => process.exit(1))

sideEffects, sideEffectsMap, or whatever other name you find it suits best

@jdvivar
Copy link
Author

jdvivar commented Feb 6, 2021

Closing because the question has been answered (by the author!) and a rough solution has been given by myself in the conversation. A feature to solve the issue (which is completely outside the responsibility of the builder/bundler but a nice-to-have nonetheless) has already been proposed as well. Thanks @evanw !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants