perf: implement LRU-cached persistent feed mounting to eliminate Virtuoso flash

Keep Board/Catalog feeds mounted outside React Router's control to prevent
Virtuoso from remounting when navigating to/from post pages. The feed is
hidden via CSS when viewing posts and shown again when returning, eliminating
the visible flash/displacement that occurred during scroll restoration.

- Add FeedCacheContainer to manage LRU cache of 2 feeds
- Modify Board/Catalog to accept cache props and visibility state
- Add route utilities for feed/post route detection
This commit is contained in:
plebeius
2025-12-08 22:40:16 +01:00
parent 6fe4116c6b
commit 896ca4ad13
8 changed files with 376 additions and 71 deletions
@@ -0,0 +1,13 @@
.visible {
}
.hidden {
visibility: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
pointer-events: none;
overflow: hidden;
height: 0;
}
@@ -0,0 +1,99 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import useFeedCacheStore, { CachedFeed } from '../../stores/use-feed-cache-store';
import { getFeedCacheKey, getFeedType, isFeedRoute } from '../../lib/utils/route-utils';
import Board from '../../views/board';
import Catalog from '../../views/catalog';
import styles from './feed-cache-container.module.css';
interface FeedContextFromKey {
viewType: 'all' | 'subs' | 'mod' | 'board';
boardIdentifier?: string;
timeFilterName?: string;
}
const parseFeedKey = (key: string): FeedContextFromKey => {
const segments = key.split('/').filter(Boolean);
const filteredSegments = segments.filter((s) => s !== 'catalog');
if (filteredSegments[0] === 'all') {
return {
viewType: 'all',
timeFilterName: filteredSegments[1],
};
}
if (filteredSegments[0] === 'subs') {
return {
viewType: 'subs',
timeFilterName: filteredSegments[1],
};
}
if (filteredSegments[0] === 'mod') {
return {
viewType: 'mod',
timeFilterName: filteredSegments[1],
};
}
return {
viewType: 'board',
boardIdentifier: filteredSegments[0],
timeFilterName: filteredSegments[1],
};
};
interface CachedFeedWrapperProps {
feed: CachedFeed;
isVisible: boolean;
}
const CachedFeedWrapper = ({ feed, isVisible }: CachedFeedWrapperProps) => {
const context = parseFeedKey(feed.key);
return (
<div className={isVisible ? styles.visible : styles.hidden}>
{feed.type === 'catalog' ? (
<Catalog
feedCacheKey={feed.key}
viewType={context.viewType}
boardIdentifier={context.boardIdentifier}
timeFilterNameFromCache={context.timeFilterName}
isVisible={isVisible}
/>
) : (
<Board
feedCacheKey={feed.key}
viewType={context.viewType}
boardIdentifier={context.boardIdentifier}
timeFilterNameFromCache={context.timeFilterName}
isVisible={isVisible}
/>
)}
</div>
);
};
const FeedCacheContainer = () => {
const location = useLocation();
const { cachedFeeds, accessFeed } = useFeedCacheStore();
const currentFeedKey = getFeedCacheKey(location.pathname);
const isOnFeedRoute = isFeedRoute(location.pathname);
const feedType = getFeedType(location.pathname);
useEffect(() => {
if (isOnFeedRoute && currentFeedKey && feedType) {
accessFeed(currentFeedKey, feedType);
}
}, [currentFeedKey, isOnFeedRoute, feedType, accessFeed]);
return (
<>
{cachedFeeds.map((feed) => (
<CachedFeedWrapper key={feed.key} feed={feed} isVisible={isOnFeedRoute && feed.key === currentFeedKey} />
))}
</>
);
};
export default FeedCacheContainer;
@@ -0,0 +1 @@
export { default } from './feed-cache-container';