Skip to content

Is Svelte Signal blocking? #17861

@fuji-184

Description

@fuji-184

Describe the problem

As an application grows in complexity, the number of signals and reactive effects increases significantly. For instance, consider 100 signals connected to 100 effects. If a single signal triggers all of them simultaneously while a user is performing an action, the user experiences lag. This happens because the user's interaction is blocked by the heavy reactive workload because Javascript is single threaded

If this is the case, I propose a priority system : reactivity updates should yield to user actions. Once the user interaction is handled, the system can then resume the paused reactive tasks

Consider this example : when I click the update button, the reactivity for the scroll status experiences a significant delay

<script>
  import { onMount } from 'svelte';

  let signals = $state(Array.from({ length: 100 }, () => 0));
  let scrollY = $state(0);
  let status = $state('Idle');

  $effect(() => {
    signals.forEach((sig, i) => {
      const start = performance.now();
      while (performance.now() - start < 10) {}
    });
    status = 'Done!';
  });

  function triggerUpdate() {
    status = 'Processing (Check if Scroll Freezes)...';
    for (let i = 0; i < signals.length; i++) {
      signals[i] += 1;
    }
  }

  onMount(() => {
    const handleScroll = () => { scrollY = window.scrollY; };
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  });
</script>

<div class="controls">
  <h3>Svelte 5 Runes Test</h3>
  <p>Status: <strong>{status}</strong></p>
  <button onclick={triggerUpdate}>Update 100 States (Blocking?)</button>
  <div class="scroll-val">Scroll Position: {scrollY}</div>
</div>

<div class="grid">
  {#each signals as sig}
    <div class="cell" style="background: hsl({sig * 20 % 360}, 70%, 80%)">
      {sig}
    </div>
  {/each}
</div>

<style>
  :global(body) { height: 300vh; font-family: sans-serif; }
  .controls { position: fixed; top: 10px; left: 10px; background: white; padding: 1rem; border: 2px solid #ff3e00; z-index: 10; }
  .grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 5px; margin-top: 180px; }
  .cell { width: 50px; height: 50px; display: flex; center; border: 1px solid #ccc; justify-content: center; align-items: center; }
  .scroll-val { margin-top: 10px; font-weight: bold; }
</style>

Describe the proposed solution

The following is an illustrative code example. It is not the actual implementation of the proposed system, but rather a demonstration of how the system would be used. Regarding the implementation, let's discuss a solution that ensures minimal overhead. This demonstrates how a big reactivity job does not block user actions. The scroll position reactivity continues to update instantly, without needing to wait for the heavy reactivity job to finish

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Concurrent Signal Test</title>
    <style>
        body { font-family: sans-serif; height: 300vh; background: #f0f0f0; padding: 20px; }
        .controls { position: fixed; top: 20px; left: 20px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); z-index: 100; }
        .grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 5px; margin-top: 150px; }
        .cell { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; background: #fff; border: 1px solid #ddd; font-size: 10px; }
        .status { margin-top: 10px; font-weight: bold; color: #007bff; }
        .scroll-indicator { position: fixed; right: 20px; top: 50%; transform: translateY(-50%); background: #333; color: white; padding: 10px; border-radius: 5px; }
    </style>
</head>
<body>

<div class="controls">
    <h3>Concurrent Reactivity</h3>
    <p>Klik tombol, lalu <b>langsung scroll</b> ke bawah!</p>
    <button id="trigger">Update 100 Signals</button>
    <div id="status" class="status">Idle</div>
</div>

<div class="scroll-indicator" id="scroll-val">Scroll: 0</div>
<div class="grid" id="grid"></div>

<script>
const queue = [];
const channel = new MessageChannel();
let deadline = 0;
const yieldInterval = 5;

channel.port1.onmessage = () => {
    const currentTime = performance.now();
    deadline = currentTime + yieldInterval;

    while (queue.length > 0 && performance.now() < deadline) {
        const effect = queue.shift();
        effect.run();
    }

    if (queue.length > 0) {
        document.getElementById('status').innerText = `Processing: ${queue.length} left...`;
        channel.port2.postMessage(null);
    } else {
        document.getElementById('status').innerText = 'Done!';
    }
};

let activeEffect = null;

class Signal {
    constructor(value) {
        this._value = value;
        this.subscribers = new Set();
    }
    get value() {
        if (activeEffect) this.subscribers.add(activeEffect);
        return this._value;
    }
    set value(newValue) {
        if (this._value === newValue) return;
        this._value = newValue;
        for (const effect of this.subscribers) {
            if (!queue.includes(effect)) queue.push(effect);
        }
        channel.port2.postMessage(null);
    }
}

class Effect {
    constructor(fn) {
        this.fn = fn;
        this.run();
    }
    run() {
        activeEffect = this;
        this.fn();
        activeEffect = null;
    }
}

const signals = [];
const grid = document.getElementById('grid');

for (let i = 0; i < 100; i++) {
    const s = new Signal(0);
    signals.push(s);
    
    const cell = document.createElement('div');
    cell.className = 'cell';
    grid.appendChild(cell);

    new Effect(() => {
        const val = s.value;
        const start = performance.now();
        while (performance.now() - start < 10) {} 
        cell.innerText = val;
        cell.style.background = `hsl(${val * 20 % 360}, 70%, 80%)`;
    });
}

document.getElementById('trigger').onclick = () => {
    signals.forEach(s => s.value += 1);
};

window.onscroll = () => {
    document.getElementById('scroll-val').innerText = `Scroll: ${window.scrollY}`;
};
</script>
</body>
</html>

Importance

nice to have

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions