Open-Source Wikis

/

Svelte

/

Systems

/

Reactivity engine

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 .-> Effect

Each 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 — the Reaction currently running (a Derived or Effect).
  • active_effect — the nearest enclosing Effect, used to find the owner for new effects/teardown.
  • untracking — when true, reads inside get(...) don't push to active_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.deps records the version it last saw.
  • wvupdate_version of the active write batch.
  • equals — equality predicate (defaults to safe_equals, or equals for primitives).
  • reactions — the set of dependent Reactions.

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 fn until any of its deps changes.
  • It supports a freeze/unfreeze API (freeze_derived_effects, unfreeze_derived_effects) used by Batch to defer rebuilding cached values until a flush.
  • recent_async_deriveds tracks 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, or null.
  • 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:

  1. mark_reactions_dirty walks source.reactions and marks each DIRTY or MAYBE_DIRTY.
  2. Each affected Effect is scheduled via schedule_effect.
  3. current_batch ??= new Batch() ensures a microtask is queued via queueMicrotask(flush).
  4. On flush, Batch orders 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.

Reactivity engine – Svelte wiki | Factory