Skip to content

Commit 62f78d7

Browse files
committed
fix: only yield when DOM is in a consistent state
Yield to the browser only when no DOM-mutating work (destroy, create, render_first, render, render_priority) is pending, so event handlers that fire during the yield never see a partially-rendered tree. Also lower yield deadline from 50ms to 16ms (~60fps) and gate start_now() to non-wasm/test targets where it is actually used.
1 parent 0157b6f commit 62f78d7

1 file changed

Lines changed: 36 additions & 8 deletions

File tree

packages/yew/src/scheduler.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,12 @@ mod feat_hydration {
211211
pub(crate) use feat_hydration::*;
212212

213213
/// Execute any pending [Runnable]s
214+
#[cfg(any(
215+
not(target_arch = "wasm32"),
216+
target_os = "wasi",
217+
feature = "not_browser_env",
218+
test
219+
))]
214220
pub(crate) fn start_now() {
215221
#[tracing::instrument(level = tracing::Level::DEBUG)]
216222
fn scheduler_loop() {
@@ -270,7 +276,7 @@ mod arch {
270276
check_scheduled()
271277
}
272278

273-
const YIELD_DEADLINE_MS: f64 = 50.0;
279+
const YIELD_DEADLINE_MS: f64 = 16.0;
274280

275281
#[wasm_bindgen]
276282
extern "C" {
@@ -279,7 +285,7 @@ mod arch {
279285
}
280286

281287
fn run_scheduler(mut queue: Vec<super::QueueEntry>) {
282-
let mut deadline = js_sys::Date::now() + YIELD_DEADLINE_MS;
288+
let deadline = js_sys::Date::now() + YIELD_DEADLINE_MS;
283289

284290
loop {
285291
super::with(|s| s.fill_queue(&mut queue));
@@ -289,20 +295,27 @@ mod arch {
289295
for r in queue.drain(..) {
290296
r.task.run();
291297
}
292-
let now = js_sys::Date::now();
293-
if now >= deadline {
294-
let cb = Closure::once_into_js(move || run_scheduler(queue));
295-
set_timeout(cb.unchecked_ref(), 0);
296-
return;
298+
if js_sys::Date::now() >= deadline {
299+
// Only yield when no DOM-mutating work is pending, so event
300+
// handlers that fire during the yield see a consistent DOM.
301+
let can_yield = super::with(|s| s.can_yield());
302+
if can_yield {
303+
let cb = Closure::once_into_js(move || run_scheduler(queue));
304+
set_timeout(cb.unchecked_ref(), 0);
305+
return;
306+
}
297307
}
298308
}
299309

300310
set_scheduled(false);
311+
#[cfg(any(test, feature = "test"))]
312+
super::flush_wakers::wake_all();
301313
}
302314

303315
/// We delay the start of the scheduler to the end of the micro task queue.
304316
/// So any messages that needs to be queued can be queued.
305-
/// Once running, we yield to the browser every ~50ms to avoid long tasks.
317+
/// Once running, we yield to the browser every ~16ms, but only at points
318+
/// where the DOM is in a consistent state (no pending renders/destroys).
306319
pub(crate) fn start() {
307320
if check_scheduled() {
308321
return;
@@ -378,6 +391,21 @@ pub async fn flush() {
378391
}
379392

380393
impl Scheduler {
394+
/// Returns true when no DOM-mutating work is pending, meaning it's safe to
395+
/// yield to the browser without leaving the DOM in an inconsistent state.
396+
#[cfg(all(
397+
target_arch = "wasm32",
398+
not(target_os = "wasi"),
399+
not(feature = "not_browser_env")
400+
))]
401+
fn can_yield(&self) -> bool {
402+
self.destroy.inner.is_empty()
403+
&& self.create.inner.is_empty()
404+
&& self.render_first.inner.is_empty()
405+
&& self.render.inner.is_empty()
406+
&& self.render_priority.inner.is_empty()
407+
}
408+
381409
/// Fill vector with tasks to be executed according to Runnable type execution priority
382410
///
383411
/// This method is optimized for typical usage, where possible, but does not break on

0 commit comments

Comments
 (0)