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

Stateless lightweight components #4006

Closed
lgirma opened this issue Nov 27, 2019 · 11 comments
Closed

Stateless lightweight components #4006

lgirma opened this issue Nov 27, 2019 · 11 comments
Labels
compiler Changes relating to the compiler feature request perf popular more than 20 upthumbs

Comments

@lgirma
Copy link

lgirma commented Nov 27, 2019

I want svelte to handle simple stateless components better

I've always run into situations where I have lots of simple dumb components that don't have states but the compiler ends up generating a full blown svelte components instead of a simple JS function (a pure function somewhat similar to React's function component) that emits HTML string.

In other words, why can't the svelte compiler generate code for simple stateless components in SSR fashion for the front-end?

Current work-around

  • Create and export a function from a Javascript module; the function emits a string with HTML code inside
export function MyStatelessComponent(arg1, arg2) {
    return `<div>Some HTML string from ${arg1}</div>`
}
  • Import the function from the module to consume it:
import {MyStatelessComponent} from "MyStatelessComponent.js"
  • All consumers of that component with call {@html MyStatelessComponent(arg1, arg2)}

The solution I would like

An ideal implementation would be for svelte compiler to identify stateless components and emit an efficient code like the one above.

  • Create the stateless component just any other ordinary svelte component
<script>
    export let arg1;
    export let arg2;
</script>

<div>
    Some HTML string from {arg1}
</div>
  • Consume the component as a typical svelte component
import MyStatelessComponent from "MyStatelessComponent.svelte"
...
<MyStatelessComponent arg1={...} arg2={...} />
  • Since the component is stateless, I would expect the compiler to generate a simple {@html MyStatelessComponent(arg1, arg2)} type of code

How important is this feature to you?

Couple of important points:

  • Since the project we are working on requires smaller bundle sizes, it would be great if we get svelte to compile our stateless components to simple functions
  • We have lots of lots of such components and converting them to a simple JS module by hand is tedious
  • In the current work around (using JS modules) writing the HTML using template strings gives us all sorts of problems including making tooling numb
  • Mixing .svelte file components with .js file components is giving us trouble when bundling (specially css purging). It would have been a lot easier if all of our components are ordinary .svelte components.
  • Svelte has always been about smaller bundle sizes and performance; why not have this extra optimization?

Additional context

Demo for the above component in REPL: https://svelte.dev/repl/eb9e018b42574c43b788af809e3e8582?version=3.15.0

Look at the unnecessary JS output of the compiler in the above REPL code

@j3rem1e
Copy link

j3rem1e commented Nov 27, 2019

Just a little warning here, your workaround should not be used as it because it introduces xss vulnerabilities.

@lgirma
Copy link
Author

lgirma commented Nov 29, 2019

(Another way of solving the above issue...)

Another thing I noticed (about simple stateless components) is that bundle size significantly increases when I extract portion of my svelte HTML into a component.

Initial Situation

For example, if I had the following fragment:

my-page.svelte:

<div>
    <i class="fas fa-{icon}"></i>
</div>

and I want to extract the <i> into its own component:

MyIcon.svelte:

<script>
    export let icon;
</script>

<i class="fas fa-{icon}"></i>

and use it as:

my-page.svelte:

import MyIcon from "./MyIcon.svelte"
...
<div>
    <MyIcon {icon} />
</div>

Problem

then the bundle size increases significantly only because I moved some HTML into a new component.

Probable Solution

So, I feel like if the Svelte compiler can figure out that MyIcon component is totally stateless, it can avoid generating an entire Svelte component for it. But rather make the component inline

So, after compilation (or transpilation?)

my-page.svelte:

import MyIcon from "./MyIcon.svelte"
...
<div>
    <MyIcon {icon} />
</div>

could become

my-page.svelte:

<div>
    <i class="fas fa-{icon}"></i>
</div>

I have seen tremendous bundle size reductions when avoiding separate components (when used in lots of places in my project). But loose the benefits of making them in their own component files.

@vipero07
Copy link

vipero07 commented Dec 5, 2019

Issue #3898 directly relates to this.

@vipero07
Copy link

vipero07 commented Dec 5, 2019

I'm not so sure your solution of not using components has the benefits you think it does. Consider this REPL https://svelte.dev/repl/5a97ad55d3834ab595fdd4996c7f6fd6?version=3.15.0
In that REPL (which is a slightly modified version of your original one) you can see RepeatComponent generates 314 lines of code and no component generates 345. So even though the fairly simple stateless component itself is 59 lines of code, the more it is reused, the less other lines are generated.

There is also the obvious downside of, if not using a component, making any changes to the uses will be a nightmare. Like needing to find all uses of class="fas fa- and replace that with some other icon class library.

You should also probably consider minification and gzipping, where repeat uses of the same functions are trivialized by gzip. That isn't to say it can't be handled differently, but I don't believe the bundle size is significantly increased by making components, especially the more they are reused.

@lgirma
Copy link
Author

lgirma commented Dec 6, 2019

@vipero07 Run some tests using your unmodified code from the REPL you posted above

I downloaded it into a zip file and modified only App.svelte then run yarn build. Results:

  • Using only NoComponent and removing RepeatComponent bundle size: 5.68 KB
  • Using only RepeatComponent and removing NoComponent bundle size: 6.44 KB

Note that the build command produces a minified bundle.

@vipero07
Copy link

vipero07 commented Dec 6, 2019

Fair, however you are forgoing OOP principals like DRY in favor of 0.78 KB (in this case). I get that having a bunch of smaller components this may add up but I imagine the actual difference between many components and copy pasta is just as trivial. 1KB is nothing compared to something like the entire React library. I'm not railing against the idea of helping reduce the overall size. However personally I'd avoid copy pasta or any of the other solutions for that small a savings, and code with the expectation that a future release implements #3898 or something similar.

Consider that change occurs in the next release and now you have to refactor everything.

@benmccann
Copy link
Member

Overview

Svelte generates classes that are able to reconcile changes to data. However, quite often I find myself knowing that the data will change completely when updated and that there is no UI that can store user state like form fields. In this case, the reconciliation may be largely unnecessary and we would do just as well to blast away what's there and start anew.

As an example, on the homepage of hn.svelte.dev, if I hit "More..." to go to the next page then there's probably not a need to compare the new data to the old data. I don't need to individually check if item.domain, item.url, item.id, item.title, item.user, item.comments_count, etc. changed. If I got a new item I'm fine assuming they all changed. That allows the component to be much smaller and dumber

Benefits

This change would have two large benefits:

  • Smaller file size. E.g. 20% of lines in hn.svelte.dev's [page].js are the p methods and those could be removed. This would result in faster network transfer as well as reduced script parsing times.
  • Hydration could potentially be much cheaper. The main reason we do hydration is to make sure the client UI is in sync with the client's data state. However, if we don't store data state on the client for some components because we don't do reconciliation, then possibly we don't need to update the UI or even transfer the data to the client in the first place. This could be a cool way of doing incremental/partial hydration by allowing to basically skip hydration on a per component-basis. This might make the file size 10% smaller still not to mention the runtime improvements, which would be substantial

Drawback

In terms of costs, there is likely some savings we get today by reusing the existing DOM structure that we would lose. However, most of any savings could be gained back by simply working on optimizing fragment creation (#3898). E.g. by creating a template and cloning it instead of recreating the DOM structure for each instance.

Implementation

I'm thinking this would be specified in <svelte:options>. Perhaps something like <svelte:options reconcile=false />. There may be cases where you would want to call a component in a reconciled fashion and non-reconciled fashion. In that case you would simply use the standard reconciled component everywhere. Once you are including that extra code in your app in one place, there's not much need to do something different elsewhere

Though I wonder if there might be some other way to accomplish this as well. It almost feels like the combination of immutable and a keyed each block should give this to me

I implemented this for the page component of the hn.svelte.dev example just by editing the output of the compiler as can be seen below. create_fragment initialized some values that I had to update in p by duplicating the initialization code and that could be refactored out into a separate function to reduce the duplication if desired.

m: function mount(target, anchor) {
	this.target = target;
	...
},
p: function update(ctx, [dirty]) {
	t0_value = /*item*/ ctx[0].title + "";
	if_block0 = /*item*/ ctx[0].domain && create_if_block_1(ctx);
	var anchor = article.nextElementSibling;
	this.d(true);
	this.c();
	this.m(this.target, anchor);
},

Unanswered questions

  • What to call this?
  • I'm not that familiar with Svelte internals, so I'm sure there are things I'm overlooking that might be challenges. But this seems powerful enough that it'd be interesting to brainstorm if it can be made to work

@pngwn pngwn added popular more than 20 upthumbs feature request compiler Changes relating to the compiler temp-stale and removed proposal labels Jun 26, 2021
@Madd0g
Copy link

Madd0g commented Nov 12, 2021

is there a better or more recent discussion about island architecture or partial hydration with svelte? I saw several issues opened about this (across different repos) but it doesn't seem like anything has been accepted as the plan?

As for myself, I think it would be cool to control what the default is, for mostly static sites, I would want to opt-into hydration and for mostly dynamic sites, I would want to be able to opt out.

But is this the right place to talk about this?

@MrHBS
Copy link

MrHBS commented Mar 29, 2024

This issue could use some love.

@dummdidumm
Copy link
Member

Svelte 5 solves this through better compiler output (example), therefore closing.

@dominikg
Copy link
Member

dominikg commented Mar 29, 2024

svelte5 output is very different from svelte3/4:

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAA42OPQvCMBBA_0o4hCqEZi9pwc3d0TiU5gqBfJFcK1L63yUoLeLieI97d2-B0VjM0NwW8L1DaOAcI3CgZyxDntESAoccpjQUIvOQTKROeUXGxZCIXakntJgzG1NwrKrFRur3gUp5KXbRy10pX1sFj5CsViA64OCCNqNBDQ2lCVe-pW3Wv4FD8JnYUvSVtewQU4j5ePqqkdrM3QWtDZ9FKQr57bivL-hXroIuAQAA

// Stateless.svelte (Svelte v5.0.0-next.87)
// Note: compiler output will change before 5.0 is released!
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";

var root = $.template(`<div> </div>`);

export default function Stateless($$anchor, $$props) {
	$.push($$props, true);

	var div = root();
	var text = $.child(div);

	$.render_effect(() => $.set_text(text, `Hello ${$.stringify($$props.name)}`));
	$.append($$anchor, div);
	$.pop();
}

svelte 4:
https://svelte.dev/repl/68e9a85b41a0416496e17e9742d1b535?version=4.2.12

/* Stateless.svelte generated by Svelte v4.2.12 */
import {
	SvelteComponent,
	append,
	detach,
	element,
	init,
	insert,
	noop,
	safe_not_equal,
	set_data,
	text
} from "svelte/internal";

import "svelte/internal/disclose-version";

function create_fragment(ctx) {
	let div;
	let t0;
	let t1;

	return {
		c() {
			div = element("div");
			t0 = text("Hello ");
			t1 = text(/*name*/ ctx[0]);
		},
		m(target, anchor) {
			insert(target, div, anchor);
			append(div, t0);
			append(div, t1);
		},
		p(ctx, [dirty]) {
			if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]);
		},
		i: noop,
		o: noop,
		d(detaching) {
			if (detaching) {
				detach(div);
			}
		}
	};
}

function instance($$self, $$props, $$invalidate) {
	let { name } = $$props;

	$$self.$$set = $$props => {
		if ('name' in $$props) $$invalidate(0, name = $$props.name);
	};

	return [name];
}

class Stateless extends SvelteComponent {
	constructor(options) {
		super();
		init(this, options, instance, create_fragment, safe_not_equal, { name: 0 });
	}
}

export default Stateless;

and just now sniped by @dummdidumm 😂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler Changes relating to the compiler feature request perf popular more than 20 upthumbs
Projects
None yet
Development

No branches or pull requests

10 participants