Svelte: Stateless lightweight components

Created on 27 Nov 2019  路  7Comments  路  Source: sveltejs/svelte

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

internals perf proposal

Most helpful comment

(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.

All 7 comments

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

(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.

Issue #3898 directly relates to this.

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.

@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.

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.

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 (https://github.com/sveltejs/svelte/issues/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
Was this page helpful?
0 / 5 - 0 ratings

Related issues

plumpNation picture plumpNation  路  3Comments

mmjmanders picture mmjmanders  路  3Comments

juniorsd picture juniorsd  路  3Comments

Rich-Harris picture Rich-Harris  路  3Comments

Rich-Harris picture Rich-Harris  路  3Comments