svelte icon indicating copy to clipboard operation
svelte copied to clipboard

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

Open sherifsalah opened this issue 1 year ago • 13 comments

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

sherifsalah avatar Jun 23 '23 18:06 sherifsalah

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

mnorlin avatar Jun 25 '23 20:06 mnorlin

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?

dummdidumm avatar Jun 26 '23 11:06 dummdidumm

@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 avatar Jun 26 '23 14:06 sherifsalah

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 avatar Jun 26 '23 15:06 sherifsalah

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}
		`

sherifsalah avatar Jun 26 '23 15:06 sherifsalah

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.

dummdidumm avatar Jun 27 '23 14:06 dummdidumm

@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 avatar Jul 04 '23 10:07 sherifsalah

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

sherifsalah avatar Jul 04 '23 12:07 sherifsalah

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.

dummdidumm avatar Jul 04 '23 12:07 dummdidumm

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

sherifsalah avatar Jul 04 '23 14:07 sherifsalah

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.

dummdidumm avatar Jul 04 '23 15:07 dummdidumm

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.

sherifsalah avatar Jul 04 '23 15:07 sherifsalah

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??

yujonglee avatar Jun 17 '24 12:06 yujonglee

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:options tag="svelte-counter" />

<script lang="ts">
  import "../app.css";

  export let backgroundColor = "white";
  export let color = "black";
  export let rounded = "false";

  let count = 0;

  function decrement() {
    count -= 1;
  }

  function increment() {
    count += 1;
  }
</script>

<div
  class="p-4 shadow-md"
  role="banner"
  style={`background-color: ${backgroundColor}; color: ${color}; border-radius: ${rounded === "true" ? "1rem" : "0"};`}
>
  <h2 class="bg-green-500">Shopify Theme App Extension with Svelte</h2>
  <p>
    This is a simple counter component that is rendered as a custom element.
  </p>

  <div class="h-10 w-32">
    <div
      class="flex flex-row h-10 w-full rounded-lg relative bg-transparent mt-1"
    >
      <button
        on:click={decrement}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-l cursor-pointer outline-none"
      >
        <span class="m-auto text-2xl font-thin">−</span>
      </button>
      <span
        class="flex-1 focus:outline-none text-center w-full bg-gray-300 font-semibold text-md hover:text-black focus:text-black md:text-base cursor-default flex items-center text-gray-700"
      >
        {count}
      </span>
      <button
        on:click={increment}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-r cursor-pointer"
      >
        <span class="m-auto text-2xl font-thin">+</span>
      </button>
    </div>
  </div>
</div>

Svelte 4 | Counter.svelte
<svelte:options
  customElement={{
    tag: "svelte-counter",
    shadow: "open",
  }}
/>

<script lang="ts">
  import "../app.css";

  export let backgroundColor = "white";
  export let color = "black";
  export let rounded = "false";

  let count = 0;

  function decrement() {
    count -= 1;
  }

  function increment() {
    count += 1;
  }
</script>

<div
  class="p-4 shadow-md"
  role="banner"
  style={`background-color: ${backgroundColor}; color: ${color}; border-radius: ${rounded === "true" ? "1rem" : "0"};`}
>
  <h2 class="bg-green-500">Shopify Theme App Extension with Svelte</h2>
  <p>
    This is a simple counter component that is rendered as a custom element.
  </p>

  <div class="h-10 w-32">
    <div
      class="flex flex-row h-10 w-full rounded-lg relative bg-transparent mt-1"
    >
      <button
        on:click={decrement}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-l cursor-pointer outline-none"
      >
        <span class="m-auto text-2xl font-thin">−</span>
      </button>
      <span
        class="flex-1 focus:outline-none text-center w-full bg-gray-300 font-semibold text-md hover:text-black focus:text-black md:text-base cursor-default flex items-center text-gray-700"
      >
        {count}
      </span>
      <button
        on:click={increment}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-r cursor-pointer"
      >
        <span class="m-auto text-2xl font-thin">+</span>
      </button>
    </div>
  </div>
</div>

Svelte 5 | Counter.svelte
<svelte:options
  customElement={{
    tag: "svelte-counter",
    shadow: "open",
  }}
/>

<script lang="ts">
  import "../app.css";

  const {
    backgroundColor = "white",
    color = "black",
    rounded = "false",
  } = $props();

  let count = $state(0);

  function decrement() {
    count -= 1;
  }

  function increment() {
    count += 1;
  }
</script>

<div
  class="p-4 shadow-md"
  role="banner"
  style={`background-color: ${backgroundColor}; color: ${color}; border-radius: ${rounded === "true" ? "1rem" : "0"};`}
>
  <h2 class="bg-green-500">Shopify Theme App Extension with Svelte</h2>
  <p>
    This is a simple counter component that is rendered as a custom element.
  </p>

  <div class="h-10 w-32">
    <div
      class="flex flex-row h-10 w-full rounded-lg relative bg-transparent mt-1"
    >
      <button
        onclick={decrement}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-l cursor-pointer outline-none"
      >
        <span class="m-auto text-2xl font-thin">−</span>
      </button>
      <span
        class="flex-1 focus:outline-none text-center w-full bg-gray-300 font-semibold text-md hover:text-black focus:text-black md:text-base cursor-default flex items-center text-gray-700"
      >
        {count}
      </span>
      <button
        onclick={increment}
        class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-r cursor-pointer"
      >
        <span class="m-auto text-2xl font-thin">+</span>
      </button>
    </div>
  </div>
</div>
SolidJs | Counter.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 (
      <div
        style={{
          "background-color": backgroundColor,
          color: color,
          "border-radius": rounded === "true" ? "1rem" : "0",
        }}
        class="p-4 shadow-md"
        role="banner"
      >
        <h2 class="bg-green-500">Shopify Theme App Extension with SolidJs</h2>
        <p>
          This is a simple counter component that is rendered as a custom
          element.
        </p>

        <div class="h-10 w-32">
          <div class="flex flex-row h-10 w-full rounded-lg relative bg-transparent mt-1">
            <button
              onClick={() => setCount((prev) => prev - 1)}
              class=" bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-l cursor-pointer outline-none"
            >
              <span class="m-auto text-2xl font-thin">−</span>
            </button>
            <span class="flex-1 focus:outline-none text-center w-full bg-gray-300 font-semibold text-md hover:text-black focus:text-black md:text-basecursor-default flex items-center text-gray-700 outline-none">
              {count()}
            </span>
            <button
              onClick={() => setCount((prev) => prev + 1)}
              class="bg-gray-300 text-gray-600 hover:text-gray-700 hover:bg-gray-400 h-full w-20 rounded-r cursor-pointer"
            >
              <span class="m-auto text-2xl font-thin">+</span>
            </button>
          </div>
        </div>
      </div>
    );
  },
);

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

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

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 :)

bennobuilder avatar Jul 22 '24 06:07 bennobuilder

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.

dummdidumm avatar Jul 22 '24 07:07 dummdidumm

@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:options
  customElement={{
    tag: "svelte-reviews",
    shadow: "open",
  }}
/>

<script lang="ts">
  import "../app.css";
  import { StarIcon } from "../components";

  function classNames(...classes: string[]) {
    return classes.filter(Boolean).join(" ");
  }

  let reviews = {
    average: 4,
    totalCount: 1624,
    counts: [
      { rating: 5, count: 1019 },
      { rating: 4, count: 162 },
      { rating: 3, count: 97 },
      { rating: 2, count: 199 },
      { rating: 1, count: 147 },
    ],
    featured: [
      {
        id: 1,
        rating: 5,
        content: `
            This is the bag of my dreams. I took it on my last vacation and was able to fit an absurd amount of snacks for the many long and hungry flights.
          `,
        author: "Emily Selman",
        avatarSrc:
          "https://images.unsplash.com/photo-1502685104226-ee32379fefbe?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80",
      },
      {
        id: 2,
        rating: 5,
        content: `
          Before getting the Ruck Snack, I struggled my whole life with pulverized snacks, endless crumbs, and other heartbreaking snack catastrophes. Now, I can stow my snacks with confidence and style!
          `,
        author: "Hector Gibbons",
        avatarSrc:
          "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=8&w=256&h=256&q=80",
      },
      {
        id: 3,
        rating: 4,
        content: `
        I love how versatile this bag is. It can hold anything ranging from cookies that come in trays to cookies that come in tins.
          `,
        author: "Mark Edwards",
        avatarSrc:
          "https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?ixlib=rb-1.2.1&ixqx=oilqXxSqey&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
      },
    ],
  };
</script>

<div class="bg-white">
  <div
    class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:grid lg:max-w-7xl lg:grid-cols-12 lg:gap-x-8 lg:px-8 lg:py-32"
  >
    <div class="lg:col-span-4">
      <h2 class="text-2xl font-bold tracking-tight text-gray-900">
        Customer Reviews
      </h2>

      <div class="mt-3 flex items-center">
        <div>
          <div class="flex items-center">
            {#each Array(5).fill(0) as _, index}
              <StarIcon
                aria-hidden="true"
                class={classNames(
                  reviews.average > index ? "text-yellow-400" : "text-gray-300",
                  "h-5 w-5 flex-shrink-0",
                )}
              />
            {/each}
          </div>
          <p class="sr-only">{reviews.average} out of 5 stars</p>
        </div>
        <p class="ml-2 text-sm text-gray-900">
          Based on {reviews.totalCount} reviews
        </p>
      </div>

      <div class="mt-6">
        <h3 class="sr-only">Review data</h3>

        <dl class="space-y-3">
          {#each reviews.counts as count}
            <div class="flex items-center text-sm">
              <dt class="flex flex-1 items-center">
                <p class="w-3 font-medium text-gray-900">
                  {count.rating}
                  <span class="sr-only"> star reviews</span>
                </p>
                <div aria-hidden="true" class="ml-1 flex flex-1 items-center">
                  <StarIcon
                    aria-hidden="true"
                    class={classNames(
                      count.count > 0 ? "text-yellow-400" : "text-gray-300",
                      "h-5 w-5 flex-shrink-0",
                    )}
                  />

                  <div class="relative ml-3 flex-1">
                    <div
                      class="h-3 rounded-full border border-gray-200 bg-gray-100"
                    ></div>
                    {#if count.count > 0}
                      <div
                        style="width: calc({count.count} / {reviews.totalCount} * 100%)"
                        class="absolute inset-y-0 rounded-full border border-yellow-400 bg-yellow-400"
                      ></div>
                    {/if}
                  </div>
                </div>
              </dt>
              <dd
                class="ml-3 w-10 text-right text-sm tabular-nums text-gray-900"
              >
                {Math.round((count.count / reviews.totalCount) * 100)}%
              </dd>
            </div>
          {/each}
        </dl>
      </div>

      <div class="mt-10">
        <h3 class="text-lg font-medium text-gray-900">Share your thoughts</h3>
        <p class="mt-1 text-sm text-gray-600">
          If you’ve used this product, share your thoughts with other customers
        </p>

        <a
          href="#"
          class="mt-6 inline-flex w-full items-center justify-center rounded-md border border-gray-300 bg-white px-8 py-2 text-sm font-medium text-gray-900 hover:bg-gray-50 sm:w-auto lg:w-full"
        >
          Write a review
        </a>
      </div>
    </div>

    <div class="mt-16 lg:col-span-7 lg:col-start-6 lg:mt-0">
      <h3 class="sr-only">Recent reviews</h3>

      <div class="flow-root">
        <div class="-my-12 divide-y divide-gray-200">
          {#each reviews.featured as review}
            <div class="py-12">
              <div class="flex items-center">
                <img
                  alt="{review.author}."
                  src={review.avatarSrc}
                  class="h-12 w-12 rounded-full"
                />
                <div class="ml-4">
                  <h4 class="text-sm font-bold text-gray-900">
                    {review.author}
                  </h4>
                  <div class="mt-1 flex items-center">
                    {#each Array(5).fill(0) as _, index}
                      <StarIcon
                        aria-hidden="true"
                        class={classNames(
                          review.rating > index
                            ? "text-yellow-400"
                            : "text-gray-300",
                          "h-5 w-5 flex-shrink-0",
                        )}
                      />
                    {/each}
                  </div>
                  <p class="sr-only">{review.rating} out of 5 stars</p>
                </div>
              </div>

              <div class="mt-4 space-y-6 text-base italic text-gray-600">
                {@html review.content}
              </div>
            </div>
          {/each}
        </div>
      </div>
    </div>
  </div>
</div>
<script lang="ts">
  import type { HTMLAttributes } from "svelte/elements";

  interface TProps extends HTMLAttributes<SVGElement> {}

  let props: TProps = $props();
</script>

<svg
  width={"15"}
  height={"15"}
  viewBox="0 0 15 15"
  fill={"none"}
  xmlns="http://www.w3.org/2000/svg"
  {...props}
>
  <path
    d="M7.22303 0.665992C7.32551 0.419604 7.67454 0.419604 7.77702 0.665992L9.41343 4.60039C9.45663 4.70426 9.55432 4.77523 9.66645 4.78422L13.914 5.12475C14.18 5.14607 14.2878 5.47802 14.0852 5.65162L10.849 8.42374C10.7636 8.49692 10.7263 8.61176 10.7524 8.72118L11.7411 12.866C11.803 13.1256 11.5206 13.3308 11.2929 13.1917L7.6564 10.9705C7.5604 10.9119 7.43965 10.9119 7.34365 10.9705L3.70718 13.1917C3.47945 13.3308 3.19708 13.1256 3.25899 12.866L4.24769 8.72118C4.2738 8.61176 4.23648 8.49692 4.15105 8.42374L0.914889 5.65162C0.712228 5.47802 0.820086 5.14607 1.08608 5.12475L5.3336 4.78422C5.44573 4.77523 5.54342 4.70426 5.58662 4.60039L7.22303 0.665992Z"
    fill="currentColor"
  ></path>
</svg>

bennobuilder avatar Jul 22 '24 07:07 bennobuilder