Skip to content

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 → Browser

When a component first loads:

  1. PHP renders Blade template with component state
  2. HTML is parsed into a Virtual DOM tree
  3. Virtual DOM is converted to actual HTML
  4. HTML sent to browser

2. Subsequent Updates ​

State Change → Re-render → New VDOM → Diff → Patches → Browser

When state changes:

  1. Component re-renders to new Virtual DOM
  2. Diff algorithm compares old VDOM vs new VDOM
  3. Minimal patches generated
  4. Patches sent to browser
  5. Browser applies patches to real DOM

Virtual DOM Structure ​

A Virtual DOM node looks like:

php
[
    '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:

html
<div>Count: 0</div>

After:

html
<div>Count: 1</div>

Patch:

json
{
    "type": "text",
    "node": "#text-42",
    "value": "Count: 1"
}

Attribute Changes ​

Before:

html
<button class="btn">Click</button>

After:

html
<button class="btn active">Click</button>

Patch:

json
{
    "type": "attr",
    "node": "#btn-1",
    "attr": "class",
    "value": "btn active"
}

Element Addition ​

Before:

html
<ul>
    <li>Item 1</li>
</ul>

After:

html
<ul>
    <li>Item 1</li>
    <li>Item 2</li>
</ul>

Patch:

json
{
    "type": "add",
    "parent": "#list-1",
    "html": "<li>Item 2</li>"
}

Element Removal ​

Before:

html
<div>
    <p>Text</p>
    <button>Click</button>
</div>

After:

html
<div>
    <p>Text</p>
</div>

Patch:

json
{
    "type": "remove",
    "node": "#btn-1"
}

Patch Types ​

Diffyne supports these patch types:

TypeDescriptionExample
textUpdate text contentChange "0" to "1"
attrUpdate attributeAdd/remove class
addAdd new elementInsert list item
removeRemove elementDelete button
replaceReplace elementSwap div with span
moveReorder elementsDrag-drop reorder

Performance Benefits ​

Traditional Approach (HTMX/Livewire) ​

Sends full HTML fragment:

html
<!-- 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:

json
// 52 bytes
{"type":"text","node":"#count","value":"1"}

Result: 88% smaller payload!

Optimization Strategies ​

1. Keyed Lists ​

Use keys for list items:

blade
@foreach($items as $item)
    <li key="{{ $item['id'] }}">{{ $item['name'] }}</li>
@endforeach

This helps the diff algorithm:

  • Identify moved items
  • Avoid unnecessary re-renders
  • Maintain component state

2. Conditional Rendering ​

blade
@if($showDetails)
    <div>Details...</div>
@endif

When $showDetails changes:

  • True → Adds element (one patch)
  • False → Removes element (one patch)

3. Static Regions ​

Mark static content:

blade
<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:

json
{
    "s": true,
    "c": {
        "i": "comp-1",
        "p": [
            {"t": "text", "n": "#count", "v": "1"}
        ],
        "st": {"count": 1}
    }
}

Key mapping:

  • s → success
  • c → component
  • i → id
  • p → patches
  • t → type
  • n → node
  • v → value
  • st → state

Benchmarks ​

Typical payload sizes:

OperationHTML SizeDiffyne PatchSavings
Counter increment423 bytes52 bytes88%
Todo add1.2 KB156 bytes87%
Text update856 bytes89 bytes90%
List reorder2.4 KB234 bytes90%

Implementation Details ​

Node Identification ​

Each DOM node gets a unique ID:

html
<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:

javascript
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:

javascript
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:

  1. Binary patches - Use binary format instead of JSON
  2. Patch batching - Combine multiple patches
  3. Delta compression - Compress patch data
  4. Streaming - Stream patches as they're generated
  5. Partial hydration - Only hydrate visible components

Next Steps ​

Released under the MIT License.