perf(banner): smoother deep-link from banner to /browse post
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 27s
All checks were successful
Deploy to Frontend Servers / deploy (push) Successful in 27s
- MessageStream: drop the mount-time scroll lock and the 80ms-delayed
custom rAF; engage the lock only while the smooth animation runs and
use native scrollTo({behavior:'smooth'}) so the page never feels frozen
during pagination and the easing is buttery.
- PublicLayout: fire the default /browse prefetch immediately on mount
(banner / Home tile destination) so a fast tap hits a warm cache;
popular / latest stay deferred to idle.
- FigmaBanner: prefetch the all-scope stream on mount and on pointerdown
as safety nets, and ignore empty / '#' / javascript: link URLs so a
contentless banner renders as a non-interactive image.
- usePostStream: dedupe in-flight prefetches by key so concurrent
callers (layout + banner) collapse into a single network request.
This commit is contained in:
@@ -79,9 +79,10 @@ export function MessageStream({ scope }: MessageStreamProps) {
|
||||
? hash.slice("#post-".length)
|
||||
: "";
|
||||
const targetPostId = queryTargetPostId || hashTargetPostId;
|
||||
const [isAligningQueryTarget, setIsAligningQueryTarget] = useState(
|
||||
Boolean(queryTargetPostId),
|
||||
);
|
||||
// Lock only engages while we are actively running the smooth-scroll animation
|
||||
// — not during the wait/pagination phase — so the page never feels frozen
|
||||
// before the bubble exists.
|
||||
const [isAligningQueryTarget, setIsAligningQueryTarget] = useState(false);
|
||||
const handledTargetRef = useRef<string>("");
|
||||
const targetScrollTimersRef = useRef<number[]>([]);
|
||||
const targetScrollFrameRef = useRef<number | null>(null);
|
||||
@@ -100,7 +101,7 @@ export function MessageStream({ scope }: MessageStreamProps) {
|
||||
useEffect(() => {
|
||||
handledTargetRef.current = "";
|
||||
clearTargetScrollTimers();
|
||||
setIsAligningQueryTarget(Boolean(queryTargetPostId));
|
||||
setIsAligningQueryTarget(false);
|
||||
}, [queryTargetPostId, targetPostId]);
|
||||
|
||||
useEffect(() => clearTargetScrollTimers, []);
|
||||
@@ -168,52 +169,29 @@ export function MessageStream({ scope }: MessageStreamProps) {
|
||||
window.scrollTo({ top, left: 0, behavior });
|
||||
};
|
||||
|
||||
const boundedSmoothScrollToTarget = () => {
|
||||
const firstTarget = targetScrollTop();
|
||||
if (firstTarget === null) return;
|
||||
const start = window.scrollY;
|
||||
const startedAt = performance.now();
|
||||
const duration = 520;
|
||||
const direction = firstTarget >= start ? 1 : -1;
|
||||
const easeOutCubic = (x: number) => 1 - Math.pow(1 - x, 3);
|
||||
|
||||
const tick = (now: number) => {
|
||||
const latestTarget = targetScrollTop();
|
||||
if (latestTarget === null) return;
|
||||
const progress = Math.min(1, (now - startedAt) / duration);
|
||||
const ideal = start + (latestTarget - start) * easeOutCubic(progress);
|
||||
const next =
|
||||
direction >= 0
|
||||
? Math.min(ideal, latestTarget)
|
||||
: Math.max(ideal, latestTarget);
|
||||
window.scrollTo({ top: next, left: 0, behavior: "auto" });
|
||||
|
||||
if (progress < 1) {
|
||||
targetScrollFrameRef.current = window.requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToTarget("auto");
|
||||
setIsAligningQueryTarget(false);
|
||||
targetScrollFrameRef.current = null;
|
||||
};
|
||||
|
||||
targetScrollFrameRef.current = window.requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
|
||||
if (queryTargetPostId) {
|
||||
// Query deep-links (`?post=<id>`) usually come from Home cards/list
|
||||
// rows. Keep the premium motion, but drive it ourselves so scrolling is
|
||||
// bounded to the current target position and cannot visibly pass the
|
||||
// target before snapping back. User-driven scroll is temporarily locked
|
||||
// by the effect above; programmatic scroll remains allowed.
|
||||
targetScrollTimersRef.current = [80].map((ms) =>
|
||||
window.setTimeout(boundedSmoothScrollToTarget, ms),
|
||||
// Query deep-links (`?post=<id>`) come from banner / Home card clicks.
|
||||
// Use the browser-native smooth scroll (runs on the compositor, much
|
||||
// smoother than a hand-rolled rAF) and lock user scroll only for the
|
||||
// duration of the animation so the page never feels frozen. Quiet
|
||||
// re-alignments run inside the lock window to absorb late image-shift
|
||||
// above the target; nothing nudges the user after the lock releases.
|
||||
setIsAligningQueryTarget(true);
|
||||
window.requestAnimationFrame(() =>
|
||||
scrollToTarget(prefersReducedMotion ? "auto" : "smooth"),
|
||||
);
|
||||
targetScrollTimersRef.current = [
|
||||
window.setTimeout(() => scrollToTarget("auto"), 450),
|
||||
window.setTimeout(() => scrollToTarget("auto"), 750),
|
||||
window.setTimeout(() => {
|
||||
scrollToTarget("auto");
|
||||
setIsAligningQueryTarget(false);
|
||||
}, 950),
|
||||
];
|
||||
} else {
|
||||
window.requestAnimationFrame(() =>
|
||||
scrollToTarget(prefersReducedMotion ? "auto" : "smooth"),
|
||||
|
||||
Reference in New Issue
Block a user