File size: 4,322 Bytes
dbb1bf9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | type IdleCallback = () => void;
type RequestIdleCallback = (cb: IdleCallback, opts?: { timeout: number }) => number;
/**
* Run non-critical work after first paint and browser load, then yield to idle time when available.
*/
export function scheduleAfterFirstPaint(task: () => void, timeoutMs = 3000): void {
if (typeof window === 'undefined' || typeof document === 'undefined') return;
let ran = false;
const runOnce = (): void => {
if (ran) return;
ran = true;
task();
};
const scheduleIdle = (): void => {
const ric = (window as unknown as { requestIdleCallback?: RequestIdleCallback }).requestIdleCallback;
if (typeof ric === 'function') {
ric(runOnce, { timeout: timeoutMs });
return;
}
setTimeout(runOnce, 0);
};
const afterPaint = (): void => {
if (typeof window.requestAnimationFrame === 'function') {
window.requestAnimationFrame(() => window.requestAnimationFrame(scheduleIdle));
return;
}
scheduleIdle();
};
if (document.readyState === 'complete') {
afterPaint();
} else {
window.addEventListener('load', afterPaint, { once: true });
}
}
/**
* Yield to the main thread (ends the current task) so a long render can be split into
* sub-50ms tasks — lowers TBT, which deferral alone does not (#4442), and shortens
* INP processing time by letting a higher-priority paint run between chunks (#4537).
*
* Prefers the native `scheduler.yield()` where available: it ends the current task
* AND resumes the continuation ahead of a fresh `setTimeout(0)`, which is clamped and
* scheduled behind other timer work — strictly better for INP. Falls back to
* `setTimeout(resolve, 0)` on browsers without the Scheduler API.
*/
export function yieldToMain(): Promise<void> {
const scheduler = (globalThis as unknown as {
scheduler?: { yield?: () => Promise<void> };
}).scheduler;
if (typeof scheduler?.yield === 'function') {
return scheduler.yield();
}
return new Promise((resolve) => { setTimeout(resolve, 0); });
}
/**
* Whether the browser currently has queued input (a click/keypress) waiting to
* be dispatched. Recurring, animation-driven work on the input path calls this
* and skips its tick when input is pending, so the interaction handler can run
* sooner (#5042).
*
* Feature-detected: `navigator.scheduling.isInputPending` is Chromium-only,
* which matches INP being a Chromium-measured metric. Returns `false` wherever
* the API is absent (Firefox/Safari/node), so callers degrade to today's
* behavior with no functional change.
*
* Called with no arguments, so it reports DISCRETE input (clicks/keys) only —
* `includeContinuous` defaults to `false`. That is the correct scope for the
* click/keypress control targets; passing `{ includeContinuous: true }` would
* also report pointermove/wheel/drag and stall callers during any hover/pan.
*/
export function isInputPending(): boolean {
const scheduling = (globalThis as unknown as {
navigator?: { scheduling?: { isInputPending?: () => boolean } };
}).navigator?.scheduling;
if (typeof scheduling?.isInputPending !== 'function') return false;
return scheduling.isInputPending();
}
/**
* Adapt `yieldToMain()` to a fire-once scheduler with a cancel handle:
* `scheduleYield(run)` runs `run` after the yield resolves and returns a cancel
* that prevents it. `DeferredHeavyCommit` (#4558) depends on this shape to
* coalesce a burst of stages into one flush and to drop the flush on teardown;
* the `yieldToMain` promise can't be cleared, so an abort flag preserves those
* semantics. Prefer this over `setTimeout(0)`: `scheduler.yield()` resumes ahead
* of clamped timer work (but still behind queued input), lowering commit latency
* without newly blocking the input path (#5042 U4).
*/
export function scheduleYield(run: () => void): () => void {
let aborted = false;
void yieldToMain().then(() => {
if (aborted) return;
try {
run();
} catch (err) {
// Surface a throw from the deferred flush on the global error channel
// (window.onerror -> Sentry) exactly as the replaced setTimeout(fn, 0)
// did, rather than leaving it as a floating promise rejection.
setTimeout(() => { throw err; });
}
});
return () => { aborted = true; };
}
|