<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: HostingSift</title>
    <description>The latest articles on DEV Community by HostingSift (@hostingsift).</description>
    <link>https://dev.to/hostingsift</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3781248%2Fb9ccc1bf-a147-491d-a8ef-32f6dcafac23.png</url>
      <title>DEV Community: HostingSift</title>
      <link>https://dev.to/hostingsift</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hostingsift"/>
    <language>en</language>
    <item>
      <title>The Raw Power of Svelte 5 Runes: 70 Components, 25 KB, Full Reactivity</title>
      <dc:creator>HostingSift</dc:creator>
      <pubDate>Wed, 25 Feb 2026 13:14:30 +0000</pubDate>
      <link>https://dev.to/hostingsift/the-raw-power-of-svelte-5-runes-70-components-25-kb-full-reactivity-2npp</link>
      <guid>https://dev.to/hostingsift/the-raw-power-of-svelte-5-runes-70-components-25-kb-full-reactivity-2npp</guid>
      <description>&lt;h1&gt;
  
  
  Svelte 5 Runes in Production: What We Learned Building 70+ Components
&lt;/h1&gt;

&lt;p&gt;When Svelte 5 shipped runes, the community split in two. One side said "you're just copying React." The other saw the future of reactivity. We just started writing code. 70+ components later, powering &lt;a href="https://hostingsift.com" rel="noopener noreferrer"&gt;HostingSift&lt;/a&gt; (a hosting comparison platform), here's what we actually know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runes are and why you should care
&lt;/h2&gt;

&lt;p&gt;Runes are compiler instructions. They look like functions (&lt;code&gt;$state()&lt;/code&gt;, &lt;code&gt;$derived()&lt;/code&gt;, &lt;code&gt;$effect()&lt;/code&gt;), but they're not. Svelte transforms them at build time, not at runtime. That's the key difference from React hooks, Vue composables, and everything else out there.&lt;/p&gt;

&lt;p&gt;In Svelte 4, reactivity was implicit. Write &lt;code&gt;let count = 0&lt;/code&gt; and the compiler decides when to update the DOM. It worked, but it had traps. &lt;code&gt;$:&lt;/code&gt; blocks got confusing with complex logic, and the line between "reactive" and "just a variable" was blurry.&lt;/p&gt;

&lt;p&gt;Runes fix that with explicit signals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hostings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Hosting&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentSort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalPages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$derived&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="nf"&gt;$effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentQueryString&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;fetchHostings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You read it and know exactly what's reactive. No magic. No guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reactivity model: what actually happens under the hood
&lt;/h2&gt;

&lt;p&gt;Here's the thing most articles skip. Svelte 5's reactivity is signal-based, but the compiler does the dependency tracking for you. You never call &lt;code&gt;createSignal()&lt;/code&gt; or wrap things in &lt;code&gt;ref()&lt;/code&gt;. You write normal JavaScript and the compiler figures out the reactive graph.&lt;/p&gt;

&lt;p&gt;When you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;let loading = $state(true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler turns it into something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every read of &lt;code&gt;loading&lt;/code&gt; in the template or in a &lt;code&gt;$derived&lt;/code&gt;/&lt;code&gt;$effect&lt;/code&gt; block registers a dependency. Every write triggers updates to anything that depends on it. No manual dependency arrays. No stale closure bugs.&lt;/p&gt;

&lt;p&gt;This is fundamentally different from React's model. In React, &lt;code&gt;useEffect&lt;/code&gt; re-runs based on a dependency array &lt;em&gt;you&lt;/em&gt; maintain. Forget a dependency, get a stale value. Add too many, get infinite loops. Svelte tracks dependencies automatically because it controls the compiler. The reactive graph is built at compile time, not runtime.&lt;/p&gt;

&lt;p&gt;Compared to Vue's &lt;code&gt;ref()&lt;/code&gt; / &lt;code&gt;reactive()&lt;/code&gt;, you also skip the &lt;code&gt;.value&lt;/code&gt; ceremony. In Vue you write &lt;code&gt;count.value++&lt;/code&gt;. In Svelte you write &lt;code&gt;count++&lt;/code&gt;. The compiler knows it's reactive and generates the right code.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we built a reactive filter system with runes
&lt;/h2&gt;

&lt;p&gt;This is where runes really clicked for us. Our &lt;a href="https://hostingsift.com/hosting" rel="noopener noreferrer"&gt;hosting comparison page&lt;/a&gt; has multiple filter dimensions: hosting type, technology stack, price range, search, sort order, pagination. Each filter lives in its own nanostore atom, and they all feed into a computed API query string.&lt;/p&gt;

&lt;p&gt;Here's the store layer (plain nanostores, framework-agnostic):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stores/filters.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nanostores&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$tech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$minPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$maxPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$sort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price-asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;atom&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// This recomputes whenever ANY filter changes&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$apiQueryString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;$type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$tech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$minPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$maxPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;$search&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tech&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven independent atoms feeding into one computed query string. When you change any filter, &lt;code&gt;$apiQueryString&lt;/code&gt; recomputes and the list refetches. That's the reactive graph in action.&lt;/p&gt;

&lt;p&gt;Now here's the interesting part: bridging this into Svelte 5 components.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bridge pattern we use everywhere
&lt;/h3&gt;

&lt;p&gt;Nanostores don't know about runes. So we bridge them with a pattern that shows up in almost every component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$type&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../stores/filters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;activeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="nf"&gt;$effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;activeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines: initial value from the store, subscribe in an effect, cleanup returned automatically. Every filter component does this. &lt;code&gt;TypeTabs&lt;/code&gt;, &lt;code&gt;TechFilter&lt;/code&gt;, &lt;code&gt;PriceRangeSlider&lt;/code&gt;, &lt;code&gt;ActiveFilters&lt;/code&gt; all follow the same pattern.&lt;/p&gt;

&lt;p&gt;For components that track multiple stores, we batch the subscriptions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;$effect(() =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;techStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentTech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;minPriceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentMinPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;maxPriceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentMaxPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;priceMaxStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;priceFloorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One effect, six subscriptions, one cleanup function. The effect runs once on mount, subscribes to everything, and cleans up all listeners when the component is destroyed. In React you'd need &lt;code&gt;useEffect&lt;/code&gt; with a dependency array (probably wrong on the first try) or multiple &lt;code&gt;useEffect&lt;/code&gt; calls. Here it's flat and obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  $derived for computed filter state
&lt;/h3&gt;

&lt;p&gt;The real power shows when you start deriving values. Our &lt;code&gt;ActiveFilters&lt;/code&gt; component needs to know if any filter is active:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;const hasFilters = $derived(
  currentType !== null ||
  currentTech.length &amp;gt; 0 ||
  currentMinPrice &amp;gt; floor ||
  currentMaxPrice &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nt"&gt;ceiling&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;useMemo&lt;/code&gt;. No cache key. Just an expression that re-evaluates when any of its dependencies change. Svelte tracks which &lt;code&gt;$state&lt;/code&gt; variables you read inside &lt;code&gt;$derived&lt;/code&gt; and only recalculates when those specific values change.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TechFilter&lt;/code&gt; component uses &lt;code&gt;$derived&lt;/code&gt; for show/hide logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;const visibleOptions = $derived(
  showAll ? techOptions : techOptions.slice(0, VISIBLE_COUNT)
)
const hiddenCount = $derived(
  Math.max(0, techOptions.length - VISIBLE_COUNT)
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;showAll&lt;/code&gt; flips or &lt;code&gt;techOptions&lt;/code&gt; changes (after the API responds), both derived values update and the template re-renders. You don't think about it. It just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The price slider: reactive all the way down
&lt;/h3&gt;

&lt;p&gt;Our dual-range price slider is the most reactive component we have. It juggles local drag state, store values, computed percentages, and dynamically generated pip labels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;let localMin = $state(minPriceStore.get())
let localMax = $state(maxPriceStore.get())
let ceiling = $state(priceMaxStore.get())
let isDragging = $state(false)

// Sync from store, but not while dragging
$effect(() =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;minPriceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDragging&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;localMin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;)
  const unsubMax = maxPriceStore.subscribe(val =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDragging&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;localMax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;)
  // ...
  return () =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;unsubMin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;unsubMax&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;
})

// These recompute on every drag frame
const range = $derived(ceiling - floor)
const minPercent = $derived(range &amp;gt; 0 ? ((localMin - floor) / range) * 100 : 0)
const maxPercent = $derived(range &amp;gt; 0 ? ((localMax - floor) / range) * 100 : 100)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the user drags, &lt;code&gt;localMin&lt;/code&gt;/&lt;code&gt;localMax&lt;/code&gt; update, which triggers &lt;code&gt;minPercent&lt;/code&gt;/&lt;code&gt;maxPercent&lt;/code&gt; to recompute, which moves the slider fill in the template. When they release, we commit to the store, which triggers the API fetch. The &lt;code&gt;isDragging&lt;/code&gt; guard prevents the store subscription from fighting with the local drag state.&lt;/p&gt;

&lt;p&gt;In React you'd model this with &lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useRef&lt;/code&gt; + &lt;code&gt;useCallback&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt; with carefully managed dependency arrays. Here it's just assignments and derived values. The compiler handles the rest.&lt;/p&gt;

&lt;p&gt;The pip labels are a &lt;code&gt;$derived.by()&lt;/code&gt; because the logic needs intermediate variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;const pips = $derived.by(() =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`$${floor}`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rough&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;range&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rough&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rough&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;mag&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;mag&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;norm&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mag&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;mag&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`$${floor}`&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`$${v}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;
  return labels
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$derived()&lt;/code&gt; is for single expressions. &lt;code&gt;$derived.by()&lt;/code&gt; is for when you need &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;for&lt;/code&gt;, or temporary variables. Both are reactive the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  $effect: the cleanup story
&lt;/h2&gt;

&lt;p&gt;The killer feature of &lt;code&gt;$effect&lt;/code&gt; isn't the effect itself. It's the cleanup. You return a function and Svelte calls it when dependencies change or the component unmounts. No &lt;code&gt;onDestroy&lt;/code&gt; import, no separate cleanup lifecycle.&lt;/p&gt;

&lt;p&gt;Click outside handler with automatic cleanup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;$effect(() =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleClickOutside&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleClickOutside&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener only exists while the dropdown is open. Close it, listener gone. Unmount the component, listener gone. You can't forget.&lt;/p&gt;

&lt;p&gt;Body scroll lock for a mobile bottom sheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;let isOpen = $state(false)

function open() &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;

function close() &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No effect needed here because the state change is synchronous and user-driven. That's another thing runes teach you: not everything needs to be reactive. Sometimes a plain function is better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simplicity argument: less code, fewer bugs
&lt;/h2&gt;

&lt;p&gt;Let's be blunt. Every framework claims to be simple. But simplicity isn't about marketing copy. It's about how much code you write for the same result, and how many places that code can go wrong.&lt;/p&gt;

&lt;p&gt;Here's a toggle button. React:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Svelte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three are one-liners. Fair enough. Now let's look at something real: a component that reads from an external store, derives a computed value, and cleans up on unmount.&lt;/p&gt;

&lt;p&gt;React version of our TypeTabs filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;TypeTabs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;activeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActiveType&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setActiveType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;unsub&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap gap-2"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`chip &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;activeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handleClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Svelte version. The actual code we ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"ts"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$type&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../stores/filters&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;activeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$state&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="nf"&gt;$effect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;activeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-wrap gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;#each&lt;/span&gt; &lt;span class="nx"&gt;TYPES&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
      &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"chip {activeType === t.value ? 'active' : ''}"&lt;/span&gt;
      &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;/each&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Count the concepts. React: &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useCallback&lt;/code&gt;, dependency array, JSX return statement, &lt;code&gt;className&lt;/code&gt; instead of &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt; prop. Svelte: &lt;code&gt;$state&lt;/code&gt;, &lt;code&gt;$effect&lt;/code&gt;, template. That's it.&lt;/p&gt;

&lt;p&gt;The React version has more ways to break. Forget the dependency array, you get stale state. Skip &lt;code&gt;useCallback&lt;/code&gt;, you get unnecessary re-renders (or your linter screams at you). The Svelte version has one moving part: the store subscription with automatic cleanup.&lt;/p&gt;

&lt;p&gt;Now look at our &lt;code&gt;ActiveFilters&lt;/code&gt; component. It subscribes to 6 stores and derives a boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;let currentType = $state(typeStore.get())
let currentTech = $state(techStore.get())
let currentMinPrice = $state(minPriceStore.get())
let currentMaxPrice = $state(maxPriceStore.get())
let ceiling = $state(priceMaxStore.get())
let floor = $state(priceFloorStore.get())

$effect(() =&amp;gt; &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unsubs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;typeStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;techStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentTech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;minPriceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentMinPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;maxPriceStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;currentMaxPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;priceMaxStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;priceFloorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;floor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unsubs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsub&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;unsub&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;)

const hasFilters = $derived(
  currentType !== null ||
  currentTech.length &amp;gt; 0 ||
  currentMinPrice &amp;gt; floor ||
  currentMaxPrice &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nt"&gt;ceiling&lt;/span&gt;
&lt;span class="err"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In React, this would be six &lt;code&gt;useState&lt;/code&gt; calls, a &lt;code&gt;useEffect&lt;/code&gt; with an empty dependency array, and a &lt;code&gt;useMemo&lt;/code&gt; with six dependencies (that you'd probably get wrong on the first try). Or you'd pull in Zustand or Jotai and add another abstraction layer.&lt;/p&gt;

&lt;p&gt;In Svelte, it's assignments and one effect. &lt;code&gt;$derived&lt;/code&gt; doesn't need you to list dependencies. The compiler reads the expression and tracks them automatically.&lt;/p&gt;

&lt;p&gt;This scales. Our most complex component (&lt;code&gt;HostingList&lt;/code&gt;) has 12 &lt;code&gt;$state&lt;/code&gt; declarations, 4 &lt;code&gt;$derived&lt;/code&gt; values, 5 &lt;code&gt;$effect&lt;/code&gt; blocks, and handles search, sort, filters, pagination, and compare selection. It's 286 lines including the template. The equivalent React component would easily be 400+, and most of that extra code would be dependency arrays, memoization, and state setter functions.&lt;/p&gt;

&lt;p&gt;The point isn't that React is bad. It's that Svelte's compiler does work that React pushes onto the developer. Less manual bookkeeping means fewer places to mess up.&lt;/p&gt;

&lt;h2&gt;
  
  
  $props: TypeScript-first component contracts
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;interface Props &lt;span class="si"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CompareItem&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;onRemove&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nx"&gt;onClear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nx"&gt;onCompare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;span class="si"&gt;}&lt;/span&gt;

let &lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onRemove&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClear&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onCompare&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;: Props = $props()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of &lt;code&gt;export let items&lt;/code&gt; and hoping the parent passes the right type. TypeScript catches mistakes at compile time. Default values work with standard destructuring. Callbacks are just props, same as React.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bundle size: the actual numbers
&lt;/h2&gt;

&lt;p&gt;Svelte 5 compiles runes to vanilla JS at build time. There's no 40+ KB runtime (React) or 30+ KB runtime (Vue) shipped to the browser. Svelte's runtime is under 5 KB gzipped and tree-shakes aggressively.&lt;/p&gt;

&lt;p&gt;In our case: 70+ Svelte components including a quiz wizard, dual-range price slider, filter system, compare bar, review form, and a full admin panel. Client JS stays under 25 KB per page (gzipped, including routing). React's runtime alone is bigger than that.&lt;/p&gt;

&lt;p&gt;Runes add zero overhead. &lt;code&gt;$state(0)&lt;/code&gt; compiles to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;source&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$derived(items.length &amp;gt;= 2)&lt;/code&gt; becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;canCompare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler knows exactly which values are reactive and generates only the code it needs. No virtual DOM diffing. No runtime dependency tracking. Just surgical DOM updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised us
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Granular updates without effort.&lt;/strong&gt; When &lt;code&gt;hostings = newArray&lt;/code&gt; runs, Svelte doesn't re-render the whole list. It diffs the keyed &lt;code&gt;{#each}&lt;/code&gt; block and only touches changed DOM nodes. We never had to think about &lt;code&gt;React.memo&lt;/code&gt; or &lt;code&gt;shouldComponentUpdate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional effects actually work.&lt;/strong&gt; &lt;code&gt;$effect&lt;/code&gt; + &lt;code&gt;if&lt;/code&gt; + &lt;code&gt;return cleanup&lt;/code&gt; gives you fine-grained control in 5 lines. The click-outside handler above would be a custom hook in React (15+ lines, reusable but noisy).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript just works.&lt;/strong&gt; &lt;code&gt;$state&amp;lt;Hosting[]&amp;gt;([])&lt;/code&gt; does what you expect. No wrapper types, no utility generics, no &lt;code&gt;Ref&amp;lt;T&amp;gt;&lt;/code&gt; vs &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Derived chains are fast.&lt;/strong&gt; &lt;code&gt;floor&lt;/code&gt; changes, &lt;code&gt;range&lt;/code&gt; recomputes, &lt;code&gt;minPercent&lt;/code&gt; recomputes, &lt;code&gt;pips&lt;/code&gt; recomputes, DOM updates. Four levels of derived state and it's instant because each step only runs if its inputs changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What bites
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The nanostores bridge.&lt;/strong&gt; Four lines per store isn't terrible, but it's ceremony. We'll probably write a &lt;code&gt;useNanostore()&lt;/code&gt; utility at some point. For now the pattern is consistent and bug-free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;$effect grabs everything it reads.&lt;/strong&gt; If you read &lt;code&gt;currentQueryString&lt;/code&gt; inside an effect, the effect re-runs when it changes. This is correct behavior, but you need to be careful not to trigger infinite loops (effect writes to state that the effect reads).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrating from Svelte 4.&lt;/strong&gt; Not trivial. &lt;code&gt;$:&lt;/code&gt; blocks need to be split into &lt;code&gt;$derived&lt;/code&gt; and &lt;code&gt;$effect&lt;/code&gt;. &lt;code&gt;export let&lt;/code&gt; becomes &lt;code&gt;$props()&lt;/code&gt;. The good news: &lt;code&gt;npx sv migrate svelte-5&lt;/code&gt; handles 90% of cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rune&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$state()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mutable value&lt;/td&gt;
&lt;td&gt;&lt;code&gt;let loading = $state(true)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$derived()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Computed from state&lt;/td&gt;
&lt;td&gt;&lt;code&gt;const total = $derived(items.length)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$derived.by()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complex computed with if/for&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$derived.by(() =&amp;gt; { ... })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$effect()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Side effects, subscriptions&lt;/td&gt;
&lt;td&gt;Fetch, event listeners, store sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$props()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Component inputs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;let { name } = $props()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$bindable()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Two-way binding prop&lt;/td&gt;
&lt;td&gt;&lt;code&gt;let { value = $bindable() } = $props()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Runes make reactivity explicit. You know what's state, what's computed, what's a side effect. The compiler generates minimal code. TypeScript works without hacks. The reactive graph is built once at compile time and runs with near-zero overhead.&lt;/p&gt;

&lt;p&gt;We run 70+ components in production at &lt;a href="https://hostingsift.com" rel="noopener noreferrer"&gt;HostingSift&lt;/a&gt; with Astro SSR, nanostores for cross-island state, and Svelte 5 runes for local reactivity. A full filter system with type tabs, technology chips, a dual-range price slider, active filter pills, search with autocomplete, and sort dropdown. All reactive, all coordinated through a single computed query string, all under 25 KB shipped to the browser.&lt;/p&gt;

&lt;p&gt;If you're starting a new project and the React/Vue/Svelte decision is eating your time, just try runes. They're different enough to deserve a real look.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>javascript</category>
      <category>performance</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why we chose Astro over SvelteKit for our hosting comparison platform</title>
      <dc:creator>HostingSift</dc:creator>
      <pubDate>Mon, 23 Feb 2026 21:00:31 +0000</pubDate>
      <link>https://dev.to/hostingsift/why-we-chose-astro-over-sveltekit-for-our-hosting-comparison-platform-3cc6</link>
      <guid>https://dev.to/hostingsift/why-we-chose-astro-over-sveltekit-for-our-hosting-comparison-platform-3cc6</guid>
      <description>&lt;h1&gt;
  
  
  Why we chose Astro over SvelteKit for our hosting comparison platform
&lt;/h1&gt;

&lt;p&gt;We're building &lt;a href="https://hostingsift.com" rel="noopener noreferrer"&gt;HostingSift&lt;/a&gt;, a platform for comparing hosting providers side by side. Pricing tables, feature breakdowns, filters, user reviews. When we started, we had one decision that would shape everything else: SvelteKit or Astro?&lt;/p&gt;

&lt;p&gt;We went with Astro. Here's why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some context first
&lt;/h2&gt;

&lt;p&gt;HostingSift is a content-heavy site. We have 23+ hosting provider profiles, each with multiple plans, pricing tiers, feature specs, and user reviews. On top of that there are comparison pages, category listings, a recommendation quiz, and a blog.&lt;/p&gt;

&lt;p&gt;The interactive parts are specific and well-contained: filtering plans by billing period, toggling comparison tables, authentication flows, review forms. Most of what users actually see is static content that doesn't need JavaScript to render.&lt;/p&gt;

&lt;p&gt;This ratio matters. It shaped everything that followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  SvelteKit was the obvious choice
&lt;/h2&gt;

&lt;p&gt;We love Svelte. The reactivity model in Svelte 5 with runes is elegant. The developer experience is fantastic. SvelteKit is a mature, full-featured framework with file-based routing, SSR, API endpoints, form actions.&lt;/p&gt;

&lt;p&gt;So why not just use it?&lt;/p&gt;

&lt;p&gt;Because SvelteKit is an application framework. It assumes your pages are interactive by default. Every route ships a JavaScript bundle to hydrate the page client-side, even if the page is mostly static HTML.&lt;/p&gt;

&lt;p&gt;You can work around this. You can set &lt;code&gt;ssr: true&lt;/code&gt; and use &lt;code&gt;+page.server.ts&lt;/code&gt; to keep data fetching on the server. You can prerender specific routes. But you're swimming against the current. The defaults push you toward shipping more JavaScript than a content site needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Astro gets right for content sites
&lt;/h2&gt;

&lt;p&gt;Astro starts from the opposite end. Zero client JavaScript by default. Every page renders as static HTML until you explicitly opt in to interactivity.&lt;/p&gt;

&lt;p&gt;This is not a subtle difference. It changes how you think about building pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Islands, not full-page hydration
&lt;/h3&gt;

&lt;p&gt;With Astro, we compose pages from &lt;code&gt;.astro&lt;/code&gt; components (static HTML, zero JS) and Svelte components (interactive, with JS). The Svelte components are islands. They hydrate independently. The rest of the page is just HTML that the browser renders immediately.&lt;/p&gt;

&lt;p&gt;Our hosting profile page is a good example. The page layout, provider description, specs table, pros and cons list, SEO metadata... all static Astro components. Zero JavaScript shipped. Then we drop in a &lt;code&gt;PlanCard&lt;/code&gt; component with &lt;code&gt;client:load&lt;/code&gt; for the interactive pricing table where users toggle billing periods. A &lt;code&gt;ReviewForm&lt;/code&gt; for authenticated review submission. A &lt;code&gt;HeaderAuth&lt;/code&gt; for login state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
import Layout from '../../layouts/Layout.astro'
import PlanCard from '../../components/PlanCard.svelte'
import ReviewSection from '../../components/ReviewSection.svelte'
import SpecsTable from '../../components/SpecsTable.astro'

const { hosting, plans } = await fetchHostingData(slug)
---

&amp;lt;Layout title={hosting.name}&amp;gt;
  &amp;lt;article&amp;gt;
    &amp;lt;h1&amp;gt;{hosting.name}&amp;lt;/h1&amp;gt;
    &amp;lt;p&amp;gt;{hosting.description}&amp;lt;/p&amp;gt;

    &amp;lt;!-- Static HTML, no JS overhead --&amp;gt;
    &amp;lt;SpecsTable specs={hosting.specs} /&amp;gt;

    &amp;lt;!-- Interactive islands, hydrate independently --&amp;gt;
    &amp;lt;PlanCard client:load plans={plans} /&amp;gt;
    &amp;lt;ReviewSection client:load hostingId={hosting.id} /&amp;gt;
  &amp;lt;/article&amp;gt;
&amp;lt;/Layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each island hydrates on its own. If one fails, the rest of the page still works fine. In SvelteKit, the entire page is a single hydration unit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerendering is the default, not an opt-in
&lt;/h3&gt;

&lt;p&gt;In Astro's hybrid mode, pages are prerendered at build time unless you explicitly say otherwise. Our homepage, category pages, legal pages, blog posts... all static HTML files. Fast, cacheable, no server compute on each request.&lt;/p&gt;

&lt;p&gt;For pages that need fresh data (hosting profiles with live pricing, filtered listings), we mark them as server-rendered. It's explicit and intentional.&lt;/p&gt;

&lt;p&gt;SvelteKit takes the opposite approach. Everything is server-rendered by default. You opt in to prerendering per route. The mental model is reversed. For a site where the majority of pages can safely be static, Astro's default aligns better with reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  We still write Svelte
&lt;/h3&gt;

&lt;p&gt;This is what made the decision easy. Astro is not a replacement for Svelte. It's a composition layer around it.&lt;/p&gt;

&lt;p&gt;All our interactive components are pure Svelte 5 with runes, stores, reactive state. We didn't lose anything from the Svelte ecosystem. We just moved the routing and page composition up to Astro, where it can make smarter decisions about what ships to the browser.&lt;/p&gt;

&lt;p&gt;The pattern feels natural once you get used to it. Page structure lives in &lt;code&gt;.astro&lt;/code&gt; files (templating with zero overhead). Interactive behavior lives in &lt;code&gt;.svelte&lt;/code&gt; files (full reactivity where it's actually needed).&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoffs we accepted
&lt;/h2&gt;

&lt;p&gt;Astro is not a free lunch. Here's what we gave up.&lt;/p&gt;

&lt;h3&gt;
  
  
  No form actions
&lt;/h3&gt;

&lt;p&gt;SvelteKit's form actions with progressive enhancement are genuinely nice. In Astro, we handle forms through our Hono API with client-side fetch calls. It works, but there's more boilerplate involved. We write &lt;code&gt;onSubmit&lt;/code&gt; handlers and manage loading states manually where SvelteKit would handle the plumbing for us.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two component models
&lt;/h3&gt;

&lt;p&gt;Anyone working on the codebase needs to understand both &lt;code&gt;.astro&lt;/code&gt; and &lt;code&gt;.svelte&lt;/code&gt; files. When does something belong in Astro vs Svelte? The rule is straightforward (static content = Astro, interactive behavior = Svelte), but it's still a decision you make dozens of times a day.&lt;/p&gt;

&lt;h3&gt;
  
  
  View Transitions have rough edges
&lt;/h3&gt;

&lt;p&gt;Astro's View Transitions look great but come with real gotchas. Inline scripts re-execute on each navigation. We spent more time than expected getting Google Analytics and &lt;code&gt;vanilla-cookieconsent&lt;/code&gt; to behave correctly, adding guard flags to prevent double initialization. SvelteKit's client-side routing handles this more gracefully out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Separate API server
&lt;/h3&gt;

&lt;p&gt;SvelteKit has &lt;code&gt;+server.ts&lt;/code&gt; for API routes, tightly integrated with the framework. We chose to run a standalone Hono server for our API. This was intentional (the API will eventually serve a mobile app too), but it means two processes to manage, separate deployments, and CORS configuration that SvelteKit wouldn't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this paid off
&lt;/h2&gt;

&lt;p&gt;The results justified the decision.&lt;/p&gt;

&lt;p&gt;Our homepage ships about 40KB of JavaScript total. The interactive bits (provider grid, newsletter form) are small isolated bundles. In a SvelteKit setup, the framework's client-side router adds baseline overhead before any of our code even loads.&lt;/p&gt;

&lt;p&gt;Server-side OG image generation fits naturally into Astro's endpoint model. We generate dynamic Open Graph images for every hosting profile, comparison page, and blog post using Satori and Sharp. It's just a standard API route that returns a PNG buffer. No special configuration needed.&lt;/p&gt;

&lt;p&gt;Build times stay fast. Prerendering 50+ static pages, generating sitemaps, and compiling the Svelte islands takes under 30 seconds. Adding a new provider doesn't slow anything down because the build is mostly parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deployment question
&lt;/h2&gt;

&lt;p&gt;This one doesn't get discussed enough.&lt;/p&gt;

&lt;p&gt;SvelteKit works everywhere in theory. In practice, the smoothest path is deploying to Vercel. Rich Harris works there, &lt;code&gt;adapter-auto&lt;/code&gt; detects Vercel out of the box, and the integration is polished. You can absolutely use &lt;code&gt;adapter-node&lt;/code&gt; and self-host, but you'll be configuring things that Vercel handles for you automatically.&lt;/p&gt;

&lt;p&gt;We wanted to self-host. Our stack runs on a single Hetzner ARM64 VPS: the Astro site, the Hono API, PostgreSQL, all behind nginx and Cloudflare. The whole thing costs about €5 a month.&lt;/p&gt;

&lt;p&gt;With Astro, deploying to a VPS was straightforward. Install &lt;code&gt;@astrojs/node&lt;/code&gt;, build, run with PM2. That's it. No adapter quirks, no platform-specific edge cases. The output is a standard Node.js server.&lt;/p&gt;

&lt;p&gt;The cost angle is worth mentioning too. Platforms like Vercel and Netlify use usage-based pricing. The Pro plans start at $20-25 per month, but the real cost depends on traffic, serverless function execution time, and bandwidth. Overages are billed automatically. There are well-documented cases of bills spiking unexpectedly after traffic surges or bot crawls. For a content site that serves a lot of static pages, this pricing model works against you.&lt;/p&gt;

&lt;p&gt;A fixed-cost VPS eliminates that variable entirely. Traffic can double or triple and the bill stays the same. If we outgrow the current server, the next tier up is still a fraction of what a managed platform would cost at equivalent traffic. We're not locked into any vendor's pricing model, and migrating to a bigger box is a straightforward process.&lt;/p&gt;

&lt;p&gt;To be fair, managed platforms give you CI/CD, preview deployments, edge functions, and zero-config scaling. Those are real features with real value, especially for teams that don't want to manage infrastructure. The tradeoff is control and cost predictability versus convenience.&lt;/p&gt;

&lt;p&gt;For us, a €5 VPS with full control was the obvious pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  After several months in production
&lt;/h2&gt;

&lt;p&gt;23 hosting providers, hundreds of plans, comparison pages, a quiz, a blog, user auth, admin panel. All running on that single Hetzner box.&lt;/p&gt;

&lt;p&gt;The architecture has held up. New provider profiles are just data. New interactive features are new Svelte islands dropped into existing pages. New static pages are &lt;code&gt;.astro&lt;/code&gt; files with no client-side cost.&lt;/p&gt;

&lt;p&gt;Would SvelteKit have worked? Of course. For a more app-like product with dashboards, real-time collaboration, heavy client state, we'd pick SvelteKit without thinking twice.&lt;/p&gt;

&lt;p&gt;But for a content-first site where interactivity is the exception rather than the rule, Astro with Svelte islands turned out to be the right call. Not because SvelteKit couldn't do it. Because Astro's defaults already pointed in the direction we wanted to go.&lt;/p&gt;

&lt;p&gt;There's real value in not having to fight your framework.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Hosting Comparison Platform with Astro, Svelte 5 &amp; Hono</title>
      <dc:creator>HostingSift</dc:creator>
      <pubDate>Sat, 21 Feb 2026 08:59:10 +0000</pubDate>
      <link>https://dev.to/hostingsift/i-built-a-hosting-comparison-platform-with-astro-svelte-5-hono-3kde</link>
      <guid>https://dev.to/hostingsift/i-built-a-hosting-comparison-platform-with-astro-svelte-5-hono-3kde</guid>
      <description>&lt;p&gt;I got tired of hosting comparison websites. You know the ones. "Top 10 Best Hosting 2024" articles that haven't been updated since 2022, prices that are completely wrong, and rankings that suspiciously always put the same provider first no matter what.&lt;/p&gt;

&lt;p&gt;So I decided to build my own. Not a blog post with affiliate links, but an actual tool where you can filter by hosting type, compare plans side by side, and see prices that are actually current. After about six months of work, &lt;a href="https://hostingsift.com/" rel="noopener noreferrer"&gt;HostingSift&lt;/a&gt; is live and I figured I'd share what went into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Astro&lt;/strong&gt; in hybrid SSR mode was the obvious pick. Most pages (hosting profiles, categories, homepage) get prerendered at build time, which means they're fast out of the box. A few routes like the comparison tool need server rendering, and Astro handles that switch per-page which is really convenient.&lt;/p&gt;

&lt;p&gt;For interactive bits I went with &lt;strong&gt;Svelte 5&lt;/strong&gt;. I was on React before and honestly the new runes API (&lt;code&gt;$state&lt;/code&gt;, &lt;code&gt;$derived&lt;/code&gt;, &lt;code&gt;$effect&lt;/code&gt;) won me over pretty quickly. No dependency arrays, no stale closures, components feel lighter. It took maybe a week to get comfortable with the mental model shift and after that I didn't look back.&lt;/p&gt;

&lt;p&gt;The backend is &lt;strong&gt;Hono&lt;/strong&gt; on Node.js. Think Express but with actual TypeScript support and better performance. It handles auth, the admin panel API, affiliate click tracking, all that stuff. Database is &lt;strong&gt;PostgreSQL&lt;/strong&gt; with &lt;strong&gt;Drizzle ORM&lt;/strong&gt; which has been solid for both queries and migrations.&lt;/p&gt;

&lt;p&gt;Everything lives in a monorepo: &lt;code&gt;apps/web&lt;/code&gt;, &lt;code&gt;apps/api&lt;/code&gt;, &lt;code&gt;packages/db&lt;/code&gt;, &lt;code&gt;packages/email&lt;/code&gt;. pnpm workspaces glue it together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping Prices Up to Date
&lt;/h2&gt;

&lt;p&gt;The whole point of the site is showing accurate pricing, so I built an automated pipeline that pulls plan data from each provider on a weekly schedule. Every plan, every billing period, every feature toggle. It compares incoming data against what's already in the database and upserts the differences. Each run also creates a snapshot, so if a provider quietly bumps their renewal price by $2/month, that gets recorded. Over time this builds a price history for every single plan.&lt;/p&gt;

&lt;p&gt;Getting the data right for each provider was the biggest time sink. Every provider structures their pricing page differently. Some have clean layouts, others hide plans behind tabs and dropdowns, and a few change their page structure often enough that things break. You end up writing very provider-specific logic and accepting that maintenance is part of the deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;I'd spend more time on data validation early on. I almost "fixed" what looked like a pricing bug for one provider before checking their actual website. Turns out they give their biggest promotional discount to mid-tier plans, not the cheapest ones. Now I always verify against the source before touching anything in the database.&lt;/p&gt;

&lt;p&gt;Svelte 5 + Astro is a great combo but the ecosystem is still catching up. Some things that are one-line solutions in React/Next don't have equivalents yet. The tradeoff is worth it though, the output is noticeably lighter and the developer experience is genuinely good.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://hostingsift.com" rel="noopener noreferrer"&gt;HostingSift&lt;/a&gt; is live if you want to check it out. You can filter by hosting type, compare providers, see current prices. Still adding more providers and features so if something looks off or you have ideas, I'd love to hear about it in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
