Virtual DOM Engine
Diffyne uses a custom Virtual DOM (VDOM) engine to achieve high performance with minimal DOM updates.
How It Works
1. Initial Render
Component State → Blade Template → Virtual DOM Tree → HTML → BrowserWhen a component first loads:
- PHP renders Blade template with component state
- HTML is parsed into a Virtual DOM tree
- Virtual DOM is converted to actual HTML
- HTML sent to browser
2. Subsequent Updates
State Change → Re-render → New VDOM → Diff → Patches → BrowserWhen state changes:
- Component re-renders to new Virtual DOM
- Diff algorithm compares old VDOM vs new VDOM
- Minimal patches generated
- Patches sent to browser
- Browser applies patches to real DOM
Virtual DOM Structure
A Virtual DOM node looks like:
[
'type' => 'element',
'tag' => 'div',
'attrs' => ['class' => 'container', 'id' => 'app'],
'children' => [
[
'type' => 'text',
'content' => 'Hello World'
]
]
]Diff Algorithm
The diff engine compares two VDOM trees and generates minimal patches.
Text Node Changes
Before:
<div>Count: 0</div>After:
<div>Count: 1</div>Patch:
{
"type": "text",
"node": "#text-42",
"value": "Count: 1"
}Attribute Changes
Before:
<button class="btn">Click</button>After:
<button class="btn active">Click</button>Patch:
{
"type": "attr",
"node": "#btn-1",
"attr": "class",
"value": "btn active"
}Element Addition
Before:
<ul>
<li>Item 1</li>
</ul>After:
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>Patch:
{
"type": "add",
"parent": "#list-1",
"html": "<li>Item 2</li>"
}Element Removal
Before:
<div>
<p>Text</p>
<button>Click</button>
</div>After:
<div>
<p>Text</p>
</div>Patch:
{
"type": "remove",
"node": "#btn-1"
}Patch Types
Diffyne supports these patch types:
| Type | Description | Example |
|---|---|---|
text | Update text content | Change "0" to "1" |
attr | Update attribute | Add/remove class |
add | Add new element | Insert list item |
remove | Remove element | Delete button |
replace | Replace element | Swap div with span |
move | Reorder elements | Drag-drop reorder |
Performance Benefits
Traditional Approach (HTMX/Livewire)
Sends full HTML fragment:
<!-- 423 bytes -->
<div class="p-6 max-w-sm mx-auto bg-white rounded-xl shadow-md">
<div class="text-center">
<h2 class="text-2xl font-bold mb-4">Counter</h2>
<div class="text-6xl font-bold mb-6 text-blue-600">1</div>
<button class="px-6 py-3 bg-green-500">+</button>
</div>
</div>Diffyne Approach
Sends minimal patch:
// 52 bytes
{"type":"text","node":"#count","value":"1"}Result: 88% smaller payload!
Optimization Strategies
1. Keyed Lists
Use keys for list items:
@foreach($items as $item)
<li key="{{ $item['id'] }}">{{ $item['name'] }}</li>
@endforeachThis helps the diff algorithm:
- Identify moved items
- Avoid unnecessary re-renders
- Maintain component state
2. Conditional Rendering
@if($showDetails)
<div>Details...</div>
@endifWhen $showDetails changes:
- True → Adds element (one patch)
- False → Removes element (one patch)
3. Static Regions
Mark static content:
<div>
{{-- Static header (never changes) --}}
<header>
<h1>My App</h1>
</header>
{{-- Dynamic content --}}
<div>{{ $dynamicContent }}</div>
</div>The diff engine skips static regions.
Minified Response Format
Diffyne minifies patch responses:
{
"s": true,
"c": {
"i": "comp-1",
"p": [
{"t": "text", "n": "#count", "v": "1"}
],
"st": {"count": 1}
}
}Key mapping:
s→ successc→ componenti→ idp→ patchest→ typen→ nodev→ valuest→ state
Benchmarks
Typical payload sizes:
| Operation | HTML Size | Diffyne Patch | Savings |
|---|---|---|---|
| Counter increment | 423 bytes | 52 bytes | 88% |
| Todo add | 1.2 KB | 156 bytes | 87% |
| Text update | 856 bytes | 89 bytes | 90% |
| List reorder | 2.4 KB | 234 bytes | 90% |
Implementation Details
Node Identification
Each DOM node gets a unique ID:
<div data-diffyne-id="comp-1-div-0">
<p data-diffyne-id="comp-1-p-1">Text</p>
</div>This allows precise targeting in patches.
Patch Application
Browser applies patches in order:
function applyPatch(patch) {
switch(patch.type) {
case 'text':
document.getElementById(patch.node).textContent = patch.value;
break;
case 'attr':
document.getElementById(patch.node)
.setAttribute(patch.attr, patch.value);
break;
case 'add':
document.getElementById(patch.parent)
.insertAdjacentHTML('beforeend', patch.html);
break;
// ... other patch types
}
}State Hydration
After patches applied, component state is updated:
component.state = response.c.st;This keeps client and server in sync.
Comparison with Other Frameworks
vs. Livewire
Livewire:
- Sends full HTML fragments
- Uses morphing algorithm
- ~1-5 KB per update
Diffyne:
- Sends minimal patches
- Uses Virtual DOM diff
- ~50-200 bytes per update
vs. Alpine.js
Alpine.js:
- Client-side only
- No server state management
- Fast but limited
Diffyne:
- Server-driven
- Full Laravel integration
- Fast with server-side logic
vs. Inertia.js
Inertia.js:
- SPA-like experience
- Vue/React required
- Larger payload
Diffyne:
- Traditional server rendering
- No frontend framework needed
- Minimal payload
Future Optimizations
Planned improvements:
- Binary patches - Use binary format instead of JSON
- Patch batching - Combine multiple patches
- Delta compression - Compress patch data
- Streaming - Stream patches as they're generated
- Partial hydration - Only hydrate visible components
Next Steps
- Lifecycle Hooks - Component lifecycle
- Component State - State management
- Performance - Optimization tips
