evanw / esbuild

An extremely fast bundler for the web
https://esbuild.github.io/
MIT License
38.15k stars 1.15k forks source link

esbuild creates unintended non-dynamic chunks? #1716

Open use opened 3 years ago

use commented 3 years ago

Hello,

I'm 100% sure this is something I'm doing wrong but I was wondering if someone can point me in the right direction?

I'm coming from a webpack config, if that helps.

Here is my esbuild command:

npx esbuild src/js/theme.js --bundle --define:process.env.NODE_ENV=\"production\" --sourcemap --minify --analyze --splitting --format=esm --outdir=js

I'm using codesplitting so I can dynamically load chunks when they're needed. I'm using the dynamic import() function. This actually works great. Those chunks look like this in the --analyze output:

js/program-explorer-W7FFKVTU.js                                                  20.1kb  100.0%
   └ src/modules/program-explorer/program-explorer.tsx                             19.8kb   98.4%

However, I'm also getting "unintended" chunks. I am not loading them dynamically in any way that I'm aware of - they're being imported with plain old static import. The outputted chunks look like the following. Notably, the chunks have no real name in the prefix.

js/chunk-JPIFNYYQ.js                                                              2.4kb  100.0%
   ├ src/modules/course-popup/course-popup.jsx                                      1.4kb   57.7%
   └ src/modules/ewu-popup/ewu-popup.jsx                                            900b    36.8%

I guess my expectation is that when I use static import, the bundler would inline those modules rather than splitting and dynamically loading them. And I can't figure out why these statically loaded modules are being split into chunks.

hyrious commented 3 years ago

The extra chunk can exist because of common dependencies of your entrypoints including src/js/theme.js and the dynamic-imported ones.

Let me give you a minimal example:

// a.js
import { d } from "./c.js"
console.log('a', d)
import("./b.js")
// b.js
import { c } from "./c.js"
console.log('b', c)
// c.js
export const c = 1
export const d = 2

As you can see both a.js and b.js depend on c.js, turns out c.js is a common chunk. So:

// esbuild a.js --bundle --format=esm --splitting --outdir=dist --analyze
  dist/a.js               100b   100.0%
   └ a.js                  58b    58.0%

  dist/b-63JZQOHX.js       73b   100.0%
   └ b.js                  21b    28.8%

  dist/chunk-XF7GIIAF.js   52b   100.0%  // <- see, it doesn't has a name
   └ c.js                  22b    42.3%

The output code is almost the same as the input ones. Now you may know how esbuild's code splitting works.

evanw commented 3 years ago

Code splitting splits on any shared code, either statically or dynamically imported. This is documented here: https://esbuild.github.io/api/#splitting. This behavior is intended. It both reduces downloaded code when moving between entry points on different pages and fixes correctness issues due to duplicate module instantiation if multiple entry points are included in the same page (either static or dynamic).

use commented 3 years ago

In my situation I only ever have 1 entry point. With these chunks being splitted, every page has 5 "extra" chunks which get downloaded. I'm working with the assumption that I should avoid extra requests within reason, but also avoid downloading + parsing unneeded code (hence why I am using splitting).

Maybe there's a different way I should structure things?

Or am I overvaluing the downsides of downloading e.g. 5 extra small chunks?

Mwni commented 2 years ago

While the implementation seems very efficient, it slightly misses the point: reducing load times. The few kb saved by splitting everything up into multiple chunks make up less of an impact than the sequential import of said chunks, which is inevitable using ESM imports; The browser can't parallelize the downloads. The usual application has one entry, with peripheral content like async components, pages or widgets. Coming from Rollup, this is how developers are used to the concept.

garygreen commented 2 years ago

While the implementation seems very efficient, it slightly misses the point: reducing load times. The few kb saved by splitting everything up into multiple chunks make up less of an impact than the sequential import of said chunks

+1. By default esbuild seems to chunk even the slightest amount of data.

Webpack has a default specific set of criteria on when it chunks - it prefers bigger chunks because it's actually counterintuitive to create lots of mini chunks when they are small because the additional HTTP overhead outweighs simply inlining that chunk.

I would love for esbuild to provide more control over when it decides to chunk. E.g. only chunk for files larger than 5KB, chunk together if they are in node_modules, etc.

Overall this issue relates to: #207 - being able to configure when chunking occurs, much like Webpacks splitChunks config.

RomanHotsiy commented 2 years ago

+1 for having some way to disable static chunks, e.g. split only on dynamic imports, even sacrificing the correctness of the import order and code size.

With a not-too-complex real-world application, we're getting tons of tiny chunks:

image

alexblack commented 1 year ago

Seems like an issue to us too... Our build produces 91 chunks, about 50% of those are 1kb or smaller

abettadapur commented 1 year ago

Figma is also trying to migrate to a splitting solution, and we are also producing a lot of chunks (I think in testing, we are at 20 critical chunks). For every dynamic import we add, we increase the number of emitted chunks by a large amount. I suspect it will get even worse as we add more dynamic imports, because the number of permutations increases.

I think adding at least a minChunkSize would go a long way here, even if the browser ends up having to download slightly more code. Is addressing this on the roadmap at all?