The Svelte Mental Model
A compiler reads your component, works out which parts of the markup depend on which values, and emits code that updates exactly those parts.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What can a build step know about my UI that a runtime cannot, and what does it do with that knowledge?
Someone drags a slider. They expect one number and one bar to move, on a device that is not new, without the page doing anything else.
Svelte is a framework with less syntax. The compiler just strips the boilerplate away; underneath it must be doing what the others do.
The compiler is not removing boilerplate; it is doing analysis. It reads which template expressions reference which values and emits an update function that touches only the corresponding nodes — work the other models perform at runtime, on the user's device, on every change.
- The compiler is not removing boilerplate; it is doing analysis. It reads which template expressions reference which values and emits an update function that touches only the corresponding nodes — work the other models perform at runtime, on the user's device, on every change.
- That analysis has a boundary: the component. What crosses a module boundary, arrives through a dynamic property lookup, or is mutated by code the compiler cannot see is beyond what static analysis can prove.
- "Less syntax" hides the real trade: semantics now live in a build tool. Reading the source no longer tells you the whole story, and understanding the output is a debugging skill you did not need before.
- A smaller runtime does not mean a smaller bundle at every size. Generated per-component code grows with component count, so the crossover with a fixed-runtime framework depends on how many components you have (Bundle Analysis).
- Compiled output is what runs, so what you step through in a debugger is generated code. Source maps make this workable; they do not make it invisible (Source Maps).
What is actually happening
In the browser, not in the framework.
- Compile time is analysis time. The component is parsed into a template tree plus a script. The compiler determines, for each expression in the markup, which declared values it reads.
- That produces a dependency graph from values to the specific text nodes, attributes and blocks that display them — the same graph a runtime tracker would build, established before the browser has seen a byte.
- Updates are emitted as code, not decided at runtime. The output is roughly a create function that builds the nodes once, and an update function containing "if this value changed, write it here" for each binding.
- Assignment is the update signal. The compiler instruments the places where a tracked value is assigned, so an ordinary-looking assignment is what schedules the update. Only assignments the compiler can see participate.
- Derived values and effects are declared, so their dependencies are also known statically and their re-evaluation is emitted rather than discovered.
- There is no runtime tree comparison for a component's own markup. The generated code writes to nodes it holds references to. Keyed list blocks are the exception, and they need exactly the same identity discipline as everywhere else (Reconciliation and Keys).
What this makes the browser do
And which of it is avoidable.
- Parsing and compiling the generated JavaScript. It is smaller than a general runtime plus templates, and it is not zero (The Real Cost of JavaScript).
- Creating nodes once per component instance, then running small update functions that compare a value with its previous value and write to a node reference.
- No per-update construction of a description tree and no comparison of one, which is the concrete saving. It is a saving in JavaScript, not in DOM cost.
- DOM mutations at the ordinary cost, followed by the ordinary style, layout and paint invalidation (The Cost of a Change).
- Avoidable work: a keyed block over a large list still moves and re-creates nodes, and a large list is a large list in every framework (List Virtualization).
What the compiler emits
The most useful thing you can do with a compiled framework is look at its output once. The shape below is deliberately schematic — it is not any framework's real emitted code — but it is the right shape, and holding it in mind explains the model's strengths and its boundary at the same time.
Two things stand out. First, there is no description tree and no comparison of one: the update function holds a direct reference to the node it writes to. Second, the "if this changed" check comes from analysis, which is exactly why a value the analysis never saw produces no check at all.
1// Source (conceptually):2// let name = 'Ada'3// let count = 04// <h1>Hello {name}</h1>5// <p>Clicked {count} times</p>6 7// Emitted (schematic, not real output):8function create(target) {9 const h1 = document.createElement('h1')10 const p = document.createElement('p')11 h1.textContent = 'Hello ' + name12 p.textContent = 'Clicked ' + count + ' times'13 target.append(h1, p)14 return { h1, p }15}16 17// The compiler proved: h1 depends on `name`, p depends on `count`.18// So the update path is a direct write, with no tree to compare.19function update(nodes, dirty) {20 if (dirty.name) nodes.h1.textContent = 'Hello ' + name21 if (dirty.count) nodes.p.textContent = 'Clicked ' + count + ' times'22}The whole model is in the two if statements. They exist because a build step read the markup and the script together — and a value the build step could not see gets no if statement, which is the failure mode rather than an error.
Build time, and what it can and cannot know
Compile-time reactivity is dependency analysis: reading source, building a graph from declarations to uses, and emitting code informed by it. That is a compilers problem, and it inherits the classic limit of static analysis — anything decided at runtime is beyond it.
That limit is not a defect; it is the definition. A runtime tracker knows what was actually read because it watched it happen. A compiler knows what will be read because it proved it. The first is complete and costs you every update; the second is free at runtime and stops at the edge of what it can see.
- 1Parse
The component becomes a markup tree plus a script, as two related structures rather than a string.
fails by Nothing much — but it is why templates are analysable and a hand-built string of HTML is not.
- 2Analyse
For each markup expression, determine which declared values it reads; for each assignment, determine which bindings it affects.
fails by Indirection the analysis cannot follow: a value reached through a computed property name, or mutated by imported code.
- 3Plan
Build the graph from values to the nodes, attributes and blocks that display them.
fails by Over-broad dependencies when an expression reads a whole object, which widens the emitted check.
- 4Emit
Generate a create function for the initial nodes and an update function with one guarded write per binding.
fails by Bundle growth: the code is per component, so many small components means many small functions.
- 5Run
Instrumented assignments mark values dirty; the scheduler runs update functions before the next rendering opportunity.
fails by A mutation that was never an assignment the compiler instrumented — the silent stale-UI failure.
Everything through "Emit" happens on your machine. Only "Run" happens on the user's.
Where the analysis ends
Every failure that is characteristic of this model is the same failure: a change happened somewhere the compiler could not prove it would happen. The value is right, the markup is right, and no code exists to connect them.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A value is mutated by a helper in another module | The data is correct in the console and stale on screen | The assignment the compiler instrumented never ran; a property was mutated instead | Assign the value in the component, or use the framework's explicit shared-state mechanism (Who Owns This State?). |
| A nested object property is set directly | Some bindings update, others do not | The analysis tracked the container, not that path | Reassign the container, or model the state so the update is an assignment the compiler sees. |
| A derived value reads a variable only in one branch | Correct until the other branch runs, then stale | The emitted dependency set reflects what was statically reachable | Make the dependency unconditional, or split the derivation (Derived State). |
| A keyed block keyed by array index | Reordering swaps row contents and loses input state | Identity was positional, so the block reused the wrong node | Key by a stable id — the same rule as every framework in this module (Reconciliation and Keys). |
| Debugging a generated stack frame | The line numbers do not match the source | What runs is compiler output | Ensure source maps are emitted and served for the build you are debugging (Source Maps). |
How to build it
Most important first.
- Keep reactive values inside the boundary the compiler can see. State shared across modules needs the framework's explicit shared-state mechanism rather than an exported plain variable.
- Assign rather than mutate for anything the markup reads, unless you are using a mechanism explicitly documented to observe mutation. The compiler instruments assignments; a method call on a nested object is not obviously one.
- Declare derived values instead of recomputing them in handlers. A declared derivation gets a statically known dependency set; an imperative recomputation gets nothing (Derived State).
- Read the generated output once, deliberately, for a component you understand. It converts "the compiler does magic" into "the compiler emits an update function", which is the whole mental model.
- Key every list block by identity from the data, exactly as in every other framework here (Reconciliation and Keys).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Targeted updates are kind to assistive technology: writing new text into an existing node preserves the node identity a screen reader is tracking, and preserves focus, selection and scroll (Node Identity Across Updates).
- Block-level conditionals still remove elements from the document, and a removed element that held focus sends focus to the body with no announcement (Focus Management).
- Keyed blocks that re-create rows undo the benefit: a re-created row is a new node, and anything the user had going on inside it is gone (Reconciliation and Keys).
- Compile-time accessibility warnings — missing labels, a click handler on a non-interactive element — are a genuine advantage of having a compiler, and they are a linter, not a guarantee (Accessibility Testing).
- Transitions and animations are first-class here, which makes it easy to ship motion that ignores a user's reduced-motion preference (Contrast, Colour and Motion).
What can go wrong
- State that the compiler never saw assigned — mutated through a helper in another module, or written by a third-party library — so no update code runs and the UI silently lags the data.
- Reactive declarations with a dependency that only appears on some code paths, producing an update that is correct in testing and stale in one branch.
- A large keyed block re-created because the key expression is not stable, which costs node creation and destroys DOM state.
- Bundle growth from many small components, each carrying its own generated create and update code — the mirror image of the small-runtime advantage.
- Debugging against generated code when a source map is missing or wrong, which turns a five-minute bug into an afternoon (Source Maps).
- Updates are batched and flushed before the next rendering opportunity, so reading the DOM immediately after an assignment reads the old DOM.
- Asynchronous data can arrive after the component has been destroyed, or out of order relative to the requests that asked for it (Cancelling a Request Nobody Is Waiting For).
- Interpolated text is escaped in the generated code; the raw-HTML tag is not, and it is the same sink under a different name (Cross-Site Scripting).
- A compiler is a build-time dependency with full access to your build environment. Supply-chain exposure moves earlier in the pipeline rather than disappearing (Third-Party Scripts and the Supply Chain).
- Generated code is still client code: everything in it is readable, and no compile step makes a value in a bundle secret (The Browser Security Model).
- Server rendering executes component code on the server, with the same module-scope-shared-between-requests hazard as any other server-rendered component model (Server-Side Rendering).
- "Compiled means there is no runtime." There is a runtime; it is smaller. Scheduling, lifecycle, keyed blocks and transitions all need shared code.
- "Svelte is faster." Faster at what, against what, on which device? It removes one specific cost — building and comparing a description tree — and changes none of the DOM, layout or paint costs that usually dominate (Measure Before Optimising).
- "The compiler understands my whole app." It understands what it can see: the component, and the values whose assignments it compiled. Anything crossing that boundary needs an explicit mechanism.
- "Less code means less to learn." The compiler's rules are the thing to learn, and they are less discoverable than an API, because they are not visible in the call site.
- "Reactivity is automatic." It is automatic *for assignments the compiler instrumented*. A mutation it did not see is not an update.
Measuring it, and what changes in the field
- Bundle analysis is the honest measurement here, because the trade is runtime size against generated code size. Measure your application, not a hello-world (Bundle Analysis).
- The Performance panel shows what is missing as much as what is there: no description-tree construction, no comparison pass, and the same DOM and layout costs as everyone else (A Mental Model of the Devtools).
- Reading the compiler output for one component is a measurement of a kind — it tells you exactly which bindings will be checked on which change.
- On a slow device the compiled model is at its best, because the work it removed was JavaScript and JavaScript is what scales with CPU.
- On a large application, the per-component code cost accumulates; the small-runtime advantage is largest for small and medium bundles (Code Splitting).
- On a large list, the advantage narrows sharply: node creation and movement dominate, and those are the browser's cost, not the framework's.
- Moving work to compile time genuinely removes it from the user's device, and it puts semantics in a tool. What the compiler cannot see, it cannot make reactive, and that boundary is not visible in the source.
- The generated code is what actually runs, so debugging, profiling and stack traces are all one indirection away from what you wrote.
- A smaller runtime is a real advantage that shrinks as component count grows, which makes framework-size comparisons on tiny demos actively misleading.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- FRAMEWORK-SPECIFICDeriving the dependency graph at build time is Svelte's distinguishing choice. Vue and Solid discover the same relationships at runtime by recording reads; React does not build the graph at all and re-runs components instead; Angular compiles templates ahead of time but still checks bindings against previous values at runtime.
- SPEC-EVOLVINGSvelte's reactivity surface has changed substantially across major versions — from assignment-instrumented top-level variables and reactive labels to explicit rune declarations backed by signals. The durable idea is that a compiler resolves the dependency graph before shipping; the syntax that expresses it should be checked against the version you are on.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Compilers & Programming Languages — this is dependency and dataflow analysis over a source tree, with code generation on the other side. The limits are the classic limits of static analysis, and the "compile-time reactivity" story is not special beyond its target being the DOM.
- — Testing & Reliability Engineering — when the semantics live in a build tool, the build becomes part of what you test, and a component test that bypasses the compiler is testing something else.