sveltejs/svelte
Reactivity engine
Active contributors: Rich Harris, Dominic Gannaway, Simon H
Purpose
The reactivity engine is the fine-grained signals system that powers Svelte 5. It maintains a graph of leaf values (Source), pure computations (Derived), and side effects (Effect), and ensures that any read inside an Effect or Derived registers as a dependency. Writes invalidate dependents transitively and re-run them on the next microtask via the Batch scheduler.
Implementation lives entirely under packages/svelte/src/internal/client/reactivity/ plus a top-level runtime.js that holds module-scoped tracking state.
Node types
graph TD
subgraph "Reactive nodes (extend Value/Reaction)"
Source["Source<br/><code>reactivity/sources.js</code>"]
Derived["Derived<br/><code>reactivity/deriveds.js</code>"]
Effect["Effect<br/><code>reactivity/effects.js</code>"]
end
Write["set(source, value)"] -->|increments<br/>update_version| Source
Source -->|read by| Derived
Source -->|read by| Effect
Derived -->|read by| Effect
Effect -->|enqueues| Batch
Batch["Batch<br/><code>reactivity/batch.js</code>"] -->|flush_effects()| Effect
Effect -. teardown .-> EffectEach node carries a status bitmask defined in packages/svelte/src/internal/client/constants.js:
| Flag | Meaning |
|---|---|
CLEAN |
No changes since last run. |
DIRTY |
A direct dependency wrote a new value. |
MAYBE_DIRTY |
A transitive dependency may have changed; needs re-evaluation. |
DERIVED |
Marker bit: this node is a Derived. |
BRANCH_EFFECT |
This effect owns a block (if, each, await). |
BLOCK_EFFECT |
This effect is a sub-block inside a branch. |
ROOT_EFFECT |
Outermost effect (one per mount/hydrate call). |
EAGER_EFFECT |
Effect that runs synchronously on write rather than via Batch. |
ASYNC |
Effect or derived can suspend on a Promise. |
CONNECTED |
Reaction is currently registered against its sources. |
DESTROYED |
Final state — no further runs. |
STALE_REACTION |
Marked stale after a read of an awaited value rejected. |
WAS_MARKED |
Optimisation flag for batched dirty propagation. |
REACTION_IS_UPDATING |
Re-entrant guard. |
REACTION_RAN |
This reaction ran in the current batch. |
Tracking reads
packages/svelte/src/internal/client/runtime.js keeps three module-scoped variables:
active_reaction— theReactioncurrently running (aDerivedorEffect).active_effect— the nearest enclosingEffect, used to find the owner for new effects/teardown.untracking— when true, reads insideget(...)don't push toactive_reaction.deps.
get(source_or_derived) is the only reactive read path. When called inside a tracked context, it pushes the value onto active_reaction.deps and (if needed) re-evaluates a stale Derived first. untrack(fn) toggles untracking for the duration of fn.
Sources
reactivity/sources.js exports state(initial), set(source, value), and increment(source). A Source holds:
v— the current value.version— incremented on each write;Reaction.depsrecords the version it last saw.wv—update_versionof the active write batch.equals— equality predicate (defaults tosafe_equals, orequalsfor primitives).reactions— the set of dependentReactions.
set short-circuits when equals(old, new) returns true. Otherwise it bumps version, increments the global update_version, and calls mark_reactions_dirty(...) to walk the dependents graph and tag them DIRTY or MAYBE_DIRTY.
$state(plainObject) and $state([...]) are wrapped in a deep-reactive Proxy from packages/svelte/src/internal/client/proxy.js so that property reads/writes create per-property Sources on demand. The Proxy uses a hidden STATE_SYMBOL to prevent double-wrapping and re-uses the same Source for the same property across reads.
Deriveds
reactivity/deriveds.js exports derived(fn) and derived_safe_equal(fn). A Derived extends both Value and Reaction:
- It memoises the result of
fnuntil any of itsdepschanges. - It supports a freeze/unfreeze API (
freeze_derived_effects,unfreeze_derived_effects) used byBatchto defer rebuilding cached values until a flush. recent_async_derivedstracks deriveds that are still resolving Promises (used for await blocks and async expressions).
execute_derived runs the function with the derived as active_reaction, replacing its deps and re-throwing any error.
Effects
reactivity/effects.js exposes the effect zoo:
| Function | Purpose |
|---|---|
effect(fn) |
Plain $effect body. |
user_effect(fn) |
$effect.pre/$effect.root user-facing effects. |
effect_root(fn) |
Outermost owner; created once per mount. |
branch(fn) |
Creates a BRANCH_EFFECT for a template block. |
template_effect(fn) |
Effect whose teardown reuses cached DOM templates. |
block(fn) |
BLOCK_EFFECT inside a branch. |
eager_effect(fn) |
Synchronous effect that runs immediately on dependency write. |
async_effect(fn) |
Effect that can await its dependencies. |
destroy_effect(effect) |
Recursively tears down child effects, runs cleanup callbacks. |
When an effect runs, it sets itself as active_reaction and active_effect, clears its previous deps, and calls fn. The return value (if a function) becomes the teardown callback, executed by execute_effect_teardown.
Batch (scheduler)
reactivity/batch.js is the largest single file in the reactivity engine (~1.3k lines). It owns:
Batch— a class collecting the set of effects to flush.current_batch— the active batch, ornull.flushSync(fn?)— public API: process the queue synchronously.fork()— opt-in "isolated" batches (Svelte 5.x feature).legacy_updates— handling for$:reactive labels in legacy mode.eager_block_effects— list of effects that opted out of microtask deferral.
The high-level flow when a Source is written:
mark_reactions_dirtywalkssource.reactionsand marks eachDIRTYorMAYBE_DIRTY.- Each affected
Effectis scheduled viaschedule_effect. current_batch ??= new Batch()ensures a microtask is queued viaqueueMicrotask(flush).- On flush,
Batchorders effects by their position in the tree (parent before child), runs them, then runs deferred legacy updates.
Async-aware reactions can suspend() a batch, which holds the flush until any pending Promises settle.
Public API
User code rarely touches the engine directly. The public entries are:
| API | Where | Purpose |
|---|---|---|
tick |
packages/svelte/src/internal/client/runtime.js |
Returns Promise<void> resolved after current flush. |
untrack(fn) |
same | Read without subscribing. |
settled() |
same | Promise that resolves once all async effects settle. |
flushSync(fn?) |
packages/svelte/src/internal/client/reactivity/batch.js |
Synchronous flush. |
fork(...) |
same | Run inside an isolated batch. |
Async mode
The async lane is gated on a runtime flag in packages/svelte/src/internal/flags/async.js. The compiler imports svelte/internal/flags/async whenever a component uses async features ({@async}, top-level await, await in markup), which sets async_mode_flag = true. CI runs the runes tests both with the flag on and (via SVELTE_NO_ASYNC=true) with the flag off — the TestNoAsync job in .github/workflows/ci.yml.
Tracing
Dev-only reactivity tracing lives in packages/svelte/src/internal/client/dev/tracing.js. The tracing_mode_flag is set by svelte/internal/flags/tracing when the user opts in. While the flag is on, every effect/derived run captures a stack trace and a list of changed deps; $inspect.trace(label) formats that into a console group.
Key source files
| File | What it owns |
|---|---|
packages/svelte/src/internal/client/runtime.js |
active_reaction, update_effect, is_dirty, get, untrack. |
packages/svelte/src/internal/client/reactivity/sources.js |
Source lifecycle, set, mark_reactions_dirty. |
packages/svelte/src/internal/client/reactivity/deriveds.js |
Derived lifecycle, recompute logic, async-aware deriveds. |
packages/svelte/src/internal/client/reactivity/effects.js |
All effect kinds, teardown semantics. |
packages/svelte/src/internal/client/reactivity/batch.js |
Batch, flushSync, fork, scheduling. |
packages/svelte/src/internal/client/reactivity/async.js |
Async reaction support glue. |
packages/svelte/src/internal/client/reactivity/store.js |
Bridges Svelte 4 stores into the reaction graph. |
packages/svelte/src/internal/client/reactivity/props.js |
$props / $bindable plumbing (deriveds + writebacks). |
packages/svelte/src/internal/client/proxy.js |
Deep-reactive Proxy for $state(object). |
packages/svelte/src/internal/client/constants.js |
Status bitmasks. |
Entry points for modification
If you're touching reactivity, the most common entry points are: tracking changes go in runtime.js#get or update_reaction; new reactive primitives go alongside state in sources.js; new effect flavours go in effects.js; scheduling tweaks go in batch.js. Always verify both the async and non-async paths by running the runes test suite under SVELTE_NO_ASYNC=true as well.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.