Ripple Runtime: Mount & Block Tree
Part 3 of the Ripple internals deep dive series.
Phase 2: Initial Execution - Component Mount
Now that we've seen how the code is compiled, let's trace what happens when the component actually executes. This is where the runtime system comes alive, creating tracked values, building the block tree, and registering dependencies.
Step 2.1: Component Function Call
When we mount the component:
mount(Counter, { target: document.getElementById('root') });
Global Runtime State (Initial):
// These global variables track the current execution context
active_component = null // No component active yet
active_block = null // No block executing
active_reaction = null // No reactive computation running
tracking = false // Dependency tracking disabled
clock = 0 // Global clock for change detection
queued_root_blocks = [] // Empty update queue
old_values = new Map() // Empty map for teardown values
These global variables form the execution context. As blocks execute, these variables change to track what's currently happening. When a block reads a tracked value with _$_.get(), it uses active_reaction to know where to register the dependency.
Step 2.2: Tracked Value Creation
When we execute let count = track(0, ...), we're creating a reactive value. Let's see exactly what happens.
Code:
let count = track(0, undefined, undefined, component_ctx);
Algorithm: track(value, get, set, block)
FUNCTION track(0, undefined, undefined, component_ctx):
// Step 1: Check if already tracked (idempotent)
IF is_tracked_object(0): // false - 0 is a primitive
RETURN value
// Step 2: Validate block exists
IF component_ctx IS NULL: // false
THROW TypeError('track() requires a valid component context')
// Step 3: Check if value is a function (derived value)
IF typeof 0 === 'function': // false
RETURN derived(...)
// Step 4: Create simple tracked value
RETURN tracked(0, component_ctx, undefined, undefined)
END FUNCTION
Algorithm: tracked(value, block, get, set)
FUNCTION tracked(0, component_ctx, undefined, undefined):
RETURN {
__v: 0, // Current value
a: { // Accessors (get/set functions)
get: undefined,
set: undefined
},
b: component_ctx, // Associated block (for scheduling updates)
c: 0, // Clock value (incremented on change)
f: TRACKED // Flags (TRACKED = simple value, not derived)
}
END FUNCTION
Result - count object:
count = {
__v: 0, // The actual value
a: { get: undefined, set: undefined },
b: component_ctx, // Which block to update when this changes
c: 0, // Clock value (starts at 0)
f: TRACKED // Flag: this is a simple tracked value
}
Result - showDouble object:
showDouble = {
__v: true, // The actual value
a: { get: undefined, set: undefined },
b: component_ctx, // Same block association
c: 0, // Clock value
f: TRACKED // Simple tracked value
}
Key Points:
__vstores the actual value (0 or true)b(block) is crucial - when this value changes, we'll schedule an update to this blockc(clock) starts at 0 and increments every time the value changesf(flags) tells us this is a simple tracked value, not a computed one
Tracked Value Structure:

Now let's see how the derived value double is created, which has a different structure because it's computed rather than stored directly.
Step 2.3: Derived Value Creation
Now for the interesting one - double is a derived value. It's computed from count, not stored directly.
Code:
let double = track(() => _$_.get(count) * 2, undefined, undefined, component_ctx);
Notice: We pass a function, not a value. This tells Ripple "compute this value when needed."
Algorithm: track(fn, get, set, block)
FUNCTION track(fn, undefined, undefined, component_ctx):
// fn = () => _$_.get(count) * 2
// Check if it's a function (derived value)
IF typeof fn === 'function': // true
RETURN derived(fn, component_ctx, undefined, undefined)
// Otherwise create simple tracked value
RETURN tracked(fn, component_ctx, undefined, undefined)
END FUNCTION
Algorithm: derived(fn, block, get, set)
FUNCTION derived(fn, component_ctx, undefined, undefined):
RETURN {
__v: UNINITIALIZED, // Not computed yet! (lazy)
a: { get: undefined, set: undefined },
b: component_ctx, // Associated block
blocks: null, // Child blocks (created during computation)
c: 0, // Clock value
co: active_component, // Component context
d: null, // Dependency chain (empty - will be populated)
f: TRACKED | DERIVED, // Flags: both TRACKED and DERIVED
fn: fn // Computation function
}
END FUNCTION
Result - double object:
double = {
__v: UNINITIALIZED, // Not computed yet! (lazy evaluation)
a: { get: undefined, set: undefined },
b: component_ctx,
blocks: null, // Will hold child blocks created during computation
c: 0, // Clock value
co: null, // Component context
d: null, // No dependencies yet (will track count)
f: TRACKED | DERIVED, // Both flags set
fn: () => _$_.get(count) * 2 // The computation function
}
Key Differences from Simple Tracked Values:
| Property | Simple (count) | Derived (double) |
|---|---|---|
__v | Has value immediately (0) | UNINITIALIZED (lazy) |
f | TRACKED | TRACKED | DERIVED |
fn | undefined | Computation function |
d | null (not used) | null (will track dependencies) |
Derived values aren't computed until someone reads them. This lazy evaluation avoids unnecessary computation. When we first access @double, that's when double.fn() runs and double.d gets populated with dependencies.
Derived Value Structure:

With our tracked values created, the next step is to create the root block that will contain all the rendering logic.
Step 2.4: Root Block Creation
Blocks are the fundamental unit of reactive execution in Ripple. Think of them as "reactive functions" - they execute, track dependencies, and re-execute when those dependencies change.
Code:
return _$_.root(() => { /* render function */ }, component_ctx);
Algorithm: root(fn, compat, component_ctx)
FUNCTION root(fn, undefined, component_ctx):
// Create a root block (top-level block for component)
RETURN block(ROOT_BLOCK, fn, { compat: undefined }, component_ctx)
END FUNCTION
Algorithm: block(flags, fn, state, co)
FUNCTION block(ROOT_BLOCK, fn, { compat }, component_ctx):
block = {
co: component_ctx, // Component context
d: null, // Dependency chain (empty initially)
first: null, // First child block
f: ROOT_BLOCK, // Flags (ROOT_BLOCK = top-level)
fn: fn, // Function to execute
last: null, // Last child block
next: null, // Next sibling block
p: null, // Parent block (null for root)
s: { compat }, // State (DOM nodes, etc.)
t: null // Teardown function
}
// Link to parent if one exists (none for root)
IF active_block IS NOT NULL:
push_block(block, active_block)
RETURN block
END FUNCTION
Result - root_block structure:
root_block = {
co: component_ctx, // Which component owns this block
d: null, // Dependency chain (will be populated)
first: null, // Will point to first child block
f: ROOT_BLOCK, // Flag: this is a root block
fn: () => { /* render function */ }, // What to execute
last: null, // Will point to last child block
next: null, // No siblings
p: null, // No parent (it's the root!)
s: { compat: undefined }, // State storage
t: null // Teardown function (set later)
}
Block Structure Explained:
| Property | Purpose | Example |
|---|---|---|
co | Component context | Links block to component |
d | Dependency chain | Tracks which values this block depends on |
first/last | Child blocks | Forms a tree structure |
f | Flags | ROOT_BLOCK, RENDER_BLOCK, BRANCH_BLOCK, etc. |
fn | Execution function | What code to run |
p | Parent block | Links to parent in tree |
s | State | Stores DOM nodes, data, etc. |
t | Teardown | Cleanup function |
Blocks form a tree structure where the root block contains everything, and child blocks handle specific parts like rendering text or conditionals.
Block Tree Structure:

Now that the root block is created, it needs to execute to render the component.
Step 2.5: Root Block Execution
Now the root block executes. This is where the component actually renders!
Code:
run_block(root_block);
Context Before Execution:
active_block = null
active_reaction = null
tracking = false
active_dependency = null
active_component = null
Algorithm: run_block(root_block)
FUNCTION run_block(root_block):
// Step 1: Save current context (for nested execution)
previous_block = active_block // null
previous_reaction = active_reaction // null
previous_tracking = tracking // false
previous_dependency = active_dependency // null
previous_component = active_component // null
TRY:
// Step 2: Set active context
active_block = root_block // Now this block is active
active_reaction = root_block // This is the current reaction
active_component = root_block.co // Set component context
// Step 3: Cleanup (none needed on first run)
destroy_non_branch_children(root_block) // No children yet
run_teardown(root_block) // No teardown yet
// Step 4: Enable tracking? NO - ROOT_BLOCK doesn't track!
tracking = (root_block.f & (ROOT_BLOCK | BRANCH_BLOCK)) === 0
// Since root_block.f has ROOT_BLOCK flag:
// tracking = false // Important: root blocks don't track dependencies
active_dependency = null
// Step 5: Execute the render function!
result = root_block.fn(root_block.s)
// This creates child blocks, renders DOM, etc.
// Step 6: Store teardown function if returned
IF typeof result === 'function':
root_block.t = result // Save for cleanup later
// Mark parent blocks as containing teardown (none for root)
current = root_block
WHILE current IS NOT NULL AND (current.f & CONTAINS_TEARDOWN) === 0:
current.f = current.f | CONTAINS_TEARDOWN
current = current.p
// Step 7: Store dependency chain
root_block.d = active_dependency // null (tracking was false)
FINALLY:
// Step 8: Restore previous context
active_block = previous_block
active_reaction = previous_reaction
tracking = previous_tracking
active_dependency = previous_dependency
active_component = previous_component
END FUNCTION
Context After Execution:
active_block = null // Restored
active_reaction = null // Restored
tracking = false // Restored
active_dependency = null // Restored
active_component = null // Restored
Key Points:
-
Context Save/Restore: This allows nested block execution. When a child block runs, it can save/restore context safely.
-
Root Blocks Don't Track:
tracking = falsemeans root blocks don't register dependencies. They always execute when scheduled. -
Dependency Chain:
root_block.d = nullbecause tracking was disabled. Child blocks will have dependency chains.
Root blocks are the entry point - they always execute when their component updates. Child blocks are the ones that need fine-grained tracking. Now let's see what happens inside the render function as it creates child blocks.
Step 2.6: Template Creation
Inside the render function:
const __template0 = _$_.template('<div><p><!></p><!></div>');
const __fragment = __template0();
Algorithm: template(content, flags)
FUNCTION template('<div><p><!></p><!></div>', flags):
node = undefined // Cached template
RETURN () => {
IF node === undefined:
// Create template element
elem = document.createElement('template')
elem.innerHTML = '<div><p><!></p><!></div>'
node = elem.content // Cache it
// Clone template
clone = node.cloneNode(true)
// Assign start/end nodes
assign_nodes(first_child(clone), clone.lastChild)
RETURN clone
}
END FUNCTION
Result: DOM fragment with placeholder comment nodes (<!>) for dynamic content.
Step 2.7: Count Text Rendering - Dependency Registration
This is where dependency tracking actually happens. When we render the count text, we read @count, which triggers the dependency registration system. This is the critical moment where blocks learn which tracked values they depend on.
Code:
_$_.render(() => {
_$_.set_text(__text0.firstChild, 'Count: ' + _$_.get(count));
});
What render() does:
FUNCTION render(fn, null, 0):
// Creates a RENDER_BLOCK (not ROOT_BLOCK!)
RETURN block(RENDER_BLOCK, fn, null)
END FUNCTION
Created Block:
count_render_block = {
co: component_ctx,
d: null, // Will be populated during execution!
first: null,
f: RENDER_BLOCK, // ← Different flag from ROOT_BLOCK
fn: () => {
_$_.set_text(__text0.firstChild, 'Count: ' + _$_.get(count));
},
last: null,
next: null,
p: root_block, // ← Parent is root block
s: null,
t: null
}
Context Before Execution:
active_block = root_block
active_reaction = root_block
tracking = false // Root block doesn't track
active_dependency = null
Execution: run_block(count_render_block)
FUNCTION run_block(count_render_block):
// Save context
previous_block = active_block // root_block
previous_reaction = active_reaction // root_block
previous_tracking = tracking // false
previous_dependency = active_dependency // null
TRY:
// Set new context
active_block = count_render_block // Now this block is active
active_reaction = count_render_block // This is the reaction
active_component = component_ctx
// Enable tracking! (RENDER_BLOCK is not ROOT_BLOCK)
tracking = (count_render_block.f & (ROOT_BLOCK | BRANCH_BLOCK)) === 0
// RENDER_BLOCK doesn't have ROOT_BLOCK or BRANCH_BLOCK flags
// tracking = true // Now dependencies will be tracked!
active_dependency = null // Will be built during execution
// Execute the function - this is where dependency registration happens
result = count_render_block.fn()
// Inside: _$_.get(count) is called
// Store the dependency chain
count_render_block.d = active_dependency // Now has count dependency!
FINALLY:
// Restore context
active_block = previous_block
active_reaction = previous_reaction
tracking = previous_tracking
active_dependency = previous_dependency
END FUNCTION
Context During Execution:
active_block = count_render_block // Changed!
active_reaction = count_render_block // Changed!
tracking = true // Enabled!
active_dependency = null // Will be built
Inside count_render_block.fn(): _$_.get(count) is called
This is the critical moment! Let's trace it step by step:
Step 1: get(count)
FUNCTION get(count):
// Check if it's a tracked object
IF NOT is_tracked_object(count): // false - count IS tracked
RETURN count
// Check if it's derived
IF (count.f & DERIVED) !== 0: // false - count.f = TRACKED, not DERIVED
RETURN get_derived(count)
// It's a simple tracked value
RETURN get_tracked(count) // ← Go here!
END FUNCTION
Step 2: get_tracked(count)
FUNCTION get_tracked(count):
// Step 1: Get the value
value = count.__v // 0
// Step 2: Register dependency (THIS IS THE KEY!)
IF tracking: // true (enabled by RENDER_BLOCK)
register_dependency(count) // ← This creates the link!
// Step 3: Handle teardown (not applicable here)
IF teardown AND old_values.has(count): // false
value = old_values.get(count)
// Step 4: Apply custom getter (none)
IF count.a.get IS NOT undefined: // false
value = trigger_track_get(count.a.get, value)
RETURN value // 0
END FUNCTION
Step 3: register_dependency(count) - The Dependency Link! 🔗
This is where count_render_block learns it depends on count:
FUNCTION register_dependency(count):
// active_reaction = count_render_block (set by run_block)
// active_dependency = null (initial state)
dependency = active_dependency // null
// Create first dependency
IF dependency IS NULL: // true
dependency = create_dependency(count)
active_dependency = dependency // ← Store in global
RETURN
END FUNCTION
Step 4: create_dependency(count) - Create the Link Node
FUNCTION create_dependency(count):
reaction = active_reaction // count_render_block
existing = reaction.d // null (no dependencies yet)
// Try to recycle (none available)
IF existing IS NOT NULL: // false
// Recycle logic...
RETURN existing
// Create new dependency node
RETURN {
c: count.c, // 0 ← Clock value when registered
t: count, // ← Reference to tracked value
n: null // ← Next dependency (none yet)
}
END FUNCTION
Result - Dependency Chain Created:
// Global state updated
active_dependency = {
c: 0, // count's clock value (0)
t: count, // Reference to count object
n: null // No next dependency
}
// Block's dependency chain stored
count_render_block.d = {
c: 0,
t: count,
n: null
}
Context After Execution:
active_block = root_block // Restored
active_reaction = root_block // Restored
tracking = false // Restored
active_dependency = null // Restored
Final State:
// The block now knows it depends on count!
count_render_block = {
// ...
d: { // Dependency chain!
c: 0, // Clock value when registered
t: count, // Which value it depends on
n: null // No other dependencies
}
}
// DOM updated
// Text node now shows: "Count: 0"
What Just Happened:
- Block executed with
tracking = true _$_.get(count)called → reads value0- Dependency registered →
count_render_block.dnow points tocount - Clock value stored →
c: 0(we'll compare this later to detect changes)
The dependency chain count_render_block.d is a linked list that says: "This block depends on count, and it was registered when count's clock was 0."
Dependency Chain Structure:

Later, when count changes, its clock increments. We can check count.c > dependency.c to see if it changed. This clock comparison is the core of Ripple's efficient change detection.
Now let's see how the conditional block for showDouble works, which will also register dependencies.
Step 2.8: Double Conditional Block
_$_.if(__anchor1, (set_branch) => {
if (_$_.get(showDouble)) {
set_branch((anchor) => { /* render double */ });
}
});
Algorithm: if_block(node, fn)
FUNCTION if_block(__anchor1, fn):
anchor = __anchor1
has_branch = false
condition = UNINITIALIZED
b = null // Branch block
set_branch = (fn, flag = true) => {
has_branch = true
update_branch(flag, fn)
}
update_branch = (new_condition, fn) => {
IF condition === new_condition: // Skip if unchanged
RETURN
// Destroy old branch
IF b !== null:
destroy_block(b)
b = null
// Create new branch if condition is truthy
IF fn !== null:
b = branch(() => fn(anchor))
}
// Render block that evaluates condition
render(() => {
has_branch = false
fn(set_branch) // Calls our function with set_branch
IF NOT has_branch:
update_branch(null, null)
}, null, IF_BLOCK)
END FUNCTION
Execution:
render()creates an IF_BLOCK- Block executes:
fn(set_branch)is called - Inside:
if (_$_.get(showDouble))evaluates
_$_.get(showDouble):
FUNCTION get(showDouble):
RETURN get_tracked(showDouble)
END FUNCTION
FUNCTION get_tracked(showDouble):
value = showDouble.__v // true
// Register dependency
IF tracking: // true
register_dependency(showDouble)
RETURN value // true
END FUNCTION
Dependency registered:
if_block.d = {
c: 0,
t: showDouble,
n: null
}
Since the condition is true, set_branch() is called, which creates a branch block. This branch block will render the double value. Inside the branch block, when we access @double, this triggers the first computation of the derived value.
Branch block execution:
branch_block = {
co: component_ctx,
d: null,
f: BRANCH_BLOCK,
fn: (anchor) => {
const __template1 = _$_.template('<p><!></p>');
const __fragment1 = __template1();
anchor.before(__fragment1);
_$_.render(() => {
_$_.set_text(__fragment1.firstChild, 'Double: ' + _$_.get(double));
});
},
p: if_block,
// ...
}
Inside branch: _$_.get(double)
This is the first access to the derived value, which means it needs to be computed for the first time.
Algorithm: get(double)
FUNCTION get(double):
// double is derived
IF (double.f & DERIVED) !== 0: // true
RETURN get_derived(double)
END FUNCTION
Algorithm: get_derived(double)
FUNCTION get_derived(double):
// Update derived value
update_derived(double)
// Register dependency
IF tracking: // true
register_dependency(double)
// No custom getter
IF double.a.get IS NOT undefined: // false
double.__v = trigger_track_get(double.a.get, double.__v)
RETURN double.__v
END FUNCTION
Algorithm: update_derived(double)
FUNCTION update_derived(double):
value = double.__v // UNINITIALIZED
// Recompute (uninitialized)
IF value === UNINITIALIZED OR is_tracking_dirty(double.d): // true
value = run_derived(double)
// Update if changed
IF value !== double.__v: // true
double.__v = value // 0
double.c = increment_clock() // 1
END FUNCTION
Algorithm: run_derived(double)
FUNCTION run_derived(double):
// Save context
previous_block = active_block // branch_block
previous_reaction = active_reaction // branch_block
previous_tracking = tracking // true
previous_dependency = active_dependency // null
previous_component = active_component // component_ctx
previous_is_mutating_allowed = is_mutating_allowed // true
TRY:
// Set context for computation
active_block = null
active_reaction = double // Derived value is the reaction!
active_component = component_ctx
tracking = true // Enable dependency tracking
active_dependency = null
is_mutating_allowed = false // Prevent mutations
// Destroy old child blocks (none yet)
destroy_computed_children(double)
// Run computation: () => _$_.get(count) * 2
value = double.fn()
// Inside fn(): _$_.get(count)
// This registers count as a dependency of double!
// No custom getter
IF double.a.get IS NOT undefined: // false
value = trigger_track_get(double.a.get, value)
RETURN value // 0
END FUNCTION
Inside double.fn(): _$_.get(count)
FUNCTION get_tracked(count):
value = count.__v // 0
// Register dependency (tracking === true, active_reaction === double)
IF tracking: // true
register_dependency(count) // Registers to double, not branch_block!
RETURN value // 0
END FUNCTION
Dependency registered to double:
double.d = {
c: 0, // count's clock value
t: count,
n: null
}
After computation:
double = {
__v: 0, // Computed value
c: 1, // Clock incremented
d: {
c: 0,
t: count,
n: null
}
// ...
}
Back to get_derived(double):
// Register double as dependency of branch_block
branch_block.d = {
c: 1, // double's clock value
t: double,
n: null
}
Final state after mount:
count = { __v: 0, c: 0, d: null, ... }
showDouble = { __v: true, c: 0, d: null, ... }
double = { __v: 0, c: 1, d: { c: 0, t: count, n: null }, ... }
count_render_block.d = { c: 0, t: count, n: null }
if_block.d = { c: 0, t: showDouble, n: null }
branch_block.d = { c: 1, t: double, n: null }
double.d = { c: 0, t: count, n: null }
Complete Dependency Graph After Mount:
