Svelte Interview Questions and Answers
Last updated:
Check out 40 of the most common Svelte interview questions, then take an AI-powered practice interview
Q1What is Svelte and how is it different from React or Vue?
BasicFundamentals
Answer
Svelte is a UI framework created by Rich Harris in 2016 that takes a fundamentally different approach from React and Vue: instead of shipping a runtime that interprets your component tree at runtime, Svelte is a compiler that converts your `.svelte` files into plain, optimised JavaScript at build time. There is no virtual DOM, no reconciliation algorithm, and no framework runtime in your production bundle, just the minimal imperative code needed to update the specific DOM nodes that depend on changed state. This means smaller bundles (a hello-world Svelte app is around 4 KB vs 40+ KB for React), faster startup because there's nothing to parse and initialise, and a simpler component file format that combines HTML, CSS, and JavaScript in one file.
Svelte also ships official solutions for things that React leaves to userland: scoped CSS, transitions, animations, and stores are all built-in. The architectural philosophy is 'write less code', Svelte takes the position that the framework should solve common UI problems out of the box rather than relying on a sprawling ecosystem of competing libraries. The trade-offs: a smaller ecosystem, fewer third-party components compared to React, and a smaller pool of developers familiar with the framework when hiring in India.
Key Points
- Compiler-first, no runtime, no virtual DOM
- Smaller bundle sizes (~4 KB vs ~40 KB for React)
- Single-file components (.svelte) combining HTML, CSS, JS
- Built-in scoped CSS, transitions, and stores
- Trade-off: smaller ecosystem and hiring pool
Q2What is a `.svelte` file and how is it structured?
BasicComponents
Answer
A `.svelte` file is a single-file component containing three optional sections: a `<script>` block for JavaScript or TypeScript logic, a `<style>` block for CSS (automatically scoped to the component), and the markup itself (HTML with Svelte-specific syntax for interpolation, control flow, and event handlers). The compiler reads this file at build time and emits a JavaScript module that exports a component class. You can also add `<script context='module'>` for code that runs once when the module loads (shared across all instances), as opposed to the regular `<script>` block which runs once per component instance.
In Svelte 5 the `context='module'` syntax was deprecated in favour of `<script module>`. Order of execution matters and interviewers probe it: the module script runs once on first import, before any instance exists, so it cannot read props or instance state, and anything exported from it becomes a named export of the component module (`import Greeting, { reset } from './Greeting.svelte'`). Svelte 5 also changed the emitted shape.
Components are functions now, not classes, so `new Component({ target })` throws `component_api_invalid_new` and you call `mount()` or `hydrate()` from `svelte` instead; third-party code that still uses `new` needs `compatibility: { componentApi: 4 }` in `svelte.config.js`. `lang="ts"` only works if `vitePreprocess()` is wired into that same config, and it strips types rather than checking them, so type errors surface when you run `svelte-check`, never during `vite build`. A related file type is `.svelte.ts` (or `.svelte.js`): not a component, just a module the compiler processes so runes work inside it.
<!-- Greeting.svelte -->
<script lang="ts">
let { name } = $props<{ name: string }>();
let count = $state(0);
</script>
<button onclick={() => count++}>
Hello {name}, clicked {count} times
</button>
<style>
button {
/* scoped to this component only */
background: #ff3e00;
color: white;
}
</style>
Key Points
- Three sections: <script>, markup, <style>
- Styles are scoped by default
- TypeScript with lang="ts"
Q3What are runes in Svelte 5 and why were they introduced?
BasicReactivity
Answer
Runes are explicit reactivity primitives introduced in Svelte 5 (October 2024), replacing the implicit reactivity model of Svelte 3 and 4. The five core runes are `$state` (declare reactive state), `$derived` (computed values), `$effect` (side effects), `$props` (component props), and `$bindable` (two-way binding). They look like functions but are compile-time language extensions, the compiler rewrites them into efficient subscription code.
Runes solve three long-standing problems in Svelte 4: (1) reactivity only worked at the top level of `<script>`, so logic couldn't easily move to `.js` files, (2) the `let` syntax made it ambiguous whether a variable was reactive or not, (3) the `$:` label was confusing, it ran both as derived state and as an effect depending on context. With runes, reactivity is explicit, can be used in any `.svelte.js` or `.svelte.ts` file, and the same primitives work everywhere. The full 5.x set also includes `$state.raw` (no deep proxy, you replace the whole value to update), `$state.snapshot`, `$derived.by`, `$effect.pre`, `$effect.root`, `$inspect` for dev-time logging, and `$host` inside custom elements.
Two facts act as filters in interviews. Runes are compiler keywords, not imports, so you cannot alias or pass one around (`const s = $state` fails to compile) and calling one from a plain `.ts` file raises `rune_outside_svelte`. And `$state` on an object or array hands back a Proxy, so `state === rawObject` is false (dev logs `state_proxy_equality_mismatch`) and passing it to `structuredClone`, `postMessage` or an IndexedDB `put` throws `DataCloneError` until you unwrap it with `$state.snapshot()`. A component flips into runes mode the moment it uses any rune, you can force it with `<svelte:options runes={true} />` or `compilerOptions.runes: true`, and legacy `$:` syntax cannot be mixed into a runes-mode component.
<script>
let count = $state(0); // reactive state
let doubled = $derived(count * 2); // computed
let { name = 'world' } = $props(); // props with default
$effect(() => { // side effect
console.log(`count changed to ${count}`);
});
</script>
<button onclick={() => count++}>{count} → {doubled}</button>
Key Points
- $state, reactive state
- $derived, computed values
- $effect, side effects (logging, DOM, subscriptions)
- $props, component props
- Works in any .svelte.js / .svelte.ts file, not just components
Q4How do you handle events in Svelte?
BasicEvents
Answer
In Svelte 5, events are plain HTML attributes, `onclick`, `oninput`, `onsubmit`, and the value is a JavaScript function or arrow expression. This replaced Svelte 4's `on:click` syntax which used a separate directive. Event modifiers (`preventDefault`, `stopPropagation`, `once`) used to be inline with the `on:click|preventDefault` syntax; in Svelte 5 you call them as regular functions inside the handler or use the new `$state.preventDefault()` style helpers.
Custom component events in Svelte 5 are callback props, instead of `createEventDispatcher()`, the parent passes a function prop and the child calls it. Two behaviours bite in production. First, Svelte 5 delegates a fixed list of bubbling events (click, input, change, keydown and similar) to a single listener on the app root rather than attaching one listener per element, so a listener you register yourself with `addEventListener` on an ancestor runs before the delegated one, and a `stopPropagation()` there silently kills your Svelte handlers.
Elements you move outside the mount root (a portal into `document.body`) stop receiving delegated events at all. Second, inline modifiers are gone: `on:click|preventDefault` becomes an explicit `e.preventDefault()` in the body, `|once` becomes a guard, and capture phase is a separate attribute, `onclickcapture`. Svelte 4 directives still compile in legacy mode but emit the `event_directive_deprecated` warning. You also cannot declare the same handler attribute twice on one element the way `on:click` allowed, so merge the two functions into one.
<!-- Svelte 5 -->
<script>
let value = $state('');
function handleSubmit(e) {
e.preventDefault();
console.log('submitting', value);
}
</script>
<form onsubmit={handleSubmit}>
<input oninput={(e) => value = e.currentTarget.value} value={value} />
<button type="submit">Save</button>
</form>
<!-- Svelte 4 (legacy) -->
<!-- <form on:submit|preventDefault={handleSubmit}> -->
Q5How do you conditionally render markup in Svelte?
BasicTemplates
Answer
Svelte uses `{#if}`, `{#each}`, `{#await}`, and `{#key}` block syntax for control flow. These are compiled into efficient imperative DOM updates, no virtual DOM diff needed. The `{#each}` block requires a key for stable identity when items reorder: `{#each items as item (item.id)}`, without a key, Svelte updates in place which can cause bugs with stateful components. `{#await}` is uniquely powerful: it lets you render loading, success, and error states for a promise directly in markup, no useState/useEffect dance required.
The keying rule is where interviewers push. Without a key, Svelte reuses DOM nodes positionally, so deleting the first row leaves the second row's `<input>` value, checkbox state and focus attached to the wrong record. With a key, duplicate values throw `each_key_duplicate` at runtime, which is usually how you find out an API returns the same id twice.
Keys are compared by reference for objects, so `(item)` over a `$state` array is workable but `({ ...item })` never is. Other blocks worth naming: `{#key expr}` tears down and rebuilds its contents when `expr` changes, which is the idiomatic way to replay a transition or reset a child's internal state; `{@const}` declares a local inside a block; `{@html value}` inserts raw markup with no sanitisation and no style scoping, so it needs DOMPurify on anything user-generated. During SSR an unresolved `{#await}` renders the pending branch, so anything that must appear in the crawled HTML has to be awaited in a load function instead.
{#if user}
<p>Welcome, {user.name}</p>
{:else if loading}
<p>Loading...</p>
{:else}
<p>Please log in</p>
{/if}
{#each todos as todo (todo.id)}
<li>{todo.text}</li>
{:else}
<li>No todos yet</li>
{/each}
{#await fetchUser()}
<p>Loading user...</p>
{:then user}
<p>Hello, {user.name}</p>
{:catch err}
<p>Error: {err.message}</p>
{/await}
Q6How do you pass data from parent to child components?
BasicComponents
Answer
In Svelte 5, props are declared with the `$props()` rune via destructuring. You can provide defaults inline and use TypeScript generics for typing. Props are read-only by default, if the child mutates them, you get a warning in dev mode.
For two-way binding (parent and child share a value), use `$bindable()` on the child and `bind:` on the parent. This explicit opt-in is safer than Svelte 4's accidental two-way binding via `export let`. The details that separate a rehearsed answer from a real one: `$props()` must be called exactly once at the top level of the instance script, and in Svelte 5 stable it takes no type arguments, you annotate the destructuring pattern instead (`let { name }: Props = $props()`).
Fallback values apply only when the prop is `undefined`, never when it is `null`, which is the usual reason a default silently fails against a nullable database column. Rename reserved words with `let { class: className } = $props()` and collect leftovers with `...rest` to spread onto the root element. Props stay reactive because the compiler rewrites reads into getters, so destructuring is safe, but copying a prop into a `$state` variable freezes it at the initial value, a very common bug in wrapper components. Mutating an object the parent owns works but logs `ownership_invalid_mutation` in dev. `$props.id()` (Svelte 5.20 and later) returns an id that is stable across SSR and hydration, which is what you want for `aria-describedby` and label wiring.
<!-- Parent.svelte -->
<script>
import Child from './Child.svelte';
let name = $state('Saksham');
</script>
<Child {name} age={29} />
<Child bind:name /> <!-- two-way -->
<!-- Child.svelte -->
<script lang="ts">
let { name = $bindable(), age = 18 } = $props<{ name: string; age?: number }>();
</script>
<input bind:value={name} />
<p>{name}, {age}</p>
Q7What is reactive state in Svelte 4 and how does it work?
BasicReactivity
Answer
In Svelte 4 (the legacy syntax), any top-level `let` declaration in a `<script>` block was automatically reactive, the compiler tracked assignments and re-rendered the component when the variable changed. Computed values used the `$:` label syntax, which the compiler turned into a reactive statement that re-ran whenever its dependencies changed. The same `$:` was also used for side effects, which was a common source of confusion.
A famous gotcha: array methods like `.push()` and `.splice()` did NOT trigger reactivity because they mutate in place, you had to reassign (`arr = [...arr, item]`) or use the assignment trick (`arr[arr.length] = item`). Svelte 5's runes (`$state`, `$derived`, `$effect`) replaced this implicit model with explicit primitives that work even on objects and arrays mutated in place. Two more legacy behaviours come up at companies still on the old syntax.
Dependencies of a `$:` statement are collected at compile time from the identifiers the statement itself reads, so a value read inside a function you call is invisible to the tracker and the statement never re-runs; the fix is to reference the dependency directly in the statement. And the compiler topologically sorts reactive statements by dependency instead of running them in source order, so moving lines around changes nothing but introducing a cycle (`$: a = b + 1; $: b = a + 1`) loops until Svelte bails out. Destructuring inside a reactive statement needs parentheses: `$: ({ id, name } = user)`. Legacy mode is still fully supported in Svelte 5, so a Svelte 4 codebase keeps building untouched, but you cannot use `beforeUpdate` or `afterUpdate` in a component that has switched to runes, they map to `$effect.pre` and `$effect` respectively.
<!-- Svelte 4 (legacy) -->
<script>
let count = 0; // reactive
$: doubled = count * 2; // derived
$: console.log(count); // effect (yes, same syntax!)
let arr = [];
function add() {
arr.push(1); // ❌ does NOT trigger update
arr = arr; // ✅ workaround: reassign
arr = [...arr, 1]; // ✅ idiomatic
}
</script>
Q8What is SvelteKit and how does it relate to Svelte?
BasicSvelteKit
Answer
SvelteKit is the official meta-framework for Svelte, equivalent to Next.js for React or Nuxt for Vue. Svelte the library is just the component model and compiler; SvelteKit adds everything you need to ship a real application: file-based routing, server-side rendering (SSR) and static site generation (SSG), API endpoints (`+server.ts`), form actions, hooks for request middleware, deployment adapters for various platforms (Vercel, Netlify, Cloudflare, Node), and progressive enhancement so forms and links work without JavaScript. SvelteKit reached 1.0 in December 2022 and has been the recommended way to build Svelte apps since.
You can technically use Svelte without SvelteKit (e.g. with Vite directly, or embedded in another framework), but the vast majority of production Svelte apps use SvelteKit. Mechanically SvelteKit is a Vite plugin: `sveltekit()` from `@sveltejs/kit/vite` in `vite.config.ts`, `vite dev` for development, `vite build` for production, and a `svelte-kit sync` step that regenerates `.svelte-kit/types` so the `./$types` imports and the `$lib` alias resolve. It ships `$app/state`, `$app/navigation`, `$app/forms`, `$app/environment`, `$app/paths` and `$app/server` as first-party modules, plus `$env/static/*` and `$env/dynamic/*` for configuration, and `app.html` with the `%sveltekit.head%` and `%sveltekit.body%` placeholders is the only raw HTML shell you own. Two version details matter in 2026: SvelteKit 2 dropped the `throw` requirement on `error()` and `redirect()`, and it made `path` mandatory on `cookies.set()`, `cookies.delete()` and `cookies.serialize()`, which is the most common upgrade breakage from 1.x because a missing cookie shows up as an infinite login loop rather than an error. `$app/stores` still works but has been superseded by the fine-grained `$app/state` since SvelteKit 2.12.
Key Points
- Svelte = the component framework
- SvelteKit = the meta-framework (routing, SSR, API, deploy)
- Equivalent to Next.js for React
Q9How does file-based routing work in SvelteKit?
BasicSvelteKit
Answer
SvelteKit uses the filesystem under `src/routes/` to define routes. A directory becomes a URL segment, and the page UI lives in a file named `+page.svelte` inside that directory. So `src/routes/blog/[slug]/+page.svelte` becomes the route `/blog/:slug` with `slug` as a dynamic parameter.
Other special files: `+layout.svelte` (shared layout for the directory and its descendants), `+page.server.ts` (server-only logic for the page), `+server.ts` (API endpoint), `+error.svelte` (error boundary). The `+` prefix marks framework files. Square brackets create dynamic segments (`[slug]`), parentheses create groups that don't affect the URL (`(marketing)`), and `[...path]` is a catch-all.
Beyond the basics, routing has a specificity order you should be able to recite: more specific segments win, static beats dynamic, dynamic beats rest, and ties break alphabetically. Param matchers live in `src/params/`, export a `match(param)` function and are applied as `[id=integer]`, so a malformed id 404s before your load function runs. Optional segments are `[[lang]]` and characters illegal in filenames are escaped as `[x+2e]`.
Layout groups in parentheses let `(marketing)` and `(app)` carry different chrome at the same URL depth, and a single route escapes its inherited layout with `+page@.svelte` (reset to root) or `+page@(app).svelte` (reset to a named group), which is how you render a checkout page without the dashboard sidebar. `+layout.server.ts` runs server-only for an entire subtree. From SvelteKit 2.26, `resolve()` in `$app/paths` builds typed URLs from route ids so a renamed directory becomes a compile error instead of a dead link, and the `reroute` hook in `src/hooks.ts` rewrites an incoming pathname onto a different route without issuing a redirect, which is the clean way to serve locale prefixes or keep legacy URLs alive.
src/routes/
├── +layout.svelte # root layout (header, footer)
├── +page.svelte # GET /
├── about/
│ └── +page.svelte # GET /about
├── blog/
│ ├── +page.svelte # GET /blog
│ └── [slug]/
│ ├── +page.svelte # GET /blog/:slug
│ └── +page.server.ts # server-only load function
└── api/
└── users/
└── +server.ts # GET/POST /api/users
Q10What is scoped CSS in Svelte?
BasicStyling
Answer
Every `<style>` block in a `.svelte` file is automatically scoped to that component, the Svelte compiler appends a unique hash class (like `svelte-1a2b3c`) to selectors and matching elements, so styles cannot accidentally leak to other components. This means you can use simple selectors like `button` or `.title` without worrying about global collisions. To escape scoping when you need it: use `:global(.selector)` to target descendants regardless of component, or put styles in a global stylesheet imported in `app.html` or a root layout.
Scoping is purely compile-time, there's no runtime CSS-in-JS cost and the styles ship as a regular CSS file. The mechanic is worth stating precisely: the hash class lands on the elements the selector matches, and because it is an extra class it adds one class's worth of specificity (0,1,0). That is why a component's `.btn` beats a global `.btn` regardless of stylesheet order, and it is the usual reason a design-system override appears to do nothing.
The compiler also deletes selectors it cannot prove are used and warns with `css_unused_selector`, so classes you attach at runtime through `classList.add` or inside `{@html}` get stripped from the build, wrap those in `:global(...)`. Related tools you should name: the `class:active={isActive}` and `style:color={c}` directives, `class` accepting an object or array (clsx style) since Svelte 5.16, a `:global { ... }` block since 5.10, and CSS custom properties passed as `<Card --accent="#ff3e00" />`, which quietly wraps the component in a `display: contents` element and can break a grid or flex parent. Sass, Less and PostCSS run through `vitePreprocess()`, and Tailwind 4 plugs in via `@tailwindcss/vite` rather than a PostCSS config.
<div class="card">
<h2>Title</h2>
<div class="content">
<slot />
</div>
</div>
<style>
.card { padding: 1rem; } /* scoped */
h2 { color: #ff3e00; } /* scoped */
:global(body) { margin: 0; } /* escape: applies globally */
.content :global(a) { color: blue } /* descendants of .content */
</style>
Q11How do you use Svelte stores for state management?
BasicState Management
Answer
Svelte's `svelte/store` module exports `writable`, `readable`, and `derived`, simple state primitives shared across components. A store is any object with a `.subscribe(callback)` method returning an unsubscribe function. In components, you read a store with the `$store` prefix syntax, the compiler auto-subscribes and unsubscribes on component lifecycle.
Stores predate runes and still work in Svelte 5, but for new code that doesn't need to share state across components, runes (`$state`) are simpler. Stores remain the standard for global state, auth user, cart, theme. Writing custom stores is one of the cleanest patterns in any framework.
The parts that get probed: `writable(value, start)` calls `start` when the subscriber count goes from zero to one and calls its returned function when it drops back to zero, which is how you build a store that holds a WebSocket or an interval only while something is watching it. `derived` accepts an array of stores plus a `(values, set)` callback for async work with an optional initial value. `get(store)` from `svelte/store` subscribes and immediately unsubscribes, fine for a one-off read and wasteful in a loop. The `$store` prefix only works inside `.svelte` files; in a `.ts` module you call `.subscribe()` yourself and must call the returned unsubscriber or you leak. The severe production bug is SSR scope: a store declared at module top level lives for the life of the Node process, so on a server-rendered page one user's cart or session object can be handed to the next request. Svelte 5 added `toStore` and `fromStore` in `svelte/store` to bridge runes and the store contract, which is how you adopt runes incrementally without rewriting every consumer at once.
// stores/cart.ts
import { writable, derived } from 'svelte/store';
export const cart = writable<Item[]>([]);
export const cartTotal = derived(cart, ($cart) =>
$cart.reduce((sum, item) => sum + item.price, 0)
);
// Cart.svelte
<script>
import { cart, cartTotal } from './stores/cart';
</script>
<p>Items: {$cart.length}, Total: ₹{$cartTotal}</p>
<button onclick={() => cart.update(c => [...c, newItem])}>Add</button>
Q12How do you handle two-way binding with `bind:value`?
BasicForms
Answer
Svelte's `bind:` directive creates two-way binding between a DOM element and a variable. The most common use is `bind:value` on inputs, typing in the input updates the variable, and changing the variable updates the input. Other bindable properties: `bind:checked` for checkboxes, `bind:group` for radio/checkbox groups, `bind:files` for file inputs, `bind:this` to get a reference to the DOM node (similar to React's ref).
Custom components can also be bound, but in Svelte 5 the prop must be marked with `$bindable()`, preventing accidental two-way binding. Compared to React's controlled inputs (`value={x} onChange={...}`), Svelte's `bind:value` is significantly less code for the most common case. Behaviour worth memorising: `bind:value` on `<input type="number">` or `type="range"` coerces to a number and produces `null` when the field is cleared, so any `value > 0` check needs a null guard. `bind:group` collects radio and checkbox values into an array.
Dimension bindings such as `bind:clientWidth` and `bind:offsetHeight` are read-only and install a ResizeObserver per element, which is cheap once and expensive across a thousand list rows. Media elements expose `bind:currentTime`, `bind:paused` and read-only `bind:duration`. `bind:this` stays `undefined` until the component mounts, so reading it in the script body gives you nothing while reading it inside `$effect` works, and this is the number one source of 'cannot read properties of null' in Svelte code. Svelte 5.9 added function bindings, `bind:value={() => value, (v) => value = v.trim()}`, letting you transform on the way in and out without a shadow variable. Binding to a property that was never made reactive triggers the `binding_property_non_reactive` dev warning.
<script>
let name = $state('');
let agreed = $state(false);
let inputEl: HTMLInputElement;
$effect(() => {
inputEl?.focus();
});
</script>
<input bind:value={name} bind:this={inputEl} />
<input type="checkbox" bind:checked={agreed} />
<p>Hello {name} (agreed: {agreed})</p>
Q13How do you pass markup into a component with `children` and `{#snippet}` / `{@render}`?
BasicComponents
Answer
Svelte 5 replaced slots with snippets. Any markup you write between a component's tags is compiled into an implicit snippet handed to the child as the `children` prop, and the child renders it with `{@render children?.()}`. Named slots become named snippet props: the parent writes `{#snippet header(post)}...{/snippet}` inside the component tags, the child declares `let { header } = $props()` and renders `{@render header(item)}`.
That one change retires three Svelte 4 concepts at once: `<slot name="header">`, the `let:` directive for slot props, and `$$slots` for testing whether a slot was filled. You now check with a plain `if (children)` because a snippet is just a value. Points a senior interviewer pushes on.
A snippet is first-class: you can declare one in a parent, pass the same one to two different components, or keep an array of them, but you cannot build one from a string at runtime. Snippets render only through `{@render}`; calling one as a normal function in an expression throws. The optional chaining in `{@render children?.()}` is idiomatic because a component used with no body gets `children === undefined`, and omitting it is the usual `children is not a function` crash.
Scope follows lexical rules, so a snippet declared in the parent reads the parent's variables and never the child's, which is precisely why the child must pass data in as snippet arguments. Type them with `Snippet<[Item]>` from `svelte`. Legacy `<slot>` still compiles in a non-runes component, but you cannot mix `<slot>` and snippets in the same file.
<!-- List.svelte -->
<script lang="ts">
import type { Snippet } from 'svelte';
type Item = { id: string; label: string };
let { items, row, empty, children }: {
items: Item[];
row: Snippet<[Item]>;
empty?: Snippet;
children?: Snippet;
} = $props();
</script>
{#if items.length}
<ul>
{#each items as item (item.id)}
<li>{@render row(item)}</li>
{/each}
</ul>
{:else}
{@render empty?.()}
{/if}
{@render children?.()}
<!-- Parent.svelte -->
<List {items}>
{#snippet row(item)}
<strong>{item.label}</strong>
{/snippet}
{#snippet empty()}
<p>Nothing here yet</p>
{/snippet}
<p>Rendered as the children snippet</p>
</List>
Key Points
- Body markup arrives as the `children` prop
- Render with {@render children?.()}, never call it directly
- Named slots become named snippet props
- Snippets are values: pass them around, store them, type them as Snippet<[T]>
Q14How do you scaffold and type-check a SvelteKit project with `npx sv create` and `svelte-check`?
BasicTooling
Answer
`npx sv create my-app` is the 2026 entry point; the older `npm create svelte@latest` now forwards to it. The wizard asks for a template (minimal, demo, or library), a TypeScript style (real TS syntax, JSDoc comments, or none) and add-ons: Prettier, ESLint, Vitest, Playwright, Tailwind, Drizzle, MDsveX, Storybook. You can bolt any of them onto an existing repo later with `npx sv add tailwindcss`, and `npx sv migrate svelte-5` runs the codemods on an older codebase.
The scaffold gives you `src/routes/`, `src/lib/` aliased as `$lib`, `src/app.html` carrying the `%sveltekit.head%` and `%sveltekit.body%` placeholders, `src/app.d.ts` where you declare the `App.Locals` interface, a `svelte.config.js` with `vitePreprocess()` plus an adapter, and a `vite.config.ts` wiring the `sveltekit()` plugin. Know the commands precisely. `vite dev` runs the dev server, `vite build` produces adapter output, and `vite preview` serves that real build so SSR-only breakage surfaces before deploy rather than after. `svelte-kit sync` regenerates `.svelte-kit/types` and the `$env` declarations; it runs from the `prepare` script, so a clone installed with `--ignore-scripts` shows red squiggles on every `./$types` import until you run it manually. The important one is `svelte-check --tsconfig ./tsconfig.json`: `vite build` only strips types and never checks them, so a green build proves nothing about type safety. Run `svelte-check --threshold error` in CI, and install the Svelte VS Code extension locally since it drives the same language server.
# scaffold, then add tooling later
npx sv create my-app
cd my-app && npm install
npx sv add tailwindcss playwright vitest
# day to day
npm run dev # vite dev
npm run build # vite build, writes adapter output
npm run preview # vite preview, serves the real build
npm run check # svelte-check, the actual type checker
// package.json
{
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync",
"check": "svelte-check --tsconfig ./tsconfig.json --threshold error",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch"
}
}
Q15How does SvelteKit's `+page.server.ts` load function work?
IntermediateSvelteKit
Answer
A `+page.server.ts` file exports a `load` function that runs ONLY on the server before the page renders. It receives an event object (`params`, `url`, `cookies`, `fetch`, `locals`) and returns data that becomes the page's `data` prop. This is where you talk to databases, read cookies, check auth, or call internal APIs, code that should never run in the browser.
The companion `+page.ts` runs on both server (during SSR) and client (during client-side navigation), use it for fetching public APIs that the browser can call directly. The pattern is: `+page.server.ts` for sensitive/server-only data, `+page.ts` for universal data, and they can coexist on the same route. Loads are parallelised across nested layouts, and SvelteKit handles client-side navigation by re-running loads without a full page reload.
The details that separate juniors from seniors: `load` runs on every client-side navigation, not just the first request, and it re-runs when something it tracked changes, `params`, `url.searchParams`, `route.id`, a cookie read through `cookies.get`, or a custom key registered with `depends('app:posts')`. Returning an unresolved promise streams it, SvelteKit flushes the shell first and the value later and you render it with `{#await}`, but only top-level promises stream and a rejected streamed promise never reaches `+error.svelte`, so catch it inside the load. Everything you return must survive devalue serialisation: a Prisma `Decimal`, a class instance or a function fails at runtime, and the `transport` hook added in SvelteKit 2.11 is the supported way to teach it custom types. Layout and page loads run in parallel, so a slow layout query blocks nothing until a child calls `await parent()`, which serialises the chain and is a frequent cause of an unexpectedly slow first byte.
// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals }) => {
const post = await locals.db.post.findUnique({ where: { slug: params.slug } });
if (!post) throw error(404, 'Post not found');
return { post };
};
// src/routes/blog/[slug]/+page.svelte
<script lang="ts">
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
<article>{@html data.post.content}</article>
Key Points
- +page.server.ts → server-only (DB, secrets, cookies)
- +page.ts → universal (public APIs, runs on both)
- Returned data becomes the page's `data` prop
- SvelteKit parallelises loads across layout chain
Q16What are SvelteKit form actions and how do they enable progressive enhancement?
IntermediateSvelteKit
Answer
Form actions in SvelteKit are server-side handlers for `<form method='POST'>` submissions, exported from `+page.server.ts`. The killer feature: forms work without JavaScript, SvelteKit auto-enhances them when JS is available for client-side navigation, but if JS fails or is disabled, the browser's native form submission still works. You define actions as named exports (or a single `default` action).
Use `use:enhance` to opt into client-side enhancement with custom feedback (loading state, optimistic UI). Form actions return either success data or `fail()` with validation errors, which appear in the page's `form` prop. This is one of SvelteKit's most differentiated features, Next.js Server Actions (2023) borrowed heavily from it.
What `use:enhance` does with no callback is worth knowing exactly: it submits with `fetch`, applies the returned `ActionResult`, calls `invalidateAll()` on success so every load re-runs against fresh data, and resets the form. Pass a callback and you take that over, it receives `{ formData, cancel, submitter }` and returns a handler for `{ result, update }`; forgetting to call `update()` or `applyAction(result)` there leaves the page frozen after submit, which is the single most reported form-actions bug. Named actions are addressed as `action="?/login"` or with `formaction` on a specific button, and posting to a page that has named actions without the `?/` prefix returns 405. `fail(400, {...})` sets the status without throwing, so it must be returned.
Never echo the password back in the `fail()` payload, it is serialised into the HTML response. Use `redirect(303, ...)` rather than a 302 so the browser reissues a GET instead of replaying the POST.
// src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
export const actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email');
const password = data.get('password');
if (!email || !password) return fail(400, { error: 'Missing fields' });
const user = await db.authenticate(email, password);
if (!user) return fail(401, { error: 'Invalid creds', email });
cookies.set('session', user.token, { path: '/' });
redirect(303, '/dashboard');
}
};
// src/routes/login/+page.svelte
<script>
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" value={form?.email ?? ''} />
<input name="password" type="password" />
{#if form?.error}<p class="err">{form.error}</p>{/if}
<button>Log in</button>
</form>
Q17How does Svelte's compiler-based reactivity differ from React's virtual DOM?
IntermediateArchitecture
Answer
React uses a runtime virtual DOM: every state change triggers a re-render of the component, which builds a new tree of JavaScript objects representing the UI, which React then diffs against the previous tree and applies minimal DOM updates. The cost: bundle includes the React runtime (~40 KB), every render allocates new objects, and you need memoisation (`useMemo`, `useCallback`, `React.memo`) to avoid unnecessary work. Svelte takes the opposite approach: at build time, the compiler analyses your component and emits imperative JavaScript that knows exactly which DOM nodes depend on which pieces of state.
When state changes, the generated code directly updates only those nodes, no virtual DOM, no diff, no reconciliation. This means smaller bundles (no framework runtime to ship), no need for manual memoisation, and predictable update performance. The trade-off: Svelte's reactivity must be statically analysable, which is why Svelte 5 introduced runes, to make the dependency graph explicit and trackable across `.svelte.js` files.
For most apps the difference is marginal, but for low-end devices (common in tier-2/tier-3 India) Svelte's smaller bundles meaningfully improve TTI. The follow-up a senior interviewer asks is where the model costs you. Compiled output scales with template size, so a component with hundreds of bindings emits more code than its React equivalent, and a route pulling in fifty such components can ship a larger chunk even though the framework itself is smaller.
Svelte 5 also reintroduced a genuine runtime, a signals implementation plus proxies, so 'zero runtime' is a Svelte 3 era claim; the honest framing in 2026 is a small runtime plus compiled update code versus a large runtime plus generic diffing. And because tracking is per signal rather than per render, a closure that captured a value instead of reading it through the proxy stops updating silently, a failure mode React's re-render model does not produce.
Key Points
- React: runtime diffing, ships ~40 KB framework code
- Svelte: compile-time analysis, no runtime framework
- Svelte updates DOM nodes directly, no virtual DOM
- No need for useMemo / useCallback / React.memo
Q18What is the `$effect` rune and what are its common gotchas?
IntermediateReactivity
Answer
`$effect` runs a function whenever its reactive dependencies change. It's roughly the Svelte 5 equivalent of React's `useEffect`, but the dependency array is tracked automatically, Svelte's compiler analyses which `$state` and `$derived` values the effect reads. Use it for side effects: DOM manipulation, logging, subscriptions, timers.
Effects run AFTER the DOM updates, in a microtask. Common gotchas: (1) Don't mutate state inside an effect, you can create infinite loops. Use `$derived` for state that depends on other state. (2) Effects ONLY run in the browser by default (not during SSR).
For pre-SSR setup, use `$effect.pre()` to run before DOM updates, or just put the code in `<script>` directly. (3) Return a cleanup function, it runs before the next effect and on component destroy. (4) Avoid `$effect` for syncing two pieces of state, that's often a sign you should refactor to a derived value. Be precise about the mechanics: effects run after the DOM commits, batched into a microtask, and they do not run during SSR at all, which is exactly why `window`, `document` and `localStorage` access belongs in `$effect` and not in the script body. Dependencies are collected only from state read synchronously in the effect body, so anything read after an `await` is untracked, and anything you deliberately want to ignore goes inside `untrack()`. Writing to state you also read in the same effect produces `effect_update_depth_exceeded` ('Maximum update depth exceeded'), while mutating state from inside a `$derived` or a template expression throws `state_unsafe_mutation`. `$effect.pre` runs before the DOM updates, which is how you capture scroll position before a list re-renders. `$effect.root` creates an effect scope outside any component lifecycle and returns a `destroy` function, which you need when wiring reactivity from a plain module.
<script>
let count = $state(0);
let title = $state('Counter');
// ✅ Good: side effect (DOM, logging, subscription)
$effect(() => {
document.title = `${title} (${count})`;
});
// ✅ Good: cleanup function
$effect(() => {
const id = setInterval(() => count++, 1000);
return () => clearInterval(id);
});
// ❌ Bad: infinite loop
$effect(() => {
count = count + 1; // triggers itself
});
// ❌ Bad: should be $derived, not $effect
let doubled = $state(0);
$effect(() => { doubled = count * 2; });
// ✅ Use this instead:
let doubled2 = $derived(count * 2);
</script>
Q19How do you handle hooks in SvelteKit (`hooks.server.ts`)?
IntermediateSvelteKit
Answer
SvelteKit's `src/hooks.server.ts` (and the optional `hooks.client.ts`) lets you intercept every request to your app. The three main hooks: `handle({ event, resolve })` runs for every request and is where you do auth, request logging, and response transformation; `handleFetch({ event, request, fetch })` intercepts internal `fetch()` calls from load functions; `handleError({ error, event })` catches unhandled errors for logging/Sentry. The `handle` hook is composable via `sequence()` so you can chain auth + logging + i18n.
A common pattern: parse the session cookie in `handle`, look up the user, and set `event.locals.user`, then every load function and form action has typed access to `locals.user`. Details worth naming: `resolve(event, opts)` accepts `transformPageChunk` for rewriting the HTML stream (locale attributes, CSP nonce injection), `filterSerializedResponseHeaders` to control which headers from server-side `fetch` calls reach the client, and `preload` to decide which `modulepreload` and stylesheet tags get emitted. `handleFetch` is what lets a `fetch('/api/x')` inside a load function bypass the network and hit the route handler directly, and it is where you attach an internal service token when proxying to a private backend. SvelteKit 2.10 added an `init` hook that runs once before the first request, the right home for a connection pool or a warm cache.
The universal `reroute` and `transport` hooks live in `src/hooks.ts` with no `.server` or `.client` suffix. Two production traps: `handle` also runs for static asset requests, so an unguarded per-request database lookup multiplies by every image on the page, and an exception thrown inside `handle` bypasses `+error.svelte` and returns a bare 500.
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
const auth: Handle = async ({ event, resolve }) => {
const session = event.cookies.get('session');
event.locals.user = session ? await getUserFromSession(session) : null;
return resolve(event);
};
const logger: Handle = async ({ event, resolve }) => {
const start = Date.now();
const response = await resolve(event);
console.log(`${event.request.method} ${event.url.pathname} ${response.status} ${Date.now() - start}ms`);
return response;
};
export const handle = sequence(auth, logger);
// src/app.d.ts, augment Locals type
declare global {
namespace App {
interface Locals { user: User | null }
}
}
Q20How do you build API endpoints in SvelteKit with `+server.ts`?
IntermediateSvelteKit
Answer
A `+server.ts` file in any route directory becomes an HTTP API endpoint at that URL. Export named functions matching HTTP methods (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`), each receives an event object and returns a `Response` (or use the `json()` helper for JSON responses). These endpoints share the same `event.locals`, hooks, and cookies as page routes, so auth done in `hooks.server.ts` applies.
Common uses: webhooks (Razorpay payment callbacks, GitHub events), public APIs consumed by mobile apps, server-rendered images or PDFs. For type-safe internal calls from the client, prefer load functions and form actions instead, `+server.ts` is for external HTTP clients. Tip: when calling your own SvelteKit API from a load function, use the `event.fetch` (not global `fetch`) to inherit cookies and run server-side without a network round-trip.
Production details: a `+server.ts` and a `+page.svelte` cannot sit in the same directory unless the endpoint's `GET` responds to something other than an HTML `accept` header, otherwise the build reports a route conflict. Return `new Response(null, { status: 204 })` for empty bodies rather than `json(null)`. Export `OPTIONS` and set the CORS headers yourself if browsers on another origin will call you, SvelteKit adds none.
Its CSRF protection rejects cross-site form posts with `Cross-site POST form submissions are forbidden` (403) unless you relax `kit.csrf.checkOrigin`, which surprises teams whose partner posts `application/x-www-form-urlencoded`. For signed webhooks (Razorpay, Stripe, GitHub) read the body with `await request.text()` and verify the HMAC before parsing, because `request.json()` consumes the stream and reserialising changes the exact bytes the signature covers. Streaming works by returning a `ReadableStream`, but serverless adapters cap execution time, so long-lived server-sent events belong on adapter-node or Cloudflare rather than a Vercel or Netlify function.
// src/routes/api/posts/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, locals }) => {
const page = Number(url.searchParams.get('page') ?? 1);
const posts = await locals.db.post.findMany({ skip: (page - 1) * 20, take: 20 });
return json({ posts });
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) throw error(401, 'Unauthorized');
const body = await request.json();
const post = await locals.db.post.create({ data: { ...body, authorId: locals.user.id } });
return json(post, { status: 201 });
};
Q21What is `$derived` and when should you use it instead of `$state` or `$effect`?
IntermediateReactivity
Answer
`$derived(expression)` computes a value from other reactive state. The expression re-runs whenever any of its dependencies change, and the result is cached until then. It's the Svelte 5 equivalent of Vue's `computed` or React's `useMemo`, but tracked automatically.
Use `$derived` whenever a value can be calculated from other state. Don't store derived values in `$state` and sync them in an `$effect`, that's a common bug pattern (creates redundant state and update glitches). For more complex calculations that need intermediate steps, use `$derived.by(() => { ... return value; })`, the function form accepts arbitrary logic.
Derived values are LAZY, the expression only runs when something reads the value, not on every state change. This makes them efficient even for expensive calculations on rarely-rendered components. Two behaviours catch people out.
A derived is push-pull: dependencies are marked dirty eagerly but the expression only recomputes when something reads it, so a `console.log` placed inside one will not fire while nothing renders the value, and side effects do not belong there at all. Since Svelte 5.25 a derived is also writable: you can assign to it for optimistic UI and the next dependency change overwrites your assignment, which is exactly the behaviour you want for a like button that reverts when the request fails. Mutating other state from inside the expression throws `state_unsafe_mutation`.
Change detection uses `===` on the result, so a derived that constructs a new array or object on every run always looks changed and re-runs everything downstream; derive primitives where you can, or accept the churn. And because deriving from a `$state` object yields a proxy, comparing the derived value against the original raw object with `===` returns false.
<script>
let items = $state([{ price: 100 }, { price: 250 }]);
let discount = $state(10);
// ✅ Simple derived
let count = $derived(items.length);
// ✅ Derived with logic, use .by()
let total = $derived.by(() => {
const sum = items.reduce((s, i) => s + i.price, 0);
return sum - (sum * discount / 100);
});
// ❌ Anti-pattern: derived stored in $state
let badTotal = $state(0);
$effect(() => { badTotal = items.reduce((s, i) => s + i.price, 0); });
</script>
<p>Count: {count}, Total: ₹{total}</p>
Q22How do transitions and animations work in Svelte?
IntermediateAnimations
Answer
Svelte ships built-in transition primitives in `svelte/transition`: `fade`, `fly`, `slide`, `scale`, `blur`, `draw` (for SVG paths). You apply them with `transition:fade` (in and out), `in:fly` (entry only), or `out:slide` (exit only). They take an options object: `{ duration: 300, delay: 100, easing: cubicOut }`.
The `svelte/animate` module adds `flip` for FLIP animations, when items reorder in an `{#each}`, animate them between positions. For physics-based or complex motion, the `svelte/motion` module provides `tweened` and `spring` stores. The killer feature: all of these are tree-shaken, if you don't import `slide`, it doesn't ship in your bundle.
Compared to React, you don't need framer-motion or react-spring as a separate dependency for the common cases. Behaviour that shows up in code review: a transition only plays when the element is added or removed by a block that owns it, so `transition:fade` on a component's root element does nothing when the parent swaps components; wrap it in `{#if}` or `{#key}`. `transition:` is bidirectional and reverses mid-flight, while separate `in:` and `out:` do not coordinate, which is why rapid toggling with `in:`/`out:` leaves ghost nodes on screen. Since Svelte 4 transitions are local by default, add `|global` (`transition:fade|global`) when a child must animate because an ancestor block toggled.
A custom transition is just a function returning `{ delay, duration, easing, css: (t, u) => ... }`, and the `css` form is compiled to a keyframe animation that the compositor runs, while the `tick` form runs per frame on the main thread and visibly drops frames on cheap Android hardware. Nothing here respects `prefers-reduced-motion` automatically, read it with `matchMedia` and zero the duration yourself.
<script>
import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { cubicOut } from 'svelte/easing';
let items = $state([1, 2, 3]);
let show = $state(true);
</script>
{#if show}
<div transition:fade={{ duration: 200 }}>Fades in and out</div>
{/if}
<button onclick={() => items = [...items, items.length + 1]}>Add</button>
{#each items as item (item)}
<li in:fly={{ y: 20, easing: cubicOut }}
out:slide
animate:flip>
Item {item}
</li>
{/each}
Q23How do you manage global state across components in SvelteKit?
IntermediateState Management
Answer
Three approaches in 2026. (1) **Svelte stores** (`writable`, `readable`, `derived`), the classic approach, works in Svelte 4 and 5, shared across components via import. Best for global UI state: theme, cart, modal visibility. Don't store user-specific data in stores on SvelteKit pages that SSR, module state persists across requests on the server, leaking data between users. (2) **Context API** (`setContext`/`getContext`), for state shared down a component subtree without prop drilling.
Lifecycle is tied to the component tree, so it's safe for SSR. (3) **Runes in `.svelte.ts` files**, Svelte 5's new pattern. Export a function that returns a reactive object, components import and use it like local state. For SSR safety, never use top-level `$state` in a module imported by SSR'd pages; instead, create the state inside a factory function called from `+layout.ts`.
Most production apps use a mix: context for per-request state, stores for browser-only UI state. The concrete rule to state in an interview: modules are evaluated once per server process, so a top-level `$state` or a top-level `writable` is a cross-request singleton. Under real traffic two users routed to the same Node process see each other's data, and it never reproduces locally because you are the only user.
The safe placements are `event.locals` for per-request server state, load return values for per-request client state, and `setContext` during component initialisation, which throws `lifecycle_outside_component` if you call it from an event handler or after an `await`. Context is not reactive by itself, you put a `$state` object or a class with `$state` fields into it and read the getters. And `page`, `navigating` and `updated` from `$app/state` are the supported replacements for the deprecated `$app/stores` equivalents, already scoped correctly per request.
// stores/theme.ts, global UI state (browser-only OK)
import { writable } from 'svelte/store';
export const theme = writable<'light' | 'dark'>('light');
// auth.svelte.ts, runes-based state
export function createAuthState(initial: User | null) {
let user = $state(initial);
return {
get user() { return user; },
login(u: User) { user = u; },
logout() { user = null; }
};
}
// +layout.svelte, wire context per request
<script>
import { setContext } from 'svelte';
import { createAuthState } from './auth.svelte.ts';
let { data, children } = $props();
setContext('auth', createAuthState(data.user));
</script>
Q24What is `bind:` vs `$bindable()` in Svelte 5?
IntermediateComponents
Answer
`bind:` is the parent-side directive that creates two-way binding between a parent's variable and a child's prop. `$bindable()` is the child-side opt-in that marks a prop as bindable. In Svelte 4, every `export let` could be bound, leading to accidental two-way data flow that was hard to audit. Svelte 5 made this explicit: a prop is one-way by default and you must call `$bindable()` to allow `bind:` from the parent.
This makes data flow easier to reason about, if you see a prop without `$bindable()`, you know mutations stay inside the child. `$bindable()` can take a default value: `let { value = $bindable('') } = $props()`. Bindable props are useful for custom form controls (date pickers, autocomplete), but for most cases callback props (`onChange`-style) are simpler and avoid hidden mutation. Mechanically `bind:x={y}` compiles to a getter and setter pair handed to the child alongside the prop, so the child assigning `value = 'x'` calls back into the parent's setter.
That is why binding to a `$derived` fails at runtime: the parent must own real state. `$bindable(fallback)` supplies a value used only when the parent passes nothing, and binding is optional, the same component still works one-way when the parent passes a plain value. `bind:value` on its own is shorthand for `bind:value={value}`. The design guidance a senior interviewer wants to hear: reach for `$bindable` when wrapping a native form control where two-way is the honest model, and use callback props (`onchange`, `onselect`) everywhere else, because a binding hides the mutation site and makes a bug hard to trace across three component layers. Bindings also cannot travel in a spread, `{...props}` never carries one.
<!-- TextInput.svelte -->
<script lang="ts">
// bindable: parent CAN use bind:value
let { value = $bindable(''), label = '' } = $props<{
value?: string; label?: string;
}>();
</script>
<label>
{label}
<input bind:value />
</label>
<!-- Parent -->
<script>
let name = $state('');
</script>
<TextInput bind:value={name} label="Name" />
<p>You typed: {name}</p>
Q25How does SvelteKit handle SSR vs CSR vs prerendering?
IntermediateRendering
Answer
SvelteKit supports three rendering modes per route, controlled by exporting flags from `+page.ts` / `+page.server.ts` or `+layout.ts`. **SSR (default)**: the page is rendered to HTML on the server for every request, fast first paint, SEO-friendly, dynamic data. **Prerendering** (`export const prerender = true`): the page is rendered once at build time to a static HTML file, best for content that never changes (marketing, docs). **CSR-only** (`export const ssr = false`): no server rendering, page hydrates from JS, useful for fully personalised dashboards behind auth. You can also disable client-side JS entirely with `export const csr = false`, perfect for content pages that don't need interactivity (blog posts), shipping zero JS to the browser. The combination is unique to SvelteKit: most frameworks force one mode per app, but SvelteKit lets you mix on a per-route basis.
For a typical SaaS in India, marketing pages might be prerendered, the dashboard SSR'd, and an admin panel CSR-only, all in the same app. Two things trip teams in production. A route marked `prerender = true` that nothing links to fails the build with 'The following routes were marked as prerenderable, but were not prerendered', so you either link it, add it to `kit.prerender.entries`, or export an `entries()` function from the route that enumerates its ids.
And these flags inherit down the tree from `+layout.ts`, so `ssr = false` on the root layout silently strips server rendering (and therefore SEO) from every page beneath it, a mistake that usually surfaces weeks later as a traffic drop. `csr = false` also disables `use:enhance`, client-side routing and every `$effect`, so the page has to work as plain HTML. For prerendered output on S3 or Cloudflare Pages, set `export const trailingSlash = 'always'` so directory URLs resolve to `index.html` instead of 404ing.
// src/routes/blog/[slug]/+page.ts
export const prerender = true; // static HTML at build
// src/routes/blog/+page.ts
export const prerender = 'auto'; // prerender if no params
// src/routes/dashboard/+page.ts
export const ssr = true; // SSR (default)
export const csr = true; // hydrate on client (default)
// src/routes/admin/+page.ts
export const ssr = false; // CSR only (skip server render)
// src/routes/about/+page.ts
export const csr = false; // ship zero JS (HTML only)
Q26How do you handle errors and error boundaries in SvelteKit?
IntermediateError Handling
Answer
SvelteKit has three layers of error handling. (1) **Expected errors**: throw `error(status, message)` from `@sveltejs/kit` in load functions, actions, or `+server.ts` handlers. SvelteKit catches these and renders the nearest `+error.svelte`. (2) **`+error.svelte` boundary**: place this file at any level of the route tree. It receives the error via `page.error` and renders a user-friendly message.
The closest `+error.svelte` up the directory tree handles the error. (3) **`handleError` hook** in `hooks.server.ts` and `hooks.client.ts`: catches unhandled errors for logging (Sentry, Datadog). Crucially, the message you throw is only shown to users if you used `error()`, uncaught exceptions show a generic 'Internal Error' to avoid leaking stack traces. Always use `error()` for expected failures (404, 403, validation), and reserve uncaught throws for actual bugs.
Specifics that matter on the job: in SvelteKit 2 `error()` and `redirect()` no longer need `throw` because they throw internally, so wrapping a load body in a broad `try/catch` swallows your redirects and converts them into 500s; rethrow anything that passes `isHttpError()` or `isRedirect()` from `@sveltejs/kit`. A `+error.svelte` cannot catch an error raised by its own `+layout.server.ts`, the handling boundary is the parent, which is why a failure in the root layout renders the bare fallback you customise at `src/error.html`. `handleError` fires only for unexpected errors, never for `error()` calls, and whatever it returns becomes `page.error`, so generate a request id there, put it in the user-facing message, and log the same id to Sentry so support can map a screenshot to a trace. On the client, Svelte 5.3 added `<svelte:boundary>` with a `failed` snippet and an `onerror` handler, giving you a component-level boundary for render-time errors that route boundaries do not cover.
// src/routes/posts/[id]/+page.server.ts
import { error } from '@sveltejs/kit';
export const load = async ({ params }) => {
const post = await db.post.findUnique({ where: { id: params.id } });
if (!post) throw error(404, 'Post not found');
return { post };
};
// src/routes/posts/+error.svelte
<script>
import { page } from '$app/state';
</script>
<h1>{page.status}: {page.error?.message}</h1>
<a href="/">Go home</a>
// src/hooks.server.ts
export const handleError: HandleServerError = ({ error, event }) => {
Sentry.captureException(error, { extra: { url: event.url.toString() } });
return { message: 'Something went wrong' };
};
Q27What are SvelteKit adapters and how do you deploy to different platforms?
IntermediateDeployment
Answer
An adapter is a SvelteKit plugin that transforms your built app into the format expected by a target deployment platform. Configured in `svelte.config.js`, the adapter runs at the end of `svelte-kit build`. Official adapters: `@sveltejs/adapter-node` (Node.js server, works anywhere, Docker, EC2, VPS), `@sveltejs/adapter-static` (pure SSG, for prerendered apps on S3/Cloudflare Pages/GitHub Pages), `@sveltejs/adapter-vercel`, `@sveltejs/adapter-netlify`, `@sveltejs/adapter-cloudflare` (Workers, runs on the edge), `@sveltejs/adapter-cloudflare-workers`.
Community adapters cover AWS Lambda, Deno Deploy, Bun, and more. Adapter-auto detects supported platforms automatically, good for getting started, but explicit adapters give you control over runtime settings (Node version, function timeouts). The same SvelteKit codebase deploys to all of these without code changes, just swap the adapter.
What actually differs is the runtime contract, and that is what senior interviews probe. `adapter-node` gives you a long-lived process, so in-memory caches, database connection pools and server-sent events work; it reads `PORT`, `HOST`, `ORIGIN`, `BODY_SIZE_LIMIT` and `SHUTDOWN_TIMEOUT` from the environment, and omitting `ORIGIN` behind Nginx or an ALB makes every form POST fail with a 403 cross-site error. `adapter-cloudflare` targets workerd rather than Node, so `fs`, most native database drivers and long timeouts are unavailable and platform bindings arrive on `platform.env` instead of `process.env`. Serverless adapters give per-request isolation plus cold starts, so a naive Prisma pool becomes a connection storm unless you front it with a pooler. `adapter-static` requires every route to be prerenderable and needs a `fallback` page for SPA-style routes. `adapter-auto` only detects a platform when the build runs on that platform, so a Docker or self-hosted build gets nothing usable, pin the adapter explicitly for anything you actually ship.
// svelte.config.js, Node server
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({ out: 'build' })
}
};
// svelte.config.js, Cloudflare Workers (edge)
import adapter from '@sveltejs/adapter-cloudflare';
export default {
kit: { adapter: adapter() }
};
// Dockerfile for adapter-node
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY build ./build
EXPOSE 3000
CMD ["node", "build"]
Q28What is a `use:` action, and how do attachments (`{@attach}`) change that in Svelte 5?
IntermediateComponents
Answer
An action is a function attached to a DOM element with `use:tooltip`. Svelte calls it with the node (and one optional parameter) when the element mounts, and you return `{ update, destroy }` to respond to parameter changes and clean up on unmount. Actions are how you wrap imperative libraries: a Chart.js canvas, a Leaflet map, an IMask input, a click-outside listener.
They never run during SSR, which also makes them a safe home for `window` and `document`. Svelte 5.29 added attachments, `{@attach fn}`, which supersede actions. An attachment receives the node and optionally returns a teardown, and the important difference is that it runs inside an effect: any `$state` it reads is tracked, so the attachment re-runs automatically when that state changes and there is no separate `update` callback to keep in sync.
Attachments also compose in ways actions cannot. They can be created inline from a factory, they survive a spread (`{...rest}` carries an attachment, `use:` never did), and they can be applied to a component so a parent attaches behaviour to whatever the child spreads onto its root element. Practical notes for the follow-up.
An action takes exactly one parameter, so `use:tip={{ text, placement }}` is the idiomatic shape and `update` fires with the new object on every change. Actions cannot be spread and cannot be applied to components. An error thrown inside either one surfaces as an ordinary render error, not something `+error.svelte` catches. When migrating, `fromAction()` in `svelte/attachments` wraps an existing action so third-party code keeps working.
<script lang="ts">
import type { Attachment } from 'svelte';
let open = $state(true);
let color = $state('#ff3e00');
// Svelte 4 style action: one param, manual update/destroy
function clickOutside(node: HTMLElement, onOut: () => void) {
const handler = (e: MouseEvent) => {
if (!node.contains(e.target as Node)) onOut();
};
document.addEventListener('click', handler, true);
return {
destroy: () => document.removeEventListener('click', handler, true)
};
}
// Svelte 5 attachment: runs in an effect, so `color` is tracked
const highlight: Attachment<HTMLElement> = (node) => {
node.style.outline = `2px solid ${color}`;
return () => { node.style.outline = ''; };
};
</script>
{#if open}
<div use:clickOutside={() => (open = false)}>menu</div>
{/if}
<div {@attach highlight}>outline follows state, no update callback</div>
<input type="color" bind:value={color} />
Q29How do `invalidate`, `invalidateAll`, `goto` and `preloadData` control data freshness in SvelteKit?
IntermediateSvelteKit
Answer
SvelteKit caches load results per navigation, so after a mutation you have to tell it what went stale. `invalidateAll()` from `$app/navigation` re-runs every load function feeding the current page: the blunt instrument, and exactly what `use:enhance` does on a successful action by default. `invalidate('app:orders')` re-runs only the loads that registered that key by calling `depends('app:orders')`. `invalidate(url)` or `invalidate((u) => u.pathname === '/api/orders')` re-runs loads that fetched that URL through the load's own `event.fetch`, which is why reaching for the global `fetch` inside a load silently breaks invalidation later. `goto('/dashboard', { replaceState, invalidateAll, keepFocus, noScroll, state })` navigates on the client with no document reload. Use it for programmatic navigation from an event handler only; inside a load or action you return `redirect(303, ...)` instead, because a `goto` there does nothing on the server. `preloadData('/orders/42')` runs the target route's loads and downloads its JS before the click; `preloadCode` fetches only the code. The declarative equivalent is `data-sveltekit-preload-data="hover"` or `"tap"` on a link or any ancestor, and `data-sveltekit-reload` forces a full document request for paths another app owns. `beforeNavigate` and `afterNavigate` cover guards and scroll handling: `beforeNavigate` gets `cancel()` for an unsaved-changes prompt but cannot block a real page unload, so pair it with `onbeforeunload`. Read the in-flight navigation from `navigating` in `$app/state` to drive a progress bar rather than touching `location`, which is undefined during SSR.
// src/routes/orders/+page.server.ts
export const load = async ({ depends, locals }) => {
depends('app:orders');
return { orders: await locals.db.order.findMany() };
};
// src/routes/orders/+page.svelte
<script lang="ts">
import { invalidate, goto, preloadData, beforeNavigate } from '$app/navigation';
import { navigating } from '$app/state';
let { data } = $props();
let dirty = $state(false);
beforeNavigate(({ cancel }) => {
if (dirty && !confirm('Discard unsaved changes?')) cancel();
});
</script>
{#if navigating.to}<progress></progress>{/if}
<button onclick={() => invalidate('app:orders')}>Refresh</button>
<button
onmouseenter={() => preloadData('/orders/new')}
onclick={() => goto('/orders/new', { noScroll: true })}
>New order</button>
<a href="/orders/42" data-sveltekit-preload-data="hover">Order 42</a>
Q30How do you test Svelte 5 components with Vitest, and why do effects need `flushSync`?
IntermediateTesting
Answer
The standard setup is Vitest (it reuses your existing Vite config, so `.svelte` files already compile) plus `vitest-browser-svelte` or `@testing-library/svelte` for rendering. What `npx sv add vitest` scaffolds in 2026 is two projects inside one config: a `client` project matching `*.svelte.test.ts` with a jsdom or real-browser environment, and a `server` project with `environment: 'node'` for everything else. That split matters because load functions, form actions and `+server.ts` handlers are plain functions, so you test them by calling `load({ params, locals })` with a hand-built event object and asserting on the return value: no framework harness required.
The rune-specific trap is scheduling. Effects are batched and flush in a microtask after the DOM commits, so an assertion written immediately after a state change reads the previous DOM. `flushSync()` from `svelte` forces a synchronous flush and `await tick()` waits for the pending one; Testing Library usually hides this because `await userEvent.click()` already yields to the scheduler. Runes outside a component need an owner. `$state` at the top level of a test compiles, but effects will not run and you get `effect_orphan` until you wrap them in `$effect.root(() => ...)` and call the returned destroy function in `afterEach`.
Runes in a plain test file only work if the file itself is named `*.svelte.test.ts`, which is the whole reason for the two-project split. Playwright against `vite preview` remains the only way to test SSR output, hydration, and form actions with JavaScript disabled.
// src/lib/Counter.svelte.test.ts
import { render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';
import Counter from './Counter.svelte';
test('increments on click', async () => {
render(Counter, { props: { start: 1 } });
const btn = screen.getByRole('button');
await userEvent.click(btn);
expect(btn).toHaveTextContent('2');
});
test('effect reruns when state changes', () => {
const log: number[] = [];
const destroy = $effect.root(() => {
let count = $state(0);
$effect(() => log.push(count));
flushSync(); // run the effect once
count = 5;
flushSync(); // and again after the change
});
destroy();
expect(log).toEqual([0, 5]);
});
// src/routes/orders/page.server.test.ts, runs in the node project
import { load } from './+page.server';
test('load returns orders', async () => {
const res = await load({ locals: { db: fakeDb } } as never);
expect(res.orders).toHaveLength(2);
});
Q31How would you architect a large SvelteKit app for a team of 10+ developers?
AdvancedArchitecture
Answer
For a 10+ developer SvelteKit codebase in production, the patterns that hold up: (1) **Feature folders, not type folders**, group routes, components, server logic, and types under `src/lib/features/<feature>/` rather than splitting by `components/` and `routes/`. SvelteKit's `$lib` alias makes this clean. (2) **Strict TypeScript everywhere**, `strict: true`, `noUncheckedIndexedAccess: true`, generate types from `$types` imports. SvelteKit's type generation catches almost all wiring bugs. (3) **Repository pattern for data access**, never call Prisma/Drizzle directly from `+page.server.ts`; route through a thin repository layer in `src/lib/server/repos/`.
This isolates database concerns for testing. (4) **Auth in hooks, not pages**, `hooks.server.ts` populates `event.locals.user`; every load and action checks it. Never re-implement auth per route. (5) **Component library in `$lib/components/`** with Storybook or Histoire. Mark internal components with `_` prefix or move them inside the feature folder. (6) **Zod or Valibot for input validation** at every boundary, form actions, API endpoints, query params.
Don't trust types from `request.formData()`, they're all strings. (7) **CI** runs `svelte-check`, `eslint`, `vitest`, and `playwright`. Production builds use `vite build && npm run preview` smoke test before deploy. Indian companies running setups at this shape: Plivo (developer infrastructure), some teams at Razorpay for marketing/landing pages.
Key Points
- Feature folders under $lib/features/
- Strict TypeScript with generated $types
- Repository pattern between routes and DB
- Auth in hooks.server.ts populating event.locals
- Zod/Valibot at every input boundary
Q32How do you optimise Svelte bundle size and TTI for low-end devices?
AdvancedPerformance
Answer
Svelte starts with a bundle advantage but you can still bloat it. The wins, in order of impact: (1) **Prerender static routes**, `export const prerender = true` ships zero JS for content pages. Combined with `export const csr = false` you ship pure HTML. (2) **Code-split per route**, SvelteKit does this automatically per `+page.svelte`, but watch out for shared `$lib/` imports that bloat the entry chunk.
Use dynamic `import()` for heavy dependencies (charts, rich-text editors, PDF viewers). (3) **Audit dependencies**, many npm packages were sized for Webpack tree-shaking and are huge in practice. Use `vite-bundle-visualizer` or `rollup-plugin-visualizer` to find offenders. Common culprits: `moment` (use `date-fns` or `dayjs`), `lodash` (use lodash-es with named imports), entire icon libraries (use individual SVG imports). (4) **Image optimization**, `@sveltejs/enhanced-img` plugin generates responsive images with modern formats (AVIF, WebP) at build time. (5) **Use Svelte's built-ins**, `svelte/transition`, `svelte/animate`, `svelte/store` ship lighter than npm equivalents and tree-shake well. (6) **Hydration cost matters**, `data-sveltekit-noscroll`, `data-sveltekit-preload-data='hover'` reduce navigation overhead. For tier-2/tier-3 India (3G networks, ₹6,000 phones), targeting < 100 KB JS on the critical path and < 3s TTI is realistic with SvelteKit but requires discipline.
// Code-split a heavy component
<script>
import { onMount } from 'svelte';
let Editor = $state(null);
onMount(async () => {
const mod = await import('./RichTextEditor.svelte');
Editor = mod.default;
});
</script>
{#if Editor}
<Editor />
{:else}
<textarea placeholder="Loading editor..." />
{/if}
// vite.config.ts, visualize bundle
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
sveltekit(),
visualizer({ open: true, gzipSize: true })
]
};
Q33How does Svelte's compiler-based reactivity scale, and what are its fundamental limits?
AdvancedArchitecture
Answer
Svelte's compiler-based model has known trade-offs that emerge at scale. (1) **Static analysability**: the compiler must trace which DOM nodes depend on which state. In Svelte 4 this required reactivity to live at the top of `<script>`. Svelte 5's runes solved this by making `$state` and `$derived` callable in any `.svelte.js/ts` file, the compiler now generates subscription code per rune rather than relying on lexical scope. (2) **Component-boundary granularity**: unlike SolidJS (which has true fine-grained reactivity at the value level), Svelte still re-runs the affected component's update function.
For most apps this is fine; for components with 10,000+ reactive bindings (data grids, complex spreadsheets), you may hit performance walls SolidJS would handle better. (3) **Compiled output size grows with template complexity**: Svelte trades runtime size for output size, a large component with many bindings compiles to more code than its React equivalent (which uses a generic diffing runtime). The crossover point where React's runtime overhead becomes cheaper than Svelte's compiled output is around 50,000+ LOC of components, almost no real apps reach it. (4) **Server-side rendering**: Svelte's SSR is a separate code path that re-implements rendering as string concatenation, components must work in both environments, and effects don't run on the server, which can surprise developers used to client-only frameworks. (5) **Static analysis limits**: dynamic property access (`obj[varName]`) is harder for the compiler to track than direct access (`obj.foo`), Svelte 5 added runtime tracking via proxies (`$state` returns a Proxy) which fixes most cases but adds a small runtime cost. The net: for SaaS dashboards, marketing sites, and content apps, Svelte scales beautifully. For ultra-high-performance data viz or 60fps real-time apps with thousands of reactive bindings, SolidJS may be a better fit.
Q34How do you implement type-safe end-to-end data flow from database to component in SvelteKit?
AdvancedTypeScript
Answer
Type-safety from DB → server → wire → client is the killer feature of full-stack TypeScript apps. SvelteKit's approach: (1) **DB layer**, use Prisma, Drizzle, or Kysely to generate types from your schema. (2) **Repository layer**, wrap DB calls in functions whose return types are inferred and exported. (3) **Load functions**, the return type of `load` flows automatically into the page's `data` prop via the generated `./$types` import (`PageData`). (4) **Form actions**, same `./$types` exports `ActionData` for the `form` prop, capturing both success returns and `fail()` shapes. (5) **Validation**, Zod schemas at every input boundary; pass `zod.infer<typeof schema>` types into your repository functions. (6) **API endpoints**, `+server.ts` returns `Response`, so you lose types at the network boundary, define a shared types module (`$lib/api/types.ts`) and import from both server and client. For end-to-end types over public APIs, libraries like `superforms` (Svelte equivalent of react-hook-form + Zod) and `sveltekit-superforms` give you typed form state + validation with zero schema duplication.
The result: rename a field in your Prisma schema, run `prisma generate`, and TypeScript surfaces every page, action, and component that needs to update. This catches more bugs at compile time than any test suite.
// src/lib/server/repos/posts.ts
import { db } from '$lib/server/db';
export async function findBySlug(slug: string) {
return db.post.findUnique({
where: { slug },
include: { author: { select: { name: true } } }
});
}
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { findBySlug } from '$lib/server/repos/posts';
export const load: PageServerLoad = async ({ params }) => {
const post = await findBySlug(params.slug);
if (!post) throw error(404);
return { post }; // type inferred end-to-end
};
// src/routes/blog/[slug]/+page.svelte
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
// data.post.author.name → fully typed
</script>
Q35How do you migrate a large Svelte 4 codebase to Svelte 5 runes?
AdvancedMigration
Answer
Svelte 5 ships with a migration script (`npx sv migrate svelte-5`) that handles 80% of mechanical transformations: `export let` → `$props()`, `let x` at top level → `$state(x)`, `$:` derivations → `$derived()` or `$effect()`, `on:click` → `onclick`, `createEventDispatcher` → callback props. The remaining 20% requires manual review and is where teams get stuck. Key gotchas: (1) **`$:` was overloaded for both derivations and effects**, the migrator guesses but can be wrong.
Audit every `$:` to confirm whether it's a `$derived` (pure computation) or `$effect` (side effect). (2) **Array/object mutation now triggers updates**, code that worked around Svelte 4's `arr = arr` reassignment trick may have redundant reassignments that are now no-ops but confusing. (3) **Slots → snippets**, Svelte 4's named slots and `let:` directive map to Svelte 5's `{#snippet}` and `{@render}`. Migration is non-trivial for component libraries with complex slot APIs. (4) **`createEventDispatcher` removed**, refactor to callback props, but typed event payloads are now your responsibility (use `(event: { type: string; data: T }) => void` shapes). (5) **Stores still work, but consider migrating to `.svelte.ts` modules**, runes-based state composes better than stores for new code, but stores are not deprecated and don't need urgent migration. Practical plan for a 50+ component codebase: branch the repo, run the migrator, fix `svelte-check` errors, run the full Playwright suite, manually audit any component with `$:` or `createEventDispatcher`, ship behind a feature flag for one team, then roll out. Plan 2-4 weeks of engineering time for a non-trivial app.
Key Points
- Run `npx sv migrate svelte-5` for mechanical transforms
- Audit every $:, derived vs effect ambiguity
- Slots → snippets is the hardest manual change
- Stores still work, migrate to runes opportunistically
- Plan 2-4 weeks for a 50+ component app
Q36A SvelteKit page logs `hydration_mismatch` and the DOM ends up wrong. How do you debug it?
AdvancedDebugging
Answer
`hydration_mismatch` means the HTML the server produced does not match what the client expects on its first pass, so Svelte discards the mismatched subtree and rebuilds it. You see a flash of the wrong content, handlers bound to the wrong nodes, and lost focus or scroll position. Four causes account for almost all of it. (1) Invalid HTML nesting.
The browser parser silently hoists a `<div>` out of a `<p>`, or wraps a bare `<tr>` in a `<tbody>`, so the DOM differs from the string the server sent. Svelte 5 catches most of these at compile time as `node_invalid_placement_validation`, which is a build error rather than a warning, and the remaining cases come from `{@html}` content whose markup you do not control. (2) Non-deterministic values rendered straight into markup: `Date.now()`, `Math.random()`, `crypto.randomUUID()`, or `toLocaleString()` with no explicit locale. Compute them in a load function so both sides see one value, and use `$props.id()` for generated ids. (3) Browser-only state read during initialisation: `localStorage`, `matchMedia`, `navigator.language` touched in the script body rather than inside `$effect` or behind the `browser` guard from `$app/environment`. (4) Timezone drift, where the server formats in UTC and the browser in IST and every timestamp shifts by 5:30.
Debug it against `vite preview`, never `vite dev`, so you are reading the real SSR output: view-source, then diff against the hydrated DOM in devtools. Setting `export const csr = false` on the route temporarily proves whether the server HTML alone is correct.
<script lang="ts">
let { data } = $props();
// ❌ differs between server and client on every request
// const id = crypto.randomUUID();
// let theme = localStorage.getItem('theme') ?? 'light';
// ✅ stable across SSR and hydration
const id = $props.id();
// ✅ browser-only reads belong in an effect
let theme = $state('light');
$effect(() => {
theme = localStorage.getItem('theme') ?? 'light';
});
// ✅ pin locale and timezone so both sides format identically
const when = new Intl.DateTimeFormat('en-IN', {
timeZone: 'Asia/Kolkata',
dateStyle: 'medium'
}).format(new Date(data.createdAt));
</script>
<span {id}>Created</span>
<p aria-describedby={id} data-theme={theme}>{when}</p>
Q37A Svelte 5 table of 5,000 rows janks on every keystroke. How do you profile and fix it?
AdvancedPerformance
Answer
Measure before changing anything. `$inspect.trace('label')` as the first statement inside an effect or derived prints, in the dev console, which reactive value caused that run and where it was assigned; it is the quickest way to find state that nobody expected to change. Then take a Chrome performance profile: long tasks under `Proxy.set` or a single enormous each-block update point at reactivity, long tasks under Recalculate Style point at CSS, and a storm of ResizeObserver callbacks almost always means a `bind:clientWidth` on every row. The fixes, in order of payoff. (1) `$state.raw` for data you replace wholesale. `$state([...5000 rows])` wraps the array and every nested object in a Proxy as they are read; `$state.raw` skips all of it and you reassign to update.
For tables and search results this is the single biggest win. (2) Key the `{#each}` on a stable id so a reorder moves nodes instead of rewriting every cell. (3) Do not build a new array on each keystroke if it feeds fifty children: change detection is `===`, so a fresh array invalidates everything downstream. Debounce the query into a separate `$state`. (4) Virtualise past a few thousand rows; no reactivity model makes 5,000 live DOM rows cheap. (5) Wrap reads you deliberately do not want tracked in `untrack()`. Two anti-patterns to call out: `flushSync()` inside a scroll or input handler forces a synchronous flush per event and destroys the frame budget, and `$state.snapshot()` deep-clones, so calling it in a render path is silently O(n) on every update.
<script lang="ts">
type Row = { id: string; name: string };
// ❌ deep proxy over 5,000 rows and every nested object
// let rows = $state<Row[]>([]);
// ✅ raw: no proxying, reassign the array to update
let rows: Row[] = $state.raw([]);
let query = $state('');
let debounced = $state('');
$effect(() => {
const q = query;
const id = setTimeout(() => (debounced = q), 150);
return () => clearTimeout(id);
});
const visible = $derived(
debounced ? rows.filter((r) => r.name.includes(debounced)) : rows
);
$effect(() => {
$inspect.trace('visible-rows'); // logs what triggered this run
visible.length;
});
</script>
<input bind:value={query} placeholder="Filter" />
{#each visible.slice(0, 100) as row (row.id)}
<div class="row">{row.name}</div>
{/each}
Q38What are SvelteKit remote functions and the experimental `async` compiler option?
AdvancedSvelteKit
Answer
Both are where the framework is heading, and both are still behind flags, so the answer an interviewer wants names them, explains the problem they solve, and states the risk honestly. Remote functions landed in SvelteKit 2.27 behind `kit: { experimental: { remoteFunctions: true } }`. You write a `.remote.ts` file exporting `query`, `form`, `command` or `prerender` wrappers from `$app/server`, then import those functions directly into any component.
The server keeps the body; the client receives a typed RPC stub. That collapses the usual `+page.server.ts` load, plus a `+server.ts` endpoint, plus a manual `invalidate` call, into one function callable from a deeply nested component that owns no route of its own. `query` results are cached and refreshed with `.refresh()`, `form` gives progressive enhancement without a route-level action, and `command` is an explicit mutation you fire from an event handler. Async Svelte is the compiler half, enabled with `compilerOptions: { experimental: { async: true } }`: you can `await` directly in `<script>` and inside markup expressions, and `<svelte:boundary>` gains a `pending` snippet that renders while those awaits settle.
Paired with `getAbortSignal()`, a superseded render cancels its own in-flight fetches instead of racing. The caveats you should volunteer unprompted: both are experimental and have already changed shape between minor releases, a remote function is a public HTTP endpoint so its argument schema (Standard Schema, meaning Zod or Valibot) is real validation and not decoration, and for anything shipping to production in 2026 load functions plus form actions remain the safe default.
// svelte.config.js
export default {
compilerOptions: { experimental: { async: true } },
kit: { experimental: { remoteFunctions: true } }
};
// src/routes/orders/data.remote.ts
import * as v from 'valibot';
import { query, command } from '$app/server';
import { db } from '$lib/server/db';
export const getOrders = query(v.string(), async (status) => {
return db.order.findMany({ where: { status } });
});
export const cancelOrder = command(v.string(), async (id) => {
await db.order.update({ where: { id }, data: { status: 'cancelled' } });
await getOrders('open').refresh();
});
// src/routes/orders/+page.svelte
<script lang="ts">
import { getOrders, cancelOrder } from './data.remote';
</script>
<svelte:boundary>
{#each await getOrders('open') as order (order.id)}
<li>
{order.id}
<button onclick={() => cancelOrder(order.id)}>Cancel</button>
</li>
{/each}
{#snippet pending()}<p>Loading orders</p>{/snippet}
</svelte:boundary>
Key Points
- `.remote.ts` exports query / form / command / prerender from $app/server
- Enable with kit.experimental.remoteFunctions
- Async Svelte: compilerOptions.experimental.async plus <svelte:boundary pending>
- Arguments need a Standard Schema validator, the endpoint is public
- Still experimental: load functions and form actions stay the production default
Q39How do you keep secrets out of the client bundle and configure CSP in SvelteKit?
AdvancedSecurity
Answer
Three mechanisms, and the interesting part is which one fails loudly and which one fails silently. Environment variables fail loudly. `$env/static/private` and `$env/dynamic/private` are server-only, and importing either from anything reachable by the client is a build error: `Cannot import $env/static/private into client-side code`. Only names prefixed `PUBLIC_` are reachable through `$env/static/public`. `static` values are inlined at build time, so rotating a key needs a rebuild, while `dynamic` reads `process.env` at runtime, which works with adapter-node and Docker secrets but is unavailable in `adapter-static` and during prerendering.
Anything under `src/lib/server/` or named `*.server.ts` is blocked from client imports by the same mechanism. The wire fails silently, and that is where real leaks happen. Everything a load function returns is serialised into the HTML for hydration, so a `user` object carrying `passwordHash`, `otpSecret` or a full ORM row is readable in view-source even though the code ran in `+page.server.ts`.
Select columns explicitly and never return a raw row. For CSP, `kit.csp.directives` makes SvelteKit generate nonces or hashes for its own inline scripts and inject them into `%sveltekit.head%`; `mode: 'auto'` gives hashes to prerendered pages and nonces to dynamic ones. Third-party origins you add yourself.
Session cookies need `httpOnly: true, secure: true, sameSite: 'lax'` and a `path`, which SvelteKit 2 makes mandatory. Finally `{@html}` sanitises nothing, so user-generated markup goes through DOMPurify before it reaches the template.
// svelte.config.js
export default {
kit: {
csp: {
mode: 'auto',
directives: {
'script-src': ['self'],
'style-src': ['self', 'unsafe-inline'],
'frame-ancestors': ['none']
}
}
}
};
// src/lib/server/payments.ts, unreachable from client code
import { createHmac } from 'node:crypto';
import { RAZORPAY_KEY_SECRET } from '$env/static/private';
export function verifyWebhook(rawBody: string, signature: string) {
const digest = createHmac('sha256', RAZORPAY_KEY_SECRET)
.update(rawBody)
.digest('hex');
return digest === signature;
}
// src/routes/account/+page.server.ts
import { PUBLIC_RAZORPAY_KEY_ID } from '$env/static/public';
export const load = async ({ locals }) => {
const row = await locals.db.user.findUnique({ where: { id: locals.user.id } });
// pick fields explicitly, the whole return value ships in the HTML
return {
user: { id: row.id, name: row.name, email: row.email },
razorpayKeyId: PUBLIC_RAZORPAY_KEY_ID
};
};
Q40What drives the hosting bill for a SvelteKit app, and how does the adapter change it?
AdvancedCost
Answer
Cost tracks the rendering mode far more than the framework, and it splits into four buckets. Prerendered routes cost a CDN request plus storage, effectively nothing. Every marketing page, docs page and blog post you move from SSR to `export const prerender = true` deletes one compute invocation per view, permanently. `adapter-vercel` offers the middle ground through `export const config = { isr: { expiration: 3600 } }`: rendered once, cached at the edge, revalidated on a timer, which is the correct setting for a jobs listing that changes hourly rather than per visitor.
Serverless adapters (`adapter-vercel`, `adapter-netlify`) bill invocations plus GB-seconds, so the bill scales with traffic and with how long your load functions sit blocked on a database. A slow query becomes a line item, not just a latency complaint. Cold starts also mean a fresh connection per instance, so you pay again for PgBouncer or RDS Proxy in front of Postgres. `adapter-cloudflare` bills CPU time rather than wall-clock, so waiting on your origin is free, which suits read-heavy pages; the catch is workerd, meaning no Node built-ins, no long-lived connections and a per-request CPU ceiling. `adapter-node` on a VM is a flat monthly cost with unlimited requests until you saturate it, and it is usually cheapest once traffic is steady, with in-memory caching removing database calls entirely. The overlooked drivers: images (use `@sveltejs/enhanced-img` at build time instead of a per-request image service), egress on oversized JS chunks, and log ingestion from a `handle` hook that logs every static asset request.
// src/routes/blog/[slug]/+page.ts
export const prerender = true; // build-time HTML, CDN only, no compute
// src/routes/jobs/+page.server.ts, Vercel ISR
export const config = {
isr: { expiration: 3600 }, // render once, revalidate hourly
runtime: 'nodejs22.x'
};
// src/routes/api/health/+server.ts
export const config = { runtime: 'edge' };
// svelte.config.js, one codebase, three deploy targets
import node from '@sveltejs/adapter-node';
import vercel from '@sveltejs/adapter-vercel';
import cloudflare from '@sveltejs/adapter-cloudflare';
const target = process.env.DEPLOY_TARGET;
const adapter =
target === 'cloudflare' ? cloudflare()
: target === 'vercel' ? vercel({ runtime: 'nodejs22.x' })
: node({ out: 'build' });
export default { kit: { adapter } };
Key Points
- Prerender + CDN is the cheapest tier, move every static route there first
- Serverless bills invocations and GB-seconds, so slow queries cost money
- Cloudflare bills CPU time, not wall-clock, good for I/O-bound pages
- adapter-node on a VM is flat-cost and allows in-memory caching
- Vercel ISR via `export const config = { isr: { expiration } }`
Frequently Asked Questions
Is Svelte production-ready in 2026?
Yes. Svelte has shipped major releases on schedule since 2019, Svelte 5 stabilised the runes API in late 2024, and SvelteKit 2.x is the recommended deployment path. Companies running Svelte in production at scale include Apple (parts of music.apple.com), Spotify (some internal tools), IBM, Brave Browser, Codecademy, and a growing list of Indian startups including Plivo. The ecosystem is smaller than React but covers all the common cases: UI libraries (skeleton.dev, bits-ui, melt-ui, shadcn-svelte), state, forms, animations, and testing.
How much does a Svelte / SvelteKit developer earn in India?
₹6-20 LPA in 2026 for mid-to-senior frontend developers with Svelte as their primary stack. Lower than equivalent React roles (₹8-25 LPA) because of smaller demand, but the gap is closing. Companies hiring Svelte talent in India: Plivo, some teams at Razorpay for marketing/landing pages, Postman for docs and marketing, and a wave of AI/dev-tool startups. Specialised areas (compiler internals, contributing to SvelteKit) pay at the upper end.
Should I learn Svelte if I already know React?
If you build greenfield apps, yes, Svelte's compiler model produces smaller, faster bundles with less ceremony. If you mostly maintain existing React apps and need to be hired widely, React still dominates the Indian job market. A pragmatic 2026 path: stay strong on React for employability, learn Svelte 5 + SvelteKit for personal projects and to stay current with where the industry is heading. Svelte's mental model also makes you a better React developer, once you've seen reactivity without virtual DOM, you appreciate React's trade-offs more clearly.
Svelte 5 with runes vs Svelte 4, should I learn the old syntax?
Learn Svelte 5 with runes from day one. The old `$:` and `export let` syntax still works in Svelte 5 for backwards compatibility, but every new tutorial, library, and job is moving to runes. The official Svelte tutorial (svelte.dev/tutorial) is fully rune-based. You may encounter legacy syntax in older codebases at job interviews, be familiar enough to read it, but write runes for everything new.
How does SvelteKit compare to Next.js?
Both are file-based meta-frameworks with SSR, SSG, API routes, and similar mental models, Next.js Server Actions (2023) borrowed heavily from SvelteKit form actions. Differences: SvelteKit's per-route SSR/CSR/prerender flexibility is cleaner than Next.js's confused App Router vs Pages Router situation. SvelteKit's adapters give you platform freedom, same code runs on Node, Cloudflare, Vercel, Netlify. Next.js has a much bigger ecosystem and more developer mindshare. For a typical Indian SaaS startup in 2026, Next.js is the safer hire but SvelteKit is meaningfully more enjoyable to work in.
What testing frameworks work well with Svelte and SvelteKit?
Vitest is the default unit/component test runner, it's Vite-native, so it understands `.svelte` files via `@sveltejs/vite-plugin-svelte` and runs fast in watch mode. Pair it with `@testing-library/svelte` for component testing with user-event style assertions. For end-to-end tests, Playwright is the recommended choice, SvelteKit's `npm create svelte@latest` scaffolds it by default. The Playwright integration is excellent: tests run against the real preview build, including SSR, form submissions, and progressive enhancement scenarios. For visual regression, Storybook with `@storybook/sveltekit` and Chromatic, or Playwright's built-in `toHaveScreenshot()`. A typical Indian production setup runs: Vitest for component logic, Playwright for critical user journeys (signup, checkout), and `svelte-check` in CI for type errors. Skip Jest, its transforms don't understand Svelte's compiler output well, and Vitest is faster.
How do you debug a Svelte 5 app without something like React DevTools?
Svelte's answer is in the language rather than a browser extension. `$inspect(value)` logs a value and every subsequent change with an `init` or `update` label, and `$inspect(x).with(console.trace)` gives you a stack for each change. `$inspect.trace('label')` as the first line of an effect or derived reports which reactive dependency triggered that specific run, which is the tool for 'why did this re-render'. Both are stripped from production builds automatically. Beyond that: `svelte-check --watch` for type and a11y problems, source maps in devtools to step through compiled output, and the Svelte DevTools browser extension for inspecting the component tree. On the server side, `handleError` in `hooks.server.ts` is the single choke point for logging, so generate a request id there and surface it in the UI so a screenshot maps to a trace.
Should I use SvelteKit remote functions at work in 2026?
Not yet for anything critical. Remote functions (`query`, `form`, `command`, `prerender` in a `.remote.ts` file) are gated behind `kit.experimental.remoteFunctions`, and the async compiler mode they pair with is behind `compilerOptions.experimental.async`. Both have changed shape between minor releases, so an upgrade can break your code with no deprecation window. They are worth knowing for interviews because they show you follow the framework's direction, and worth prototyping on an internal tool. For customer-facing routes, load functions plus form actions remain the stable, documented path, and they already give you SSR, progressive enhancement and type-safe data flow.
Introduction
Svelte is the compiler-first UI framework that has quietly become a serious alternative to React in 2026. Where React, Vue, and Angular ship a runtime that diffs a virtual DOM in the browser, Svelte does the work at build time, your `.svelte` components are compiled into surgical, imperative JavaScript that directly mutates the real DOM. The result is smaller bundles, faster startup, and a simpler mental model. There is no `useState`, no `useEffect` dependency arrays to debug, no `React.memo` to sprinkle around, reactivity is a first-class language feature, not a library convention.
Svelte 5, released in late 2024, was the biggest change in the framework's history. It introduced runes, explicit reactivity primitives like `$state`, `$derived`, and `$effect`, that replaced the implicit `let`-based reactivity of Svelte 3/4. The motivation was twofold: reactivity needed to escape the `<script>` block (so logic could live in `.js` and `.ts` files alongside components), and the implicit model had too many ambiguities. With runes, every reactive value is explicitly marked, mutations to arrays and objects 'just work' via proxies, and the same primitives compose cleanly across components and modules.
SvelteKit is the official meta-framework (think Next.js for Svelte) that handles routing, server-side rendering, API endpoints, and deployment via adapters. It reached 1.0 in December 2022 and is the recommended path for any production Svelte app in 2026. Its killer features: per-route SSR/CSR/prerender flexibility, form actions with progressive enhancement, type-safe load functions, and adapters that deploy the same code to Node, Vercel, Netlify, or Cloudflare Workers.
If you're interviewing for a Svelte role in India today, expect deep questions on the runes API, how the compiler differs from React's runtime, SvelteKit's file-based routing and load functions, form actions, stores, and transition primitives. Adoption in India is smaller than React but growing, Plivo India (developer infrastructure), several Razorpay landing pages, Postman's docs and marketing surfaces, and a wave of AI startups have shipped production Svelte. Salaries are typically ₹6-20 LPA for mid-to-senior roles, somewhat below the React equivalent but climbing. This guide covers 40 of the most-asked questions in 2026, grouped by difficulty, with code examples that show idiomatic Svelte 5.
Ready to practice Svelte interviews?
Don't just read, practice these Svelte questions live with an AI interviewer that asks follow-ups and scores your answers.