perf(banner): smoother deep-link from banner to /browse post
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:
TerryM
2026-06-02 11:30:47 +08:00
parent fbb9d21f24
commit 7ed9f8c8bf
4 changed files with 75 additions and 53 deletions

View File

@@ -12,6 +12,7 @@ import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
import { EASE_OUT } from "../motion";
import { langQuery, useI18n, type Lang } from "../i18n";
import { localizePath, stripLangPrefix } from "../languageRoutes";
import { prefetchPostStream } from "./messageStream/hooks/usePostStream";
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
@@ -87,12 +88,17 @@ function toSlides(items: BannerApiItem[]): BannerSlide[] {
.slice(0, MAX_BANNERS)
.map((item) => {
const imageUrl = assetUrl(item.imageUrl);
const raw = (item.linkUrl ?? "").trim();
// Treat empty, placeholder, or javascript: URLs as "no link" so the
// slide renders as a non-interactive image rather than a dead anchor.
const linkUrl =
raw && raw !== "#" && !/^javascript:/i.test(raw) ? raw : undefined;
return {
id: String(item.id),
mobile: imageUrl,
desktop: imageUrl,
alt: "",
linkUrl: item.linkUrl || undefined,
linkUrl,
};
});
}
@@ -138,6 +144,32 @@ export function FigmaBanner() {
};
}, [lang]);
// Banner clicks land on /browse?post=<id>. Warm the all-scope stream cache
// the moment the banner is on screen so the destination renders instantly
// instead of showing a skeleton / loading state before scrolling to the post.
useEffect(() => {
prefetchPostStream({
scope: { kind: "all" },
type: "all",
q: "",
sort: "",
lang,
});
}, [lang]);
// Safety net: if the user taps quickly before mount-prefetch completes,
// pointerdown re-issues the prefetch. Same-key requests are deduped by the
// stream cache, so this is free when the warm-up already ran.
const prefetchAllStream = useCallback(() => {
prefetchPostStream({
scope: { kind: "all" },
type: "all",
q: "",
sort: "",
lang,
});
}, [lang]);
const goTo = useCallback((index: number, behavior: ScrollBehavior) => {
const scroller = scrollerRef.current;
if (!scroller) return;
@@ -217,6 +249,7 @@ export function FigmaBanner() {
}, [hasMultiple, autoplayPaused, publicMenuOpen, slides.length, goTo]);
const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
prefetchAllStream();
if (event.pointerType !== "mouse") return;
const scroller = scrollerRef.current;
if (!scroller) return;