perf: reuse a stationary reading mask during scrolling

Keep native document geometry while translating content behind one viewport fade. Preserve navigation, focus, media preloading, and Static cleanup without changing 3D quality or render resolution.

Validated assets, types, production builds, prerendered artifacts and transfer budgets; compared 64 layouts and checked 87 live interactions plus uncached media and repeated cleanup. High improved from 71.7 to 101.4 FPS at DPR 1 and 47.8 to 62.2 FPS in the full-size DPR 1.5 window, with occasional sub-60 intervals still remaining.
This commit is contained in:
2026-09-05 17:42:19 +03:00
parent d48e9017cc
commit 15bcdf7cec
5 changed files with 324 additions and 185 deletions

View File

@@ -18,6 +18,7 @@ export function useLazyMedia(
const explicitRequested = shallowRef(false);
const shouldAttach = computed(() => explicitRequested.value || (nearby.value && unref(allowAutomatic)));
let observer: IntersectionObserver | undefined;
let stageObserver: MutationObserver | undefined;
const requestExplicit = (): void => {
explicitRequested.value = true;
@@ -28,15 +29,35 @@ export function useLazyMedia(
onMounted(() => {
if (typeof IntersectionObserver !== 'function' || target.value === null) return;
observer = new IntersectionObserver((entries) => {
const inActivationRange = entries.some((entry) => entry.isIntersecting);
nearby.value = inActivationRange;
if (!inActivationRange) clearExplicit();
}, { rootMargin: MEDIA_ACTIVATION_MARGIN, threshold: 0 });
observer.observe(target.value);
const element = target.value;
const viewport = element.closest('.reading-viewport');
const stage = viewport?.parentElement;
let currentRoot: Element | null | undefined;
const observe = () => {
// Extend the stationary viewport's bounds before clipping, so its mask
// does not remove the preloading margin. Static mode uses the window.
const root = stage?.dataset.readingStage === 'live' ? viewport : null;
if (observer && root === currentRoot) return;
currentRoot = root;
observer?.disconnect();
observer = new IntersectionObserver((entries) => {
const inActivationRange = entries.some((entry) => entry.isIntersecting);
nearby.value = inActivationRange;
if (!inActivationRange) clearExplicit();
}, { root, rootMargin: MEDIA_ACTIVATION_MARGIN, threshold: 0 });
observer.observe(element);
};
observe();
if (stage) {
stageObserver = new MutationObserver(observe);
stageObserver.observe(stage, { attributes: true, attributeFilter: ['data-reading-stage'] });
}
});
onUnmounted(() => observer?.disconnect());
onUnmounted(() => {
observer?.disconnect();
stageObserver?.disconnect();
});
return {
nearby: readonly(nearby),