Why Svelte Feels Like Rust (And React Feels Like JavaScript)
$ npx sv create my-app
# finally, a frontend that doesn't fight meI’m not a frontend developer. I want to be clear about that upfront.
I’m a systems engineer. I write Rust for a living. I think about ownership, memory layout, and zero-cost abstractions before I think about user interfaces. So when a project demands a frontend — and they always eventually demand a frontend — I reach for whatever causes the least amount of friction and lets me get back to the parts I actually enjoy.
For a long time, that was React with Next.js. Not because I loved it. Because it was the obvious choice, the thing with the most Stack Overflow answers, the framework every tutorial assumed. I used it the way a lot of backend engineers use frontend tools: grudgingly, at arm’s length, getting the job done without really understanding why it worked the way it did.
Then I switched to SvelteKit. And something clicked that I didn’t expect.
Svelte doesn’t feel like a JavaScript framework. It feels like a compiled language. And once I noticed that, I couldn’t stop seeing the parallels.
The Thing About React Nobody Said Out Loud
React’s core idea is the virtual DOM. You describe what the UI should look like. React figures out what changed and updates the real DOM accordingly. It’s a clever solution to a real problem — DOM manipulation is slow and error-prone if you do it by hand.
The thing is, the virtual DOM is a runtime. A whole layer of abstraction that runs in the browser, diffing trees, reconciling state, doing work that your CPU pays for on every render.
When you write a React component, you’re not describing a UI. You’re describing instructions for a runtime to interpret and turn into a UI. The component function runs. React processes the output. The actual DOM change happens somewhere downstream, through layers you don’t directly control.
// This runs every time state changes
// React decides what to do with the output
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}This is dynamic dispatch. Not in the C++ sense — but philosophically. The work happens at runtime. The framework intercepts, interprets, and decides.
Coming from Rust, where “zero-cost abstractions” is almost a religion, this felt wrong in a way I couldn’t fully articulate. You write code. React runs your code. React runs its own code. Two things are running when one should be enough.
What Svelte Actually Does
Svelte takes a different position entirely. There is no virtual DOM. There is no runtime diffing. Instead, Svelte is a compiler — you write your components, and Svelte compiles them into precise, surgical DOM operations at build time.
The same counter in Svelte 5:
<script>
let count = $state(0);
</script>
<p>Count: {count}</p>
<button onclick={() => count++}>
Increment
</button>When you build this, Svelte doesn’t ship a diffing engine to the browser. It generates JavaScript that knows exactly which DOM node to update when count changes. Not “re-render the component and figure it out.” Specifically: update this text node. That’s it.
The moment I understood this, I had a very specific thought: this is what Rust means by zero-cost abstractions.
In Rust, a for loop over a vector and a hand-written pointer walk generate the same machine code. The abstraction has no runtime cost because the compiler eliminates it. The ergonomics are free.
Svelte does the same thing for UI. The $state(0) declaration isn’t runtime state management — it’s a compiler annotation. Svelte sees it, instruments the surrounding code at build time, and generates the minimum JavaScript needed to keep that specific DOM node in sync. You write declarative code. The compiler figures out the imperative minimum. The abstraction is free.
Reactivity Without a Runtime
React’s reactivity model requires you to think about it constantly. useState. useEffect. useMemo. useCallback. The hook system is a protocol for communicating with React’s runtime — you’re telling it when to re-run, what to memoize, what side effects to track.
If you get it wrong, you pay for it. Stale closures. Infinite render loops. useEffect dependencies that silently don’t include everything they should. These aren’t edge cases — they’re common enough that there are entire blog post categories dedicated to explaining why your React component is rendering twelve times.
// How many times have you written this wrong?
useEffect(() => {
fetchData(userId);
}, []); // forgot userId — stale closureSvelte 5’s reactivity is built around runes — compiler annotations that look like function calls but are processed entirely at build time. You don’t manage reactivity. You declare it:
<script>
let userId = $state(1);
// $derived replaces the old $: label syntax.
// This recomputes whenever userId changes.
// No dependency array. No hook protocol.
// The compiler figures out the dependency graph.
const userData = $derived(fetchUser(userId));
</script>$state, $derived, $effect — these aren’t functions imported from a library. They’re compiler primitives. Svelte intercepts them during compilation and rewrites your component into precise, dependency-aware DOM updates. Nothing runs at startup to set up a reactivity system. The reactivity is the compiled output.
This is exactly how Rust’s derive macro works. #[derive(Debug)] isn’t a runtime registration — it’s a compile-time code generation step. The compiler sees the annotation, generates the Debug implementation, and by the time you run the binary, there’s no macro machinery left — just the generated code. Svelte runes work identically: by the time the browser runs your component, there are no runes left. Just targeted DOM operations.
The philosophical parallel is exact: both are compile-time metaprogramming that eliminates runtime overhead.
Ownership and Scope
Here’s where it gets more interesting.
One of the things that trips up React beginners — and honestly, experienced developers too — is state ownership. Where does the state live? Who’s responsible for updating it? When a child component needs to modify parent state, you’re passing callbacks down. When distant components need to share state, you’re reaching for Context or Redux or Zustand or one of the seventeen other state management libraries.
The problem isn’t that React has no answer to this. It’s that React has too many answers, and picking the right one requires understanding the entire component tree and its data flow — something that’s only obvious in retrospect.
Svelte’s store system is simpler, and the reason it’s simpler maps directly to a concept I think about in Rust: explicit ownership.
In Svelte 5, shared state lives in .svelte.js files — plain JavaScript/TypeScript modules that use runes. No special store API to learn. No writable or derived imports:
// state.svelte.js
export const counter = $state({ count: 0 });
// $derived works at module level too
export const doubled = $derived(counter.count * 2);<script>
import { counter, doubled } from './state.svelte.js';
</script>
<p>Count: {counter.count}</p>
<p>Doubled: {doubled}</p>
<button onclick={() => counter.count++}>Increment</button>There’s no subscription protocol. No $ prefix sigil to remember. No useEffect cleanup to forget. The state is a plain object made reactive by the compiler. Import it, read it, mutate it — the compiler already knows which components depend on which pieces of state and wires up the updates at build time.
In Rust, the borrow checker handles cleanup automatically through Drop. You don’t free memory manually — ownership semantics mean resources are released when they go out of scope. Svelte 5’s reactivity works on the same principle: the compiler knows the component’s scope, so effects and reactive dependencies are torn down automatically when the component is destroyed. No manual cleanup. No return () => unsubscribe(). The compiler already knows when things go out of scope, because it wrote the code that creates them.
SvelteKit vs Next.js: The Framework Layer
All of the above is about Svelte the UI framework. SvelteKit is the full-stack application framework built on top — the equivalent of Next.js in the React ecosystem.
Here too, the comparison reveals a philosophical difference.
Next.js has accumulated a lot of concepts over the years. Pages Router. App Router. getServerSideProps. getStaticProps. Server Components. Client Components. The "use client" directive. RSC payload. Each addition solves a real problem but leaves behind complexity that doesn’t go away. Working with a modern Next.js application means carrying a mental model of which rendering strategy applies where and why.
SvelteKit’s model is simpler and more consistent. Every route can export a load function that runs on the server. The load function’s return value is available as data in the component. If you need client-side navigation, it works. If you need SSR, it works. If you need static generation, it works. One mental model, applied uniformly:
// src/routes/blog/[slug]/+page.server.js
export async function load({ params }) {
const post = await getPost(params.slug);
if (!post) {
throw error(404, 'Post not found');
}
return { post };
}<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
// Svelte 5: props are declared with $props() instead of export let
let { data } = $props();
</script>
<article>
<h1>{data.post.title}</h1>
{@html data.post.content}
</article>The data flows in one direction, through a clear interface. The load function is the boundary between server and client. It’s explicit and it’s consistent everywhere.
This reminds me of how Rust handles error propagation. The ? operator is a single, consistent mechanism for propagating errors up the call stack. You don’t choose between four different error handling strategies per function — there’s one model, and it applies everywhere. SvelteKit’s load pattern has the same quality: one way to fetch data, one way to pass it down, one way to handle missing resources.
The Honest Tradeoffs
I’m not going to tell you Svelte is perfect. Here’s what you actually give up:
Ecosystem size. React’s ecosystem is enormous. Every third-party library with a UI component ships a React version first. Date pickers, rich text editors, data grids — if you need it, React probably has five options. Svelte has one, or you’re wrapping a vanilla JS library yourself. This matters more than people admit.
Job market. React is on almost every frontend job posting. Svelte is growing but it’s not React-sized. If you’re hiring React developers, you have a larger pool. If you’re looking for Svelte work, the market is smaller. Know which side of that equation you’re on.
Tooling maturity. React’s DevTools are excellent. The Svelte DevTools exist and work, but they’re not as polished. Debugging reactive state in Svelte can occasionally feel like debugging macro output in Rust — you know what you wrote, but you have to think carefully about what the compiler generated.
TypeScript integration. Svelte 5 made this substantially better — runes are TypeScript-native and the LSP support is solid. But .svelte files still have occasional rough edges compared to pure .tsx. Nothing dealbreaking, just occasional friction you wouldn’t see in a React project.
When the Parallel Breaks Down
The compiler analogy is real, but it has limits I should be honest about.
Svelte is not Rust. The compiler doesn’t give you the same guarantees. There’s no borrow checker enforcing that you’ve handled every edge case. You can still write buggy Svelte — the reactivity system catches a large class of mistakes, but not all of them. The comparison is philosophical, not technical.
Also: Svelte’s compilation model means the generated output can be surprising. When something goes wrong at a level below your component code, debugging it requires understanding what Svelte compiled your code into. This is the same experience as debugging a Rust macro — occasionally you need to look at the expansion, not the source. Most of the time it doesn’t matter. Occasionally it matters a lot.
Who Should Switch
If you’re a backend or systems developer who resents frontend work — this is for you. Svelte’s model respects your time and your instinct that runtime overhead should be justified. The learning curve is genuinely shallow. I was productive in SvelteKit within a day, which I cannot say about my first week with Next.js.
If you’re already deep in a React codebase with a team that knows React well — probably not worth switching. The philosophical advantages don’t outweigh the migration cost and team retraining. React is not a bad framework. It’s a framework with a different philosophy.
If you’re starting something new and you have the freedom to choose — try Svelte first. Specifically, notice what you don’t have to think about. No hook dependency arrays. No useMemo decisions. No render optimization passes. When the framework disappears into the background and you’re just building the thing, that’s when you know the abstraction is actually working.
The Bottom Line
React made the virtual DOM the default assumption of modern frontend development. It was the right call for its time — it solved real problems with direct DOM manipulation and gave us a component model that the entire industry adopted.
But the virtual DOM is a runtime. And runtimes have costs. And for a certain kind of developer — one who’s spent years learning to think about what the compiler can do for you instead of what the runtime does at your expense — that runtime feels like a compromise that doesn’t need to exist anymore.
Svelte proved it doesn’t.
Write components. Run the compiler. Ship less JavaScript. Get deterministic updates.
That’s a philosophy I already believe in. It just took a frontend framework to show me it applied here too.
$ vite build
✓ built in 1.2s
dist/index.html 0.52 kB
dist/assets/index-Bx92kl3m.js 42.3 kB # no virtual DOM includedThat output size is the point.
I write about Rust, systems programming, and occasionally frontend things at ademr.dev/blog. Questions? qcynaut@gmail.com