esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

esbuild creates unintended non-dynamic chunks?

Open use opened this issue 4 years ago • 6 comments

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.

use avatar Oct 26 '21 23:10 use

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.

hyrious avatar Oct 27 '21 01:10 hyrious

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).

evanw avatar Oct 27 '21 03:10 evanw

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?

use avatar Oct 27 '21 17:10 use

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.

Mwni avatar Dec 31 '21 04:12 Mwni

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.

garygreen avatar Aug 04 '22 14:08 garygreen

+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

RomanHotsiy avatar Aug 26 '22 22:08 RomanHotsiy

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

alexblack avatar Feb 08 '23 01:02 alexblack

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?

abettadapur avatar Feb 15 '23 01:02 abettadapur