perf: isolate IPFS state updates in board and catalog views to prevent excessive re-renders

This commit is contained in:
plebeius
2026-01-13 17:21:30 +01:00
parent c0c6e9ee31
commit eb8e226ff1
2 changed files with 454 additions and 192 deletions
+222 -100
View File
@@ -22,6 +22,149 @@ import { Post } from '../post';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
interface BoardFooterProps {
subplebbitAddresses: string[];
hasMore: boolean;
combinedFeedLength: number;
subplebbitAddressesWithNewerPosts: string[];
onNewerPostsClick: () => void;
isInAllView: boolean;
isInSubscriptionsView: boolean;
isInModView: boolean;
showMorePostsSuggestion: boolean;
feedLength: number;
weeklyFeedLength: number;
monthlyFeedLength: number;
yearlyFeedLength: number;
boardPath: string | undefined;
currentTimeFilterName: string | undefined;
subplebbitState: string | undefined;
subscriptionsLength: number;
accountSubplebbitAddressesLength: number;
blocked: boolean;
onUnblock: () => void;
}
// Defined outside Board to preserve component identity across renders (Virtuoso optimization)
// The useFeedStateString hook is called here instead of in Board to isolate re-renders
// caused by backend IPFS state changes to just this footer component
const BoardFooter = ({
subplebbitAddresses,
hasMore,
combinedFeedLength,
subplebbitAddressesWithNewerPosts,
onNewerPostsClick,
isInAllView,
isInSubscriptionsView,
isInModView,
showMorePostsSuggestion,
feedLength,
weeklyFeedLength,
monthlyFeedLength,
yearlyFeedLength,
boardPath,
currentTimeFilterName,
subplebbitState,
subscriptionsLength,
accountSubplebbitAddressesLength,
blocked,
onUnblock,
}: BoardFooterProps) => {
const { t } = useTranslation();
const loadingStateString =
useFeedStateString(subplebbitAddresses) ||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength)
? t('loading_feed')
: t('looking_for_more_posts'));
let footerContent;
if (combinedFeedLength === 0) {
footerContent = t('no_threads');
}
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
footerContent = (
<>
{subplebbitAddressesWithNewerPosts.length > 0 ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='newer_threads_available'
components={{
1: <span className={styles.newerPostsButton} onClick={onNewerPostsClick} />,
}}
/>
</div>
) : (
(isInAllView || isInSubscriptionsView || isInModView) &&
showMorePostsSuggestion &&
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
(() => {
const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
return weeklyFeedLength > feedLength ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_week'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={`${basePath}/1w`} />,
}}
/>
</div>
) : monthlyFeedLength > feedLength ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_month'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={`${basePath}/1m`} />,
}}
/>
</div>
) : (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_year'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={`${basePath}/1y`} />,
}}
/>
</div>
);
})()
)}
</>
);
}
return (
<div className={styles.footer}>
{footerContent}
<div>
{subplebbitState === 'failed' ? (
<span className='red'>{subplebbitState}</span>
) : isInSubscriptionsView && subscriptionsLength === 0 ? (
<span className='red'>{t('not_subscribed_to_any_board')}</span>
) : isInModView && accountSubplebbitAddressesLength === 0 ? (
<span className='red'>{t('not_mod_of_any_board')}</span>
) : blocked ? (
<span className='red'>{t('you_have_blocked_this_board')}</span>
) : (
hasMore && <LoadingEllipsis string={loadingStateString} />
)}
{blocked && (
<>
&nbsp;&nbsp;[
<span className={styles.button} onClick={onUnblock}>
{t('unblock')}
</span>
]
</>
)}
</div>
</div>
);
};
const createThreadsWithoutImagesFilter = () => ({
filter: (comment: Comment) => {
const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {};
@@ -195,12 +338,6 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
const weeklyFeedLength = weeklyFeed.length;
const monthlyFeedLength = monthlyFeed.length;
const yearlyFeedLength = yearlyFeed.length;
const hasFeedLoaded = !!feed;
const loadingStateString =
useFeedStateString(subplebbitAddresses) ||
(!hasFeedLoaded || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
? t('loading_feed')
: t('looking_for_more_posts'));
const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false);
useEffect(() => {
@@ -213,100 +350,64 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
const Footer = () => {
let footerContent;
if (combinedFeed.length === 0) {
footerContent = t('no_threads');
}
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
footerContent = (
<>
{subplebbitAddressesWithNewerPosts.length > 0 ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='newer_threads_available'
components={{
1: <span className={styles.newerPostsButton} onClick={handleNewerPostsButtonClick} />,
}}
/>
</div>
) : (
(isInAllView || isInSubscriptionsView || isInModView) &&
showMorePostsSuggestion &&
(monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
(() => {
const basePath = isInAllView ? '/all' : isInSubscriptionsView ? '/subs' : isInModView ? '/mod' : boardPath ? `/${boardPath}` : '';
return weeklyFeed.length > feed.length ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_week'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={`${basePath}/1w`} />,
}}
/>
</div>
) : monthlyFeed.length > feed.length ? (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_month'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={`${basePath}/1m`} />,
}}
/>
</div>
) : (
<div className={styles.morePostsSuggestion}>
<Trans
i18nKey='more_threads_last_year'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={`${basePath}/1y`} />,
}}
/>
</div>
);
})()
)}
</>
);
}
return (
<div className={styles.footer}>
{footerContent}
<div>
{subplebbitState === 'failed' ? (
<span className='red'>{subplebbitState}</span>
) : isInSubscriptionsView && subscriptions?.length === 0 ? (
<span className='red'>{t('not_subscribed_to_any_board')}</span>
) : isInModView && accountSubplebbitAddresses?.length === 0 ? (
<span className='red'>{t('not_mod_of_any_board')}</span>
) : blocked ? (
<span className='red'>{t('you_have_blocked_this_board')}</span>
) : (
hasMore && <LoadingEllipsis string={loadingStateString} />
)}
{blocked && (
<>
&nbsp;&nbsp;[
<span
className={styles.button}
onClick={() => {
unblock();
reset();
}}
>
{t('unblock')}
</span>
]
</>
)}
</div>
</div>
);
const handleUnblock = () => {
unblock();
reset();
};
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
// Note: useFeedStateString is called inside BoardFooter to isolate re-renders from backend state changes
const footerComponents = useMemo(
() => ({
Footer: () => (
<BoardFooter
subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore}
combinedFeedLength={combinedFeed.length}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
onNewerPostsClick={handleNewerPostsButtonClick}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
isInModView={isInModView}
showMorePostsSuggestion={showMorePostsSuggestion}
feedLength={feedLength}
weeklyFeedLength={weeklyFeedLength}
monthlyFeedLength={monthlyFeedLength}
yearlyFeedLength={yearlyFeedLength}
boardPath={boardPath}
currentTimeFilterName={currentTimeFilterName}
subplebbitState={subplebbitState}
subscriptionsLength={subscriptions?.length || 0}
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
blocked={blocked || false}
onUnblock={handleUnblock}
/>
),
}),
[
subplebbitAddresses,
hasMore,
combinedFeed.length,
subplebbitAddressesWithNewerPosts,
handleNewerPostsButtonClick,
isInAllView,
isInSubscriptionsView,
isInModView,
showMorePostsSuggestion,
feedLength,
weeklyFeedLength,
monthlyFeedLength,
yearlyFeedLength,
boardPath,
currentTimeFilterName,
subplebbitState,
subscriptions?.length,
accountSubplebbitAddresses?.length,
blocked,
handleUnblock,
],
);
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${sortType}-${timeFilterSeconds}` : `${location.pathname}-${sortType}-${timeFilterSeconds}`;
const navigationType = useNavigationType();
@@ -389,7 +490,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
data={combinedFeed}
itemContent={(index, post) => <Post index={index} post={post} />}
useWindowScroll={true}
components={{ Footer }}
components={footerComponents}
endReached={loadMore}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
@@ -400,7 +501,28 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
{combinedFeed.map((post, index) => (
<Post key={post.cid} index={index} post={post} />
))}
<Footer />
<BoardFooter
subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore}
combinedFeedLength={combinedFeed.length}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
onNewerPostsClick={handleNewerPostsButtonClick}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
isInModView={isInModView}
showMorePostsSuggestion={showMorePostsSuggestion}
feedLength={feedLength}
weeklyFeedLength={weeklyFeedLength}
monthlyFeedLength={monthlyFeedLength}
yearlyFeedLength={yearlyFeedLength}
boardPath={boardPath}
currentTimeFilterName={currentTimeFilterName}
subplebbitState={subplebbitState}
subscriptionsLength={subscriptions?.length || 0}
accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0}
blocked={blocked || false}
onUnblock={handleUnblock}
/>
</>
)}
</div>
+232 -92
View File
@@ -24,6 +24,168 @@ import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
interface CatalogFooterProps {
subplebbitAddresses: string[];
hasMore: boolean;
feedLength: number;
combinedFeedLength: number;
subplebbitAddressesWithNewerPosts: string[];
onNewerPostsClick: () => void;
isInAllView: boolean;
isInSubscriptionsView: boolean;
showMorePostsSuggestion: boolean;
weeklyFeedLength: number;
monthlyFeedLength: number;
yearlyFeedLength: number;
boardPath: string | undefined;
currentTimeFilterName: string | undefined;
}
// Defined outside Catalog to preserve component identity across renders (Virtuoso optimization)
// The useFeedStateString hook is called here instead of in Catalog to isolate re-renders
// caused by backend IPFS state changes to just this footer component
const CatalogFooter = ({
subplebbitAddresses,
hasMore,
feedLength,
combinedFeedLength,
subplebbitAddressesWithNewerPosts,
onNewerPostsClick,
isInAllView,
isInSubscriptionsView,
showMorePostsSuggestion,
weeklyFeedLength,
monthlyFeedLength,
yearlyFeedLength,
boardPath,
currentTimeFilterName,
}: CatalogFooterProps) => {
const { t } = useTranslation();
const loadingStateString =
useFeedStateString(subplebbitAddresses) ||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
? t('loading_feed')
: t('looking_for_more_posts');
let footerContent;
if (feedLength === 0) {
if (combinedFeedLength === 0) {
footerContent = t('no_threads');
}
}
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
footerContent = (
<>
{subplebbitAddressesWithNewerPosts.length > 0 ? (
<div className={styles.stateString}>
<Trans
i18nKey='newer_threads_available'
components={{
1: <span className={styles.newerPostsButton} onClick={onNewerPostsClick} />,
}}
/>
</div>
) : (
(isInAllView || isInSubscriptionsView) &&
showMorePostsSuggestion &&
(monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) &&
(weeklyFeedLength > feedLength ? (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_week'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1w'} />,
}}
/>
</div>
) : monthlyFeedLength > feedLength ? (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_month'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1m'} />,
}}
/>
</div>
) : (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_year'
values={{ currentTimeFilterName, count: feedLength }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1y'} />,
}}
/>
</div>
))
)}
<div className={styles.stateString}>
<LoadingEllipsis string={loadingStateString} />
</div>
</>
);
}
return <div className={styles.footer}>{footerContent}</div>;
};
// Separate component for the loading state when there's no feed
// This also calls useFeedStateString internally to isolate re-renders
interface CatalogLoadingProps {
subplebbitAddresses: string[];
hasMore: boolean;
feedLength: number;
weeklyFeedLength: number;
monthlyFeedLength: number;
yearlyFeedLength: number;
state: string | undefined;
subscriptionsLength: number;
blocked: boolean;
combinedFeedLength: number;
error: Error | undefined;
}
const CatalogLoading = ({
subplebbitAddresses,
hasMore,
feedLength,
weeklyFeedLength,
monthlyFeedLength,
yearlyFeedLength,
state,
subscriptionsLength,
blocked,
combinedFeedLength,
error,
}: CatalogLoadingProps) => {
const { t } = useTranslation();
const rawFeedStateString = useFeedStateString(subplebbitAddresses);
const loadingStateString =
rawFeedStateString || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
? t('loading_feed')
: t('looking_for_more_posts');
return (
<div className={styles.stateString}>
{state === 'failed' ? (
<span className='red'>{state}</span>
) : subscriptionsLength === 0 ? (
<span className='red'>{t('not_subscribed_to_any_board')}</span>
) : blocked ? (
t('you_have_blocked_this_board')
) : !hasMore && combinedFeedLength === 0 ? (
t('no_threads')
) : (
hasMore && <LoadingEllipsis string={loadingStateString} />
)}
<ErrorDisplay error={error} />
</div>
);
};
const createContentFilter = (
filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set<string>; hide: boolean; top: boolean; color?: string }[],
subplebbitAddress: string,
@@ -325,98 +487,49 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
const weeklyFeedLength = weeklyFeed.length;
const monthlyFeedLength = monthlyFeed.length;
const yearlyFeedLength = yearlyFeed.length;
const hasFeedLoaded = !!feed;
const loadingStateString =
useFeedStateString(subplebbitAddresses) ||
!hasFeedLoaded ||
(feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength))
? t('loading_feed')
: t('looking_for_more_posts');
const loadingString = (
<div className={styles.stateString}>
{state === 'failed' ? (
<span className='red'>{state}</span>
) : isInSubscriptionsView && subscriptions?.length === 0 ? (
<span className='red'>{t('not_subscribed_to_any_board')}</span>
) : blocked ? (
t('you_have_blocked_this_board')
) : !hasMore && combinedFeed.length === 0 ? (
t('no_threads')
) : (
hasMore && <LoadingEllipsis string={loadingStateString} />
)}
<ErrorDisplay error={error} />
</div>
);
const currentTimeFilterName = timeFilterName || params?.timeFilterName;
const Footer = () => {
let footerContent;
if (feed.length === 0) {
if (blocked) {
footerContent = t('you_have_blocked_this_board');
} else if (combinedFeed.length === 0) {
footerContent = t('no_threads');
}
}
if (hasMore || (subplebbitAddresses && subplebbitAddresses.length === 0)) {
footerContent = (
<>
{subplebbitAddressesWithNewerPosts.length > 0 ? (
<div className={styles.stateString}>
<Trans
i18nKey='newer_threads_available'
components={{
1: <span className={styles.newerPostsButton} onClick={handleNewerPostsButtonClick} />,
}}
/>
</div>
) : (
(isInAllView || isInSubscriptionsView) &&
showMorePostsSuggestion &&
(monthlyFeed.length > feed.length || yearlyFeed.length > monthlyFeed.length) &&
(weeklyFeed.length > feed.length ? (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_week'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1w'} />,
}}
/>
</div>
) : monthlyFeed.length > feed.length ? (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_month'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1m'} />,
}}
/>
</div>
) : (
<div className={styles.stateString}>
<Trans
i18nKey='more_threads_last_year'
values={{ currentTimeFilterName, count: feed.length }}
components={{
1: <Link to={(isInAllView ? '/all/catalog' : isInSubscriptionsView ? '/subs/catalog' : `/${boardPath}/catalog`) + '/1y'} />,
}}
/>
</div>
))
)}
<div className={styles.stateString}>
<LoadingEllipsis string={loadingStateString} />
</div>
</>
);
}
return <div className={styles.footer}>{footerContent}</div>;
};
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
const footerComponents = useMemo(
() => ({
Footer: () => (
<CatalogFooter
subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore}
feedLength={feedLength}
combinedFeedLength={combinedFeed.length}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
onNewerPostsClick={handleNewerPostsButtonClick}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
showMorePostsSuggestion={showMorePostsSuggestion}
weeklyFeedLength={weeklyFeedLength}
monthlyFeedLength={monthlyFeedLength}
yearlyFeedLength={yearlyFeedLength}
boardPath={boardPath}
currentTimeFilterName={currentTimeFilterName}
/>
),
}),
[
subplebbitAddresses,
hasMore,
feedLength,
combinedFeed.length,
subplebbitAddressesWithNewerPosts,
handleNewerPostsButtonClick,
isInAllView,
isInSubscriptionsView,
showMorePostsSuggestion,
weeklyFeedLength,
monthlyFeedLength,
yearlyFeedLength,
boardPath,
currentTimeFilterName,
],
);
const isFeedLoaded = feed.length > 0 || state === 'failed';
@@ -548,7 +661,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
data={rows}
itemContent={(index, row) => <CatalogRow index={index} row={row} />}
useWindowScroll={true}
components={{ Footer }}
components={footerComponents}
endReached={loadMore}
ref={virtuosoRef}
restoreStateFrom={lastVirtuosoState}
@@ -559,13 +672,40 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
{rows.map((row, index) => (
<CatalogRow key={index} index={index} row={row} />
))}
<Footer />
<CatalogFooter
subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore}
feedLength={feedLength}
combinedFeedLength={combinedFeed.length}
subplebbitAddressesWithNewerPosts={subplebbitAddressesWithNewerPosts}
onNewerPostsClick={handleNewerPostsButtonClick}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
showMorePostsSuggestion={showMorePostsSuggestion}
weeklyFeedLength={weeklyFeedLength}
monthlyFeedLength={monthlyFeedLength}
yearlyFeedLength={yearlyFeedLength}
boardPath={boardPath}
currentTimeFilterName={currentTimeFilterName}
/>
</>
)}
</>
) : (
<div className={styles.footer}>
{loadingString}
<CatalogLoading
subplebbitAddresses={subplebbitAddresses}
hasMore={hasMore}
feedLength={feedLength}
weeklyFeedLength={weeklyFeedLength}
monthlyFeedLength={monthlyFeedLength}
yearlyFeedLength={yearlyFeedLength}
state={state}
subscriptionsLength={isInSubscriptionsView ? subscriptions?.length || 0 : 1}
blocked={blocked || false}
combinedFeedLength={combinedFeed.length}
error={error}
/>
{blocked && (
<>
&nbsp;&nbsp;[