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

A more holistic compilation process #15116

Closed
Ocean-OS opened this issue Jan 27, 2025 · 4 comments
Closed

A more holistic compilation process #15116

Ocean-OS opened this issue Jan 27, 2025 · 4 comments

Comments

@Ocean-OS
Copy link
Contributor

Describe the problem

One important thing about component-based development is that the individual components can be any size; some can basically be your entire app, and some can just be a single styled element for reusability. As a result, some of these components have the potential to be very small and performant, with the help of a Sufficiently Smart Compiler. However, Svelte compiles every component to the same format:

import 'svelte/internal/disclose-version';
import * as $ from 'svelte/internal/client';

// insert functions not reliant on too much state here...

var root = $.template(`template stuff`);
// insert any other $.templates here...
export default function Component($$anchor, $$props) {
	let fragment = root();
        // insert component instance
        // insert template effects here
        $.append($$anchor, fragment);
}
// event delegation goes here, if necessary

In reality, not every component is large or complex enough to warrant the need for two module imports and a template; component libraries such as shadcn/ui encourage the use of small, often single element, styled components. While I do agree that Svelte's internal library and compiled code is optimized and performant, it isn't as performant as vanilla DOM operations, nor as small— a Svelte counter component compiled with default configuration through Vite is 11kB, while a counter component written in vanilla JS is less than half a kilobyte. Being a compiler, Svelte has the advantage of being able to determine the most efficient component code to emit, and to be able to use alternative methods to generate smaller code when necessary.

Describe the proposed solution

It would be pretty beneficial if Svelte could figure out if it would be more performant and smaller to emit vanilla JS without the internal library. For example, a counter component could easily be written in vanilla JS like so:

var root = document.createElement('button');
export default function Counter($$anchor) {
    let button = root.cloneNode();
    let count = 0;
    let click = () => {
        button.textContent = `Count is ${++count}`;
    }
    button.addEventListener('click', click);
    button.textContent = 'Count is 0';
    $$anchor.before(button);
    return () => { //callback to be called when the component is destroyed
        button.removeEventListener('click', click);
        button.remove();
    }
}

This code is not only smaller, but more performant, as it directly interacts with the DOM and removes the (albeit small) overhead of signals and effects. I won't try to sugarcoat the difficulty this would entail, but I will say that it could lead to more performant components and smaller bundles.
As the DOM might be difficult to efficiently directly compile to, there should be a strict criterion for a component for it to be compiled to this format. Here's a bikesheddable criteria for now:

  • If the component doesn't use any logic blocks, it might be eligible
  • If the component doesn't use any components, it might be eligible
  • If the component doesn't use any $deriveds or $effects, it might be eligible
  • If the component doesn't use many $states, it might be eligible
  • If the template of the component has very few elements, it might be eligible
  • If there aren't any bindings, actions (or other directives) or prop spreading, it might be eligible
  • If there aren't any $state proxies involved, it might be eligible

I'm being purposefully very vague here, as the compiler should be very careful when it chooses to compile to this format, and it'd probably be best if testing was done to see the effectiveness of this compilation target, in terms of performance, compiled output quality, and scaleability.

Importance

nice to have

@Prinzhorn
Copy link
Contributor

In reality, not every component is large or complex enough to warrant the need for two module imports and a template

But are such components distributed standalone? I'm not sure I understand what you're trying to solve. If you have a single "big" component in your app that requires the internal imports, then it does not matter if hundreds of small (single element) ones also need the imports, since it does not make the bundle any bigger.

@paoloricciuti
Copy link
Member

I feel this would introduce way more code than it saves to properly handle hydration and to deal with the "special" component than it saves. Especially because when bundled those imports basically vanish because they are being imported by other components. Furthermore it feels like it would actually help in such small amount of components that I'm not sure is worth the additional maintenance and runtime overhead

@PuruVJ
Copy link
Collaborator

PuruVJ commented Jan 27, 2025

I agree with Paolo: too many imports are a feature, not a bug. They all are reused at any scale and keep the bundle from growing too quickly

@Rich-Harris
Copy link
Member

This is too vague to be actionable, so I'm going to close it. But before I do, I want to explain why things work the way they do. This example code is 468 bytes...

var root = document.createElement('button');

export default function Counter($$anchor) {
  let button = root.cloneNode();
  let count = 0;
  let click = () => {
    button.textContent = `Count is ${++count}`;
  }
  button.addEventListener('click', click);
  button.textContent = 'Count is 0';
  $$anchor.before(button);
  return () => { //callback to be called when the component is destroyed
    button.removeEventListener('click', click);
    button.remove();
  }
}

...while this code weighs 466:

import * as $ from 'svelte/internal/client';

var on_click = (_, count) => $.set(count, $.get(count) + 1);
var root = $.template(`<button> </button>`);

export default function App($$anchor) {
  let count = $.state(0);
  var button = root();

  button.__click = [on_click, count];

  var text = $.child(button);

  $.reset(button);
  $.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
  $.append($$anchor, button);
}

$.delegate(['click']);

So it's already smaller, in incremental terms, and that's before we start adding new places where count is referenced or — in particular — new places where count is changed. (Imagine we had an <input type="range" bind:value={count}> in addition to the <button>, and think about how the vanilla code would immediately start ballooning, not to mention how much more complex the compiler would need to be.)

Yes, the second example imports functions — $.set, $.get, $.template and so on. But these are functions that are already used in every single Svelte app, so you don't pay anything extra for them.

Consider what you're losing when you use the naive approach of interacting with the DOM directly — no more event delegation (if this component is rendered many times, adding all those event handlers will have a noticeable impact on memory usage), no control over scheduling (doing all the DOM updates at the same time means you avoid layout thrashing), no ability to avoid updating the DOM at all in the case where work should be paused for some reason (which will become relevant very soon). You lose far more than you gain.

There is a sense in which a more 'holistic' compilation process makes sense — it would be great if (for example) the compiler could know that a specific prop was only ever a static value, so that we could optimise the code generated for rendering its value — but I'm afraid this isn't it.

@Rich-Harris Rich-Harris closed this as not planned Won't fix, can't repro, duplicate, stale Jan 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants