evanw / esbuild

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

Code splitting is creating many small unnecessary chunks #3780

Open ChristopherPHolder opened 1 month ago

ChristopherPHolder commented 1 month ago

When code splitting, esbuild does not seem to take into account code that is already downloaded or cannot be loaded on its own.

It seems important to distinguish between an application real entry points and its sub-entry points, as this has a direct impact on loading performance.

Lets say an application can only be accessed via main.ts, so it only has real "entry point", and then it has a bunch of lazy features "sub-entry points".

If the piece of shared code is consumed by main.ts, when the lazy feature imports it, the code is already there and it will be cached in some way. But in the case where there are many lazy features and many pieces of shared code that are consumed by main.ts it will split this into many little chunks.

Is there a benefit in splitting code that will necessary be imported in main.ts which will then also be imported in one of its lazy features?

These chunks seem unnecessary and are causing performance issues as to many chunks will increase load time can have a direct impact on user experience on Core Web Vitals.

Is there a way to reduce these "unnecessary chunks"?

Ideally i would like that when building esbuild looks at the node tree graph and recognizes when the code should have already been loaded by a parent node and does not splitting into smaller chunk.

Is there any way currently to reduce the number of chunks? Potentially with a plugin?

I have notice that by using bundles strategically i can reduce the number of chunks but this is not an ideal approach and it seems i am partially opting out of tree shaking.

I have tested this approach on a minimal angular application.

And created a minimal reproducible example using only esbuild, and typescript.

Minimal Preproduction

https://github.com/ChristopherPHolder/esbuild-code-splitting

The example is a minimal node cli with no dependencies only meant to illustrate this issue.

The CLI contains an execution file from where its meant to be accessed app.js. In theory it should not be accessed from anywhere else.

It constrains 3 file which are ui{1,2,3}.js and are statically imported both in app.js and in feature{1,2,3}.js which are in turn dynamically imported in app.js and nowhere else.

Even tho, all ui{1,2,3}.js are statically imported in app.js which is the only real declared entry point, it will create a chunk for each of these 3 files.

If we on add an index.js from where all ui{1,2,3}.js are imported then it will still chunk them as a separate file but it will only create 1 additional chunk.


I am aware that this is currently expected behaviour as the docs states it pretty clearly:

Note that the target of each dynamic import() expression is considered an additional entry point.

However, i am looking for a solution to reduce the number of chunks, as this seems to be a large performance issue at the moment.

sod commented 1 month ago

The pink chunks are what we talking about:

CleanShot 2024-05-28 at 12 31 23@2x

The general chunk structure is pretty nice. But the more import('...') entries you create, the more of these pink <2kb chunks appear

For us these micro chunks are created by: 1) Redux actions that are used at 2+ places. These chunks look like

import{Mb as e}from"./chunk-OMCORABT.js";var t=e("[Product] Clicked");export{t as a};

2) Or assets from { loader: { ".webp": "file" } that are used at 2+ places. These chunks look like:

var l="./media/image-FZ7VCKMX.webp";export{l as a};

Would be awesome if esbuild could inline the file loader string or via a minChunkSize pull into the largest parent chunk.

Gzip and less http requests would make this more efficient. And you reduce the chance of running into the maximum allowed requests limit.