sveltejs / svelte

web development for the rest of us
https://svelte.dev
MIT License
79.82k stars 4.24k forks source link

Svelte 4.0.0 custom-element build bundle size is way larger than Svelte 3.58.0 #8826

Open sherifsalah opened 1 year ago

sherifsalah commented 1 year ago

Describe the bug

I created a plugin as a custom element with Svelte 3 it was generating a small bundle size around 70kb but after migration to Svelte 4 its now around 120kb, that's almost double the size .. nothing changed at all just svelte version.

I provided a quick example from svelte's own examples that's generating 22.67kb with Svelte 3 and 27.21kb with Svelte 4 .. and added sass support and imported a few sass modules from bootstrap just for testing purposes.

Reproduction

REPL

Logs

Svelte 3:
✓ 8 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-e5819c2c.js  22.67 kB │ gzip: 8.95 kB
✓ built in 4.75s

Svelte 4:
✓ 29 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-d63461f3.js  27.21 kB │ gzip: 9.97 kB
✓ built in 5.19s

System Info

Windows 10, Node v16.15.1, Google chrome.

Severity

annoyance

mnorlin commented 1 year ago

I have also noticed that the output for custom elements are larger in Svelte 4 (~50% larger).

I have tried to reproduce the difference with a minimal implementation:

  1. Created a clean Svelte 3 project (npm init vite)
  2. Made Counter.svelte a custom element, and configured the project to build custom elements.
  3. Upgraded to Svelte 4 and compared the output

https://github.com/mnorlin/svelte-4-custom-element-issue

commit description js output size
318d240 Svelte 4 7.74 kB
22e7f08 Svelte 3 4.58 kB

(@sherifsalah: your reproduction don't seem to build, and I think all the bundled CSS in the component makes it hard to compare the output size of the generated javascript, as the size differences probably becomes less significant with more CSS).

dummdidumm commented 1 year ago

A slight increase in the baseline bundle size is expected, since we're now using a wrapper around the existing Svelte components to create the custom elements. It shouldn't account for that big of an increase though.

I instead suspect this being due to the CSS being inlined into the JavaScript (else there's no reliable way to load them since shadow dom is used). Could you share your repository where this happens? If that's not possible, what are the compiler options you pass to Svelte?

sherifsalah commented 1 year ago

@mnorlin @dummdidumm I used a lot of CSS and sometimes imports from CSS Frameworks and i know all the limitations cause i made a lot of plugins using svelte custom elements, and currently i do my best to manually purge any unsed css based on the used classes' names, anyways in any case logically the same amount of inline CSS shouldn't change in both versions. (10KB of CSS as example shouldn't increase!) the main problem here, the size grows exponentially as i mentioned 70KB in svelte 3 became 120KB in svelte 4 so it is up to 100% increase in size! @dummdidumm sorrowfully i can't share any existing repos that's why i tried to fabricate a close example using an existing repl, if you need me to fabricate another example or fix any issues with the current one i'll gladly do so, i depend a lot on svelte for more than 3 or 4 years now and i really need to help fixing this problem or i'll stuck with svelte 3 for a long time or until i find an alternative. i don't mind a "slight" increase in the wrapper as you mentioned maybe 5 or 10 more Kbs not a big deal but not 50Kbs!

sherifsalah commented 1 year ago

I tried to isolate the problem is it JS or CSS and compiled the plugin without any CSS at all and here is the results: with Svelte 3:

vite v4.3.9 building for production...
✓ 19 modules transformed.
dist/index.html                54.93 kB │ gzip:  7.33 kB
dist/assets/index-260c33d6.js  39.28 kB │ gzip: 13.82 kB
✓ built in 3.85s

with Svelte 4:

✓ 40 modules transformed.
dist/index.html                54.93 kB │ gzip:  7.33 kB
dist/assets/index-d5085aed.js  42.51 kB │ gzip: 14.60 kB
✓ built in 4.27s

And with CSS with Svelte 3:

✓ 19 modules transformed.
dist/index.html                54.93 kB │ gzip:  7.33 kB
dist/assets/index-c1c13d88.js  69.78 kB │ gzip: 19.79 kB
✓ built in 4.68s

with Svelte 4:

✓ 40 modules transformed.
dist/index.html                 54.93 kB │ gzip:  7.33 kB
dist/assets/index-6a6bdca3.js  106.18 kB │ gzip: 22.35 kB
✓ built in 4.88s

Concluding that the JS difference is 3.23KB which is not a big deal and with CSS the difference is 36.4KB

The provided example without CSS in Svelte 3:

✓ 8 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-6028c763.js  15.18 kB │ gzip: 6.65 kB
✓ built in 665ms

and without CSS in Svelte 4:

✓ 29 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-8d43cd03.js  17.97 kB │ gzip: 7.53 kB
✓ built in 774ms

With CSS in Svelte 3:

✓ 8 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-e5819c2c.js  22.67 kB │ gzip: 8.95 kB
✓ built in 5.24s

And with CSS in Svelte 4:

✓ 29 modules transformed.
dist/index.html                 0.40 kB │ gzip: 0.28 kB
dist/assets/index-d63461f3.js  27.21 kB │ gzip: 9.97 kB
✓ built in 5.30s

Concluding that the JS difference is 7.49KB which is not a big deal and with CSS the difference is 9.24KB And with @mnorlin example which doesn't have any CSS its 3.16KB

So JS increases as the component grows but not that much, and CSS explodes which is so weird cause its supposed to be a simple same sized string that will be injected into the header.

sherifsalah commented 1 year ago

Quick note: (maybe not related but it affectes the size a tiny bit) there is a lot of fragments in the bundle as well, mostly comes from transition functions i guess!

return{delay:t,duration:n,easing:s,css:(l,b)=>`
            transform: ${d} scale(${1-a*b});
            opacity: ${f-c*b}
        `
dummdidumm commented 1 year ago

From that comparison, the increase in size is mainly due to the new baseline size and due to the Svelte class names now being applied within Svelte components:

-.foo { color: red }
+.foo.svelte-xyz { color: red }

which is necessary because internally you can now use Svelte components as normally. This also shows due to the very small difference in size when gzipped as the hashes can be minified very well.

So far this all looks like expected, but the increase of almost 100% from 70kb to 120kb still sounds weird - which is why we need a proper reproduction for this.

sherifsalah commented 1 year ago

@dummdidumm I guess i found the problem, there is redundant class names in CSS, i'll share portions of my actual code and the built bundle and you will get the idea. Here is the actual CSS code: image and this is the result: image

As you can see the class name is written multiple times: .svelte-11mb47x.svelte-11mb47x.svelte-11mb47x.svelte-11mb47x::after{box-sizing:border-box} while we only need it once?!

After manually deleting the redundant .svelte-11mb47x and minify the script again it became pretty normal reduced to 83KB

sherifsalah commented 1 year ago

@dummdidumm can I suggest to have an option to change ‘svelte-‘ prefix to a shorter abbreviation or something to save some bytes! Maybe ‘sv-‘ or whatever.

dummdidumm commented 1 year ago

You can do that yourself using the cssHash option - if you're sure that your styles don't need to be scoped in any way, you can just return a single character from that option, which should get your script size down.

In the end it won't make a big difference though since most servers will compress everything and those algorithms are very good at shrinking character sequences that appear often.

sherifsalah commented 1 year ago

You can do that yourself using the cssHash option - if you're sure that your styles don't need to be scoped in any way, you can just return a single character from that option, which should get your script size down.

In the end it won't make a big difference though since most servers will compress everything and those algorithms are very good at shrinking character sequences that appear often.

That was a side request anyways.. Thank you so much for this info, i know it will not make a big difference in small elements but in my case it will save a few KBs.

Apart from that i hope that my last example is clear enough, the same redundancy problem exists in the original provided example as well like this .svelte-1wbq7af.svelte-1wbq7af

dummdidumm commented 1 year ago

The issue is somewhat more general, example REPL. We'll look into whether or not it's safe to deduplicate the hash in that case.

sherifsalah commented 1 year ago

The issue is somewhat more general, example REPL. We'll look into whether or not it's safe to deduplicate the hash in that case.

Oooh wow, never noticed that before (nobody digs into the generated css code i guess 😄), i hope you can find a solution for that, thanks.

yujonglee commented 4 months ago

customElement using single Dialog component from bits-ui.

With customElement=true:

vite v5.3.1 building for production...
✓ 546 modules transformed.
dist/index.js  1,488.17 kB │ gzip: 218.27 kB
dist/index.umd.cjs  830.70 kB │ gzip: 165.71 kB

With customElement=false:

vite v5.3.1 building for production...
✓ 546 modules transformed.
dist/index.js  114.75 kB │ gzip: 25.89 kB
dist/index.umd.cjs  64.94 kB │ gzip: 20.31 kB

1488 kB??

bennobuilder commented 3 months ago

I'm exploring different bundle sizes using customElement for my Shopify Theme Extension and noticed a significant increase in bundle size for customElement components from Svelte 3 to Svelte 5. While larger projects might benefit from decreased component sizes, standalone customElement bundles have increased notably, making me consider SolidJs for its smaller bundle sizes compared to Svelte 5.

app.js / solid.js are the shared "framework" code between the components (Counter and Reviews).

Framework Counter.js Reviews.js app.js/solid.js manifest.json app.css
Svelte 3 2.55 kB (1.19 kB) 9.92 kB (4.03 kB) 4.46 kB (1.97 kB) 0.64 kB (0.24 kB) 10.64 kB (2.89 kB)
Svelte 4 2.38 kB (1.11 kB) 9.79 kB (3.97 kB) 7.41 kB (3.02 kB) 0.64 kB (0.23 kB) 10.92 kB (2.91 kB)
Svelte 5 1.82 kB (0.91 kB) 10.00 kB (4.72 kB) 19.79 kB (8.04 kB) 0.64 kB (0.23 kB) 10.92 kB (2.91 kB)
SolidJs 1.65 kB (0.77 kB) 6.02 kB (2.65 kB) 14.01 kB (5.32 kB) 0.69 kB (0.24 kB) 10.54 kB (2.87 kB)
Svelte 3 | Counter.svelte ```svelte ```
Svelte 4 | Counter.svelte ```svelte ```
Svelte 5 | Counter.svelte ```svelte ```
SolidJs | Counter.tsx ```tsx import { customElement, noShadowDOM } from "solid-element"; import { createSignal } from "solid-js"; import "../app.css"; export const CounterExtension = customElement( "solid-counter", { backgroundColor: "white", color: "black", rounded: "false" }, (props, { element }) => { noShadowDOM(); const { backgroundColor, color, rounded } = props; const [count, setCount] = createSignal(0); console.log({ props, element }); return ( ); }, ); ```

Note: I used Vite (v^5.3.4) with the @sveltejs/vite-plugin-svelte (v^3.1.1 for Svelte v3/4 and v^4.0.0-next.4 for Svelte v5) and vite-plugin-solid (v^2.10.2 for SolidJs v1) plugin with minify = true and Counter.xyz and Reviews.xyz as entry points (input). And for styling I used Tailwind with PostCss.

Svelte configs `vite.config.js` ```js import { svelte } from "@sveltejs/vite-plugin-svelte"; import { defineConfig } from "vite"; export default defineConfig({ build: { minify: true, cssMinify: true, rollupOptions: { input: [ "./src/extensions/Counter.svelte", "./src/extensions/Reviews.svelte", ], }, outDir: "dist", manifest: true, }, plugins: [ svelte({ include: ["./src/**/*.svelte"], compilerOptions: { customElement: true, }, }), ], }); ``` `svelte.config.js` ```js import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' export default { // Consult https://svelte.dev/docs#compile-time-svelte-preprocess // for more information about preprocessors preprocess: vitePreprocess(), } ```

My question is whether my basic analysis is correct in concluding that for Svelte 5, the shared framework function will increase by over 100%, or if I've misconfigured something and can significantly decrease the bundle size for Svelte 5?

Thanks :)

dummdidumm commented 3 months ago

Can you show the code for Reviews.svelte? Curious to know why it's bigger than Svelte 4. In general, yes, the runtime is bigger in Svelte 5 than in Svelte 4, though there's definitely room to improve the tree-shakeability of parts of it and/or shrink it down.

bennobuilder commented 3 months ago

@dummdidumm Thanks for the quick reply. Reviews.svelte is mainly HTML code.

Ok, so it's expected to have a 100% + larger runtime size. Do you know how much room for improvement there might be?

Thanks :)

Review.svelte ```svelte

Customer Reviews

{#each Array(5).fill(0) as _, index}

{reviews.average} out of 5 stars

Based on {reviews.totalCount} reviews

Review data

{#each reviews.counts as count}

{count.rating} star reviews

{Math.round((count.count / reviews.totalCount) * 100)}%
{/each}

Share your thoughts

If you’ve used this product, share your thoughts with other customers

Write a review

Recent reviews

{#each reviews.featured as review}
{review.author}.

{review.author}

{#each Array(5).fill(0) as _, index}

{review.rating} out of 5 stars

{@html review.content}
{/each}
``` ```svelte ```