I have been working on a big project for many months called Tea Share and in the process, I switched to SvelteKit from Next.js.
What Is Tea Share?
Tea Share is a social media app similar to Instagram or Twitter for the Web and Android. It is highly interactive so it’s an obvious candidate for a fullstack framework and it also needs to be fast and accessible.
Why Did I Make The Switch?
Well, I have a very, let’s say, stupid reason for the switch. Though it’s often said to prioritize User Experience over performance, my mind just could not handle the fact that React’s bundle size is so big. I know this sounds kind of insane to ditch a framework for a few kilobytes, but to be honest, switching to Svelte was a very good decision even if we put the bundle size aside.
The main reason for this paranoia is RSC’s (React Server Component’s) complete paradigm shift. It made me avoid serving JavaScript to users because React itself is 51kb! And the Next.js addition for client-side routing and route-prefetching with many other features increased it by another 21kb.
Eventually, I couldn’t handle the app being so paranoid on JavaScript. So, I switched to SvelteKit. I couldn’t believe how low the bundle size was after I used it! The app with Next.js at a barely user friendly state (Almost everything was server-side) outputted an app with a bundle size of 109kb GZipped. But the app built with SvelteKit that now focuses on User Experience as well as performance, despite having full hydration and many features on the client-side, had its bundle size come down to ONLY 48kb GZipped.
However, it was not solely the decrease in bundle size, SvelteKit did have some features that made me really like it in general.
An Easy Way To Progressively Enhance Web Forms
This is one of the main reasons I really like SvelteKit. It puts a lot of effort in encouraging the use of features that do not require JavaScript like
<form>
and then adds easy-to-use APIs to progressively enhance it with client-side JavaScript to improve the User Experience.
I remember when I started learning HTML, CSS and JavaScript, I tried doing progressive enhancement from scratch, and it did not feel like something I would like to write everytime. For reference, here is vanilla JavaScript and HTML vs. SvelteKit:
<html lang="en">
<head>...<head>
<body>
<form action="/some-api" method="POST" id="login-form">
...
</form>
</body>
</html>
Here is the JavaScript.
document.getElementById("login-form").onsubmit = async e => {
e.preventDefault();
const formData = new FormData(e.target);
await fetch("https://.../some-api", {
method: "POST",
formData,
});
};
Whatever I am showing here is just a small example, imagine this in a big codebase. But SvelteKit? Its as simple as it gets.
<script lang="ts">
// Only around 1kb DECOMPRESSED!
import { enhance } from "$app/forms";
</script>
<!-- form action progressive enhancement -->
<form action="?/login" method="POST" use:enhance>...</form>
Thats it! Super simple and straight-forward. The framework also made me think about progressive enhancement and its importance a lot more because of this.
SvelteKit Is Nicer To Work With
To me, writing UIs in Svelte were nicer than JSX. The JavaScript (With TypeScript support) is minimal & feels like just vanilla JavaScript without the pain points.
The UI code, after converting to Svelte, decreased by 33%. Svelte also has a lot of shorthands that make writing UIs easier. So convenient, I can fit a lot of them in one snippet:
<script lang="ts">
import { onMount } from "svelte";
import { writable } from "svelte/store";
import Component from "./Component.svelte";
// Built-in State Management
const store = writable(null);
const stuff = 23;
const things = [];
// Reactivity
let count = 0;
// Computed State
$: doubled = count * 2;
$: {
// Side Effects
console.log(`Count is ${count}`);
}
// Async Mount Functions
onMount(async () => {
await import("...");
});
</script>
<!-- Managed Subscription -->
<h1>{$store}</h1>
<!-- Props Shorthand -->
<Component {stuff} />
<!-- Non-Keyed Fast Loops -->
{#each things as thing}
<div />
{/each}
<!-- Readable In-template Promise Handling -->
{#await Promise.resolve("hi")}
<div aria-busy>Wait</div>
{:then data}
<article>{data}</article>
{:catch}
<p>uhh</p>
{/await}
So much stuff done in one tiny template file. Now that’s a HUGE productivity boost.
Performance
Like I said in the “Why I made the switch” section, Svelte is already faster than React. (And that’s huge for me) I care about performance. I tried a lot on trying to get React going fast. In Next.js 12, whatever I tried, I always came to a bundle size of ~200kb and runtime performance that was good enough. (Next.js 13 is a different story) But Svelte CRUSHED it. Not only did the client-side bundle size decrease, about which I wrote before, but the server-side bundle decreased too. It went down from 6MB -> 418kb, that’s a 93.2% decrease. Response times became 2.3x faster, client-side bundles sat at 48kb and the bundle size increase is very minimal. (Only like 3kb for 3 medium-sized components) Svelte’s compiler (Especially with version 4) produces highly optimized JavaScript and it runs incredibly fast. The hydration overhead is very small. And speaking of hydration, since Svelte compiles to Vanilla JavaScript and does not have a Virtual DOM or a Component Tree, it’s actually impossible to get hydration errors in it, or at the very least, it’s impossible to get errors that are at the same or higher urgency level as React. In React, before server components, (RSCs don’t hydrate so there’s no problem using them) there were a lot of problems with hydration when showing a formatted time. If there was a post published 10s ago, on the server it would generate ’10s ago’, but by the time the client received the generated HTML, the time obviously would be now around 11-12s ago, but since the server generated HTML still has 10s, it throws a hydration error because we need both server & client data to be idempotent to hydrate the page correctly. But in Svelte, it doesn’t seem to matter as much.
Slight Inconvenience With Svelte
Well, I have just one inconvenience that I faced in Svelte. It’s the templating language of Svelte, which has certain restrictions that make it a bit annoying to use. Let’s take React’s JSX vs Svelte in the example below.
export default async function Page() {
const items = await getItems();
return items.map(item =>
<ItemCard
key={item.id}
special={items.special.property as SpecialProperty}
{...item}
/>
);
}
Notice that type assertion? Now let’s look at Svelte.
// +page.server.ts
export const load = async () => ({ items: await getItems() });
<!-- +page.svelte -->
<script lang="ts">
export let data;
</script>
{#each data.items as item}
<ItemCard
special={items.special.property as SpecialProperty}
{...item}
/>
{/each}
The code looks fine on the surface, but actually, this is wrong. Svelte’s templating language doesn’t have support for type assertions. You might think it’s alright, but it’s also really annoying in certain places. And it’s not just type assertions, many TypeScript features are slightly nerfed in Svelte.
Will I Leave React For Svelte For All My Apps?
I Guess, No?
I still love using React. I don’t know if it’s Stockholm Syndrome, but its still my favourite framework along with Svelte. If I am building a large project, Svelte is the new way for me. Its accessibility and performance is all I need for a big production-grade app But React is evolving rapidly and I expect more improvements in performance in the future. Also, sometimes, we need to put food on the table right? I guess this is the only reason React is still popular nowadays.