mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
perf(app): play gif embeds from the viewport not a js decoder (#1877)
This commit is contained in:
1 parent
69e0086144
commit
1f0f7d1222
14 files changed
+550
-1837
No files matched your search
@@ -22,30 +22,24 @@ function thresholdKey(threshold: number | Array<number> | undefined): string {
|
||||
return threshold.join(',');
|
||||
}
|
||||
|
||||
const rootIds = new WeakMap<Element, string>();
|
||||
let rootIdCounter = 0;
|
||||
|
||||
function rootKey(root: Element | null | undefined): string {
|
||||
if (!root) return 'document';
|
||||
const id = (
|
||||
root as Element & {
|
||||
dataset?: DOMStringMap;
|
||||
}
|
||||
).dataset?.sharedIoId;
|
||||
if (id) return `el:${id}`;
|
||||
const generated = `gen-${++rootIdCounter}`;
|
||||
(
|
||||
root as Element & {
|
||||
dataset: DOMStringMap;
|
||||
}
|
||||
).dataset.sharedIoId = generated;
|
||||
return `el:${generated}`;
|
||||
let id = rootIds.get(root);
|
||||
if (id === undefined) {
|
||||
id = `r${++rootIdCounter}`;
|
||||
rootIds.set(root, id);
|
||||
}
|
||||
return `el:${id}`;
|
||||
}
|
||||
|
||||
let rootIdCounter = 0;
|
||||
|
||||
function buildKey(options: ObserverOptions): string {
|
||||
return `${rootKey(options.root ?? null)}|${options.rootMargin ?? '0px'}|${thresholdKey(options.threshold)}`;
|
||||
}
|
||||
|
||||
function getOrCreate(options: ObserverOptions): SharedObserverState {
|
||||
function acquirePooledObserver(options: ObserverOptions): SharedObserverState {
|
||||
const key = buildKey(options);
|
||||
let state = pool.get(key);
|
||||
if (state) return state;
|
||||
@@ -77,7 +71,7 @@ function getOrCreate(options: ObserverOptions): SharedObserverState {
|
||||
}
|
||||
|
||||
export function observeIntersection(element: Element, callback: Callback, options: ObserverOptions = {}): () => void {
|
||||
const state = getOrCreate(options);
|
||||
const state = acquirePooledObserver(options);
|
||||
let handlers = state.callbacks.get(element);
|
||||
if (!handlers) {
|
||||
handlers = new Set();
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {observeIntersection} from '@app/features/platform/utils/SharedIntersectionObserver';
|
||||
|
||||
type PlaybackToggle = (playing: boolean) => void;
|
||||
|
||||
const PLAYBACK_VISIBLE_RATIO = 0.7;
|
||||
const PLAYBACK_MAX_ROOT_HEIGHTS = 8;
|
||||
const MAX_CONCURRENT_PLAYBACK = 100;
|
||||
|
||||
function buildPlaybackRatioSteps(): Array<number> {
|
||||
const steps = [0, PLAYBACK_VISIBLE_RATIO];
|
||||
let ratio = PLAYBACK_VISIBLE_RATIO;
|
||||
while (PLAYBACK_VISIBLE_RATIO / ratio < PLAYBACK_MAX_ROOT_HEIGHTS) {
|
||||
ratio *= PLAYBACK_VISIBLE_RATIO;
|
||||
steps.push(ratio);
|
||||
}
|
||||
return steps.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
const PLAYBACK_RATIO_STEPS = buildPlaybackRatioSteps();
|
||||
|
||||
const trackedNodes = new WeakMap<Element, PlaybackToggle>();
|
||||
const playingNodes = new Set<Element>();
|
||||
|
||||
let onScreenNodes = new Set<Element>();
|
||||
|
||||
function fillsPlaybackRoot(entry: IntersectionObserverEntry): boolean {
|
||||
const rootBounds = entry.rootBounds;
|
||||
if (rootBounds == null || rootBounds.height <= 0) return false;
|
||||
return entry.intersectionRect.height >= rootBounds.height * PLAYBACK_VISIBLE_RATIO;
|
||||
}
|
||||
|
||||
function isPlaybackVisible(entry: IntersectionObserverEntry): boolean {
|
||||
if (!entry.isIntersecting) return false;
|
||||
if (entry.intersectionRatio >= PLAYBACK_VISIBLE_RATIO) return true;
|
||||
return fillsPlaybackRoot(entry);
|
||||
}
|
||||
|
||||
function isNearLeadingEdge(entry: IntersectionObserverEntry): boolean {
|
||||
const rootBounds = entry.rootBounds;
|
||||
if (rootBounds == null) return false;
|
||||
const bottomGap = Math.abs(entry.intersectionRect.bottom - rootBounds.bottom);
|
||||
const topGap = Math.abs(entry.intersectionRect.top - rootBounds.top);
|
||||
return bottomGap < topGap;
|
||||
}
|
||||
|
||||
function stopTrailingPlayback(entered: Element): void {
|
||||
const ordered = Array.from(onScreenNodes);
|
||||
for (let index = ordered.length - 1; index >= 0; index--) {
|
||||
const node = ordered[index];
|
||||
if (node === entered) continue;
|
||||
if (!playingNodes.has(node)) continue;
|
||||
playingNodes.delete(node);
|
||||
trackedNodes.get(node)?.(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function startNextWaitingPlayback(): void {
|
||||
if (playingNodes.size >= MAX_CONCURRENT_PLAYBACK) return;
|
||||
if (onScreenNodes.size <= playingNodes.size) return;
|
||||
for (const node of onScreenNodes) {
|
||||
if (playingNodes.has(node)) continue;
|
||||
playingNodes.add(node);
|
||||
trackedNodes.get(node)?.(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEntered(entry: IntersectionObserverEntry, toggle: PlaybackToggle): void {
|
||||
const node = entry.target;
|
||||
if (onScreenNodes.has(node)) return;
|
||||
const leading = isNearLeadingEdge(entry);
|
||||
if (leading) {
|
||||
onScreenNodes = new Set<Element>([node, ...onScreenNodes]);
|
||||
} else {
|
||||
onScreenNodes.add(node);
|
||||
}
|
||||
const shouldPlay = leading || playingNodes.size < MAX_CONCURRENT_PLAYBACK;
|
||||
if (shouldPlay) {
|
||||
playingNodes.add(node);
|
||||
} else {
|
||||
playingNodes.delete(node);
|
||||
}
|
||||
toggle(shouldPlay);
|
||||
if (shouldPlay && playingNodes.size > MAX_CONCURRENT_PLAYBACK) {
|
||||
stopTrailingPlayback(node);
|
||||
}
|
||||
}
|
||||
|
||||
function handleExited(node: Element, toggle: PlaybackToggle): void {
|
||||
if (!onScreenNodes.has(node)) return;
|
||||
onScreenNodes.delete(node);
|
||||
playingNodes.delete(node);
|
||||
toggle(false);
|
||||
startNextWaitingPlayback();
|
||||
}
|
||||
|
||||
export function trackViewportPlayback(
|
||||
element: Element,
|
||||
toggle: PlaybackToggle,
|
||||
clipRoot: Element | null = null,
|
||||
): () => void {
|
||||
if (trackedNodes.has(element)) {
|
||||
throw new Error('trackViewportPlayback: element is already tracked, untrack it before tracking it again');
|
||||
}
|
||||
trackedNodes.set(element, toggle);
|
||||
const release = observeIntersection(
|
||||
element,
|
||||
(entry) => {
|
||||
const current = trackedNodes.get(entry.target);
|
||||
if (current == null) return;
|
||||
if (isPlaybackVisible(entry)) {
|
||||
handleEntered(entry, current);
|
||||
} else {
|
||||
handleExited(entry.target, current);
|
||||
}
|
||||
},
|
||||
{root: clipRoot, threshold: PLAYBACK_RATIO_STEPS},
|
||||
);
|
||||
let untracked = false;
|
||||
return () => {
|
||||
if (untracked) return;
|
||||
untracked = true;
|
||||
release();
|
||||
trackedNodes.delete(element);
|
||||
onScreenNodes.delete(element);
|
||||
if (playingNodes.delete(element)) {
|
||||
startNextWaitingPlayback();
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user