deer-flow/frontend/src/core/dom/render-activity.ts
DanielWalnut 459dd78707
perf(frontend): bound delivery, bundles, and long-running UI work (#4622)
* docs: design frontend performance remediation

* docs: plan frontend performance remediation

* test(frontend): add route asset performance budgets

* perf(nginx): compress textual responses safely

* perf(frontend): lazy load case study media

* perf(frontend): bound static demo file tracing

* perf(frontend): restore static locale boundaries

* perf(frontend): defer closed workspace panels

* perf(frontend): split editors and deduplicate highlighting

* perf(frontend): index incremental message derivation

* perf(frontend): stabilize paged history cache policy

* perf(frontend): bound streaming markdown renders

* perf(frontend): virtualize message history

* perf(frontend): bound and virtualize chat lists

* perf(frontend): suspend inactive decorative animation

* perf(browser): stream latest frames as binary

* perf(artifacts): stream bounded text previews

* docs: finalize performance runtime boundaries

* style(backend): apply test formatting

* fix(frontend): keep translation functions client-side

* perf(frontend): defer decorative animation bundles

* test(frontend): lock optimized route budgets

* fix: harden frontend performance boundaries

* test(frontend): update i18n provider fixture

* fix(frontend): preserve sidebar pagination position

* style(backend): format artifact range test
2026-08-01 22:19:59 +08:00

104 lines
2.9 KiB
TypeScript

import { useEffect, useState, type RefObject } from "react";
export type RenderActivityListener = (active: boolean) => void;
/**
* Reports whether an element is on screen, in a visible document, and allowed
* to animate by the user's motion preference.
* Consumers can use this to suspend expensive animation loops.
*/
export function observeRenderActivity(
element: Element,
listener: RenderActivityListener,
initialElementVisible = true,
respectReducedMotion = true,
) {
let documentVisible = !document.hidden;
let elementVisible =
typeof IntersectionObserver === "undefined" ? true : initialElementVisible;
const motionPreference =
!respectReducedMotion || typeof matchMedia === "undefined"
? undefined
: matchMedia("(prefers-reduced-motion: reduce)");
let reducedMotion = motionPreference?.matches ?? false;
let lastActive: boolean | undefined;
const notify = () => {
const active = documentVisible && elementVisible && !reducedMotion;
if (active !== lastActive) {
lastActive = active;
listener(active);
}
};
const handleVisibilityChange = () => {
documentVisible = !document.hidden;
notify();
};
document.addEventListener("visibilitychange", handleVisibilityChange);
const handleMotionPreferenceChange = (event: MediaQueryListEvent) => {
reducedMotion = event.matches;
notify();
};
motionPreference?.addEventListener("change", handleMotionPreferenceChange);
const observer =
typeof IntersectionObserver === "undefined"
? undefined
: new IntersectionObserver(([entry]) => {
elementVisible = entry?.isIntersecting ?? false;
notify();
});
observer?.observe(element);
notify();
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
motionPreference?.removeEventListener(
"change",
handleMotionPreferenceChange,
);
observer?.disconnect();
};
}
export function useRenderActivity(
ref: RefObject<Element | null>,
initialActive = true,
respectReducedMotion = true,
) {
// Keep server and first client render aligned; the observer corrects this
// immediately after mount.
const [active, setActive] = useState(initialActive);
useEffect(() => {
const element = ref.current;
if (!element) return;
return observeRenderActivity(
element,
setActive,
initialActive,
respectReducedMotion,
);
}, [initialActive, ref, respectReducedMotion]);
return active;
}
export function usePrefersReducedMotion() {
const [reducedMotion, setReducedMotion] = useState(false);
useEffect(() => {
if (typeof matchMedia === "undefined") return;
const preference = matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReducedMotion(preference.matches);
update();
preference.addEventListener("change", update);
return () => preference.removeEventListener("change", update);
}, []);
return reducedMotion;
}