-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Description
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