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

Svelte "hello world" increased by 4.37kB (~59%) between Svelte 3.37.0 and 3.38.0 #6462

Closed
nolanlawson opened this issue Jun 28, 2021 · 6 comments · Fixed by #6525
Closed
Labels

Comments

@nolanlawson
Copy link
Contributor

Describe the bug

First off, thank you for maintaining Svelte. It's one of my favorite JavaScript frameworks for its ease-of-use, small bundle size, and runtime performance.

Unfortunately it seems that the baseline bundle size has increased significantly in recent versions. I wrote a small repro using Rollup, rollup-plugin-svelte, and the Hello World example from the REPL. Here are the bundle sizes reported by bundlesize for recent Svelte versions:

3.37.0 3.38.3 Delta
minified 7.42KB 11.79KB +58.89%
min+gz 2.27KB 3.58KB +36.59%

Here is a diff of the JavaScript bundle:

Click to see diff
19a20,126
> 
> // Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM
> // at the end of hydration without touching the remaining nodes.
> let is_hydrating = false;
> function start_hydrating() {
>     is_hydrating = true;
> }
> function end_hydrating() {
>     is_hydrating = false;
> }
> function upper_bound(low, high, key, value) {
>     // Return first index of value larger than input value in the range [low, high)
>     while (low < high) {
>         const mid = low + ((high - low) >> 1);
>         if (key(mid) <= value) {
>             low = mid + 1;
>         }
>         else {
>             high = mid;
>         }
>     }
>     return low;
> }
> function init_hydrate(target) {
>     if (target.hydrate_init)
>         return;
>     target.hydrate_init = true;
>     // We know that all children have claim_order values since the unclaimed have been detached
>     const children = target.childNodes;
>     /*
>     * Reorder claimed children optimally.
>     * We can reorder claimed children optimally by finding the longest subsequence of
>     * nodes that are already claimed in order and only moving the rest. The longest
>     * subsequence subsequence of nodes that are claimed in order can be found by
>     * computing the longest increasing subsequence of .claim_order values.
>     *
>     * This algorithm is optimal in generating the least amount of reorder operations
>     * possible.
>     *
>     * Proof:
>     * We know that, given a set of reordering operations, the nodes that do not move
>     * always form an increasing subsequence, since they do not move among each other
>     * meaning that they must be already ordered among each other. Thus, the maximal
>     * set of nodes that do not move form a longest increasing subsequence.
>     */
>     // Compute longest increasing subsequence
>     // m: subsequence length j => index k of smallest value that ends an increasing subsequence of length j
>     const m = new Int32Array(children.length + 1);
>     // Predecessor indices + 1
>     const p = new Int32Array(children.length);
>     m[0] = -1;
>     let longest = 0;
>     for (let i = 0; i < children.length; i++) {
>         const current = children[i].claim_order;
>         // Find the largest subsequence length such that it ends in a value less than our current value
>         // upper_bound returns first greater value, so we subtract one
>         const seqLen = upper_bound(1, longest + 1, idx => children[m[idx]].claim_order, current) - 1;
>         p[i] = m[seqLen] + 1;
>         const newLen = seqLen + 1;
>         // We can guarantee that current is the smallest value. Otherwise, we would have generated a longer sequence.
>         m[newLen] = i;
>         longest = Math.max(newLen, longest);
>     }
>     // The longest increasing subsequence of nodes (initially reversed)
>     const lis = [];
>     // The rest of the nodes, nodes that will be moved
>     const toMove = [];
>     let last = children.length - 1;
>     for (let cur = m[longest] + 1; cur != 0; cur = p[cur - 1]) {
>         lis.push(children[cur - 1]);
>         for (; last >= cur; last--) {
>             toMove.push(children[last]);
>         }
>         last--;
>     }
>     for (; last >= 0; last--) {
>         toMove.push(children[last]);
>     }
>     lis.reverse();
>     // We sort the nodes being moved to guarantee that their insertion order matches the claim order
>     toMove.sort((a, b) => a.claim_order - b.claim_order);
>     // Finally, we move the nodes
>     for (let i = 0, j = 0; i < toMove.length; i++) {
>         while (j < lis.length && toMove[i].claim_order >= lis[j].claim_order) {
>             j++;
>         }
>         const anchor = j < lis.length ? lis[j] : null;
>         target.insertBefore(toMove[i], anchor);
>     }
> }
> function append(target, node) {
>     if (is_hydrating) {
>         init_hydrate(target);
>         if ((target.actual_end_child === undefined) || ((target.actual_end_child !== null) && (target.actual_end_child.parentElement !== target))) {
>             target.actual_end_child = target.firstChild;
>         }
>         if (node !== target.actual_end_child) {
>             target.insertBefore(node, target.actual_end_child);
>         }
>         else {
>             target.actual_end_child = node.nextSibling;
>         }
>     }
>     else if (node.parentNode !== target) {
>         target.appendChild(node);
>     }
> }
21c128,133
<     target.insertBefore(node, anchor || null);
---
>     if (is_hydrating && !anchor) {
>         append(target, node);
>     }
>     else if (node.parentNode !== target || (anchor && node.nextSibling !== anchor)) {
>         target.insertBefore(node, anchor || null);
>     }
189a302
>             start_hydrating();
201a315
>         end_hydrating();
232c346
< /* index.svelte generated by Svelte v3.37.0 */
---
> /* index.svelte generated by Svelte v3.38.3 */

Most of the size increase seems to have come from 10e3e3d and 04bc37d, which are related to hydration.

I'm not completely sure, but it seems like potentially this new code could be omitted for components compiled with hydratable: false? This would help a lot for use cases like mine, where I'm distributing a small standalone web component built with Svelte, with no SSR or hydration needed, and I'd ideally like it to be as small as possible.

Thank you in advance for considering this potential performance improvement!

Reproduction

git clone git@gist.github.com:0fd1d597cd18bd861a60abf035466a17.git repro
cd repro
npm i
npm t

Logs

No response

System Info

Ubuntu 20.04.2

Severity

annoyance

@Conduitry
Copy link
Member

Yep I think it would probably make sense to have dumb versions of the claim functions that are used when hydration is not enabled at compile time.

@hbirler
Copy link
Contributor

hbirler commented Jun 29, 2021

I count around 4521 letters in the diff (quite close to 11.79K - 7.42K) with around 1719 characters from comments. So I guess this is the bundle size for debug builds, right? Do bundle sizes also influence performance significantly when running locally in debug mode? (I am kinda new to modern high performance Js development so I am curious as to what should be optimized. The thought did not even occur to me to measure the actual byte size of my code while writing the PR 😂)
If we really want to micro-optimize this, we might want to shorten the name claim_order since I do not believe it is shortened even in release builds.

@nolanlawson
Copy link
Contributor Author

This isn't in dev mode, no. Also the bundlesize tool measures minified and minified+gzipped sizes. I diffed using the raw unminified file though.

@Kapsonfire-DE
Copy link
Contributor

If this is already the optimized code, shouldnt svelte minify variable and functions names for further reduction of code size?

@nolanlawson
Copy link
Contributor Author

I think that's a separate issue: #1102 (comment)

My request here would probably boil down to: "If options.hydratable is false, don't emit any code related to hydration."

@Conduitry
Copy link
Member

Svelte 3.40.0 should now be using the simpler versions of a few internal helpers that don't support hydration if you didn't compile with hydratable: true.

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

Successfully merging a pull request may close this issue.

5 participants