svelte
svelte copied to clipboard
Erratic spring behaviour
Describe the bug
If the time between frames is high (i.e. more than a small fraction of a second), it can cause springs to rapidly jump to unexpectedly large or small values. This creates a poor user experience, and can cause error conditions (such as when it wasn't anticipated that an overdamped spring could overshoot the target.)
The time between frames can increase for three main reasons:
- The computer is under heavy load, and unable to process ticks in a timely fashion
- The svelte app is itself running heavy calculations or a garbage collection cycle is occurring which temporarily blocks the thread
- The user switches tab while a spring is running, and then returns to the tab at some point
I've experienced all 3 cases, and this effects all uses of springs to varying extents.
(I will submit a pull request separately that is intended to fix this)
Reproduction
https://svelte.dev/repl/8a52664675434394899f3e58d8f542a3?version=3.44.2
Logs
No response
System Info
System:
OS: Windows 10 10.0.19042
CPU: (4) x64 Intel(R) Core(TM) i5-4670K CPU @ 3.40GHz
Memory: 6.92 GB / 15.93 GB
Binaries:
Node: 16.0.0 - C:\Program Files\nodejs\node.EXE
Yarn: 1.13.0 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
npm: 7.10.0 - C:\Program Files\nodejs\npm.CMD
Browsers:
Edge: Spartan (44.19041.1266.0), Chromium (96.0.1054.53)
Internet Explorer: 11.0.19041.1202
npmPackages:
rollup: ^2.3.4 => 2.61.0
svelte: C:/Projects/svelte => 3.44.2
Severity
annoyance
Is there any workaround to this?
The bug is caused by too long deltaTime between frames of the requestAnimationFrame, the calculation of the spring physics is not intended for such a long “prediction”. Since requestAnimationFrame does not call a callback while the tab with the page is invisible deltaTime becomes too big. Limiting deltaTime to a maximum of 1/24 of a second will help here, or taking into account the time that the tab was closed using the visibility API, for example
109 line of svelte/packages/src/motion/spring.js
dt: ((now - last_time) * 60) / 1000
could be changed to
dt: ((Math.min(now - last_time, 1000 / 24)) * 60) / 1000
Is there any workaround to this?
Recently, I wrote a similar function to svelte/spring. I needed a faster version of the spring, so I removed support for objects and arrays, and also fixed a bug with excessively large delta time.
import { writable } from 'svelte/store';
export function spring(value: number, { stiffness = 0.15, damping = 0.8, precision = 0.01 }) {
const store = writable(value);
let lastTime: number;
let lastValue = value;
let currentValue = value;
let targetValue = value;
let running = false;
function set(newValue: number) {
targetValue = newValue;
if (!running) {
running = true;
lastTime = performance.now();
requestAnimationFrame(loop);
}
}
function loop() {
const currentTime = performance.now();
const deltaTime = Math.min(currentTime - lastTime, 42) * 0.06;
const delta = targetValue - currentValue;
const velocity = (currentValue - lastValue) / deltaTime;
const spring = stiffness * delta;
const damper = damping * velocity;
const acceleration = spring - damper;
const d = (velocity + acceleration) * deltaTime;
lastValue = currentValue;
if (Math.abs(d) < precision && Math.abs(delta) < precision) {
store.set((currentValue = targetValue));
running = false;
} else {
store.set((currentValue = currentValue + d));
lastTime = currentTime;
requestAnimationFrame(loop);
}
}
return {
set,
subscribe: store.subscribe
};
}
Currently, the function does not support SSR. To add support, you need to replace requestAnimationFrame with raf as follows:
const is_client = typeof window !== 'undefined';
function noop() {}
const raf = is_client ? (cb) => requestAnimationFrame(cb) : noop;
I also ran into this. Anyone found a way to solve it while keeping svelte/motion's spring function?