perf(replies): progressive render, 500 replies/page, content-visibility

Set repliesPerPage to 500 to avoid Virtuoso pagination sort drift. Add useProgressiveRender with startTransition batching and CSS content-visibility for long reply lists.
This commit is contained in:
plebeius
2026-02-15 18:46:49 +08:00
parent ec620238e3
commit dedf18e199
5 changed files with 90 additions and 2 deletions
+11 -1
View File
@@ -43,6 +43,8 @@ import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks'; import { usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
import useQuotedByMap from '../../hooks/use-quoted-by-map'; import useQuotedByMap from '../../hooks/use-quoted-by-map';
import useProgressiveRender from '../../hooks/use-progressive-render';
import { REPLIES_PER_PAGE } from '../../lib/constants';
const { addChallenge } = useChallengesStore.getState(); const { addChallenge } = useChallengesStore.getState();
@@ -674,6 +676,7 @@ const PostDesktop = ({
comment: post, comment: post,
sortType: 'old', sortType: 'old',
flat: true, flat: true,
repliesPerPage: REPLIES_PER_PAGE,
accountComments: { newerThan: Infinity, append: true }, accountComments: { newerThan: Infinity, append: true },
}); });
const { replies, hasMore, loadMore } = repliesResult; const { replies, hasMore, loadMore } = repliesResult;
@@ -737,6 +740,13 @@ const PostDesktop = ({
const quotedByMap = useQuotedByMap(filteredReplies); const quotedByMap = useQuotedByMap(filteredReplies);
const visibleReplies = useProgressiveRender(filteredReplies, {
batchSize: 50,
intervalMs: 100,
resetKey: cid,
disabled: hasMore || !!targetReplyCid || !showAllReplies,
});
// Virtuoso scroll position management for infinite replies // Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null); const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-desktop-${cid}`; const virtuosoStateKey = `replies-desktop-${cid}`;
@@ -888,7 +898,7 @@ const PostDesktop = ({
!isInPendingPostView && !isInPendingPostView &&
showReplies && showReplies &&
!hasMore && !hasMore &&
filteredReplies.map((reply, index) => ( visibleReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply <Reply
reply={reply} reply={reply}
+11 -1
View File
@@ -37,6 +37,8 @@ import useFeedResetStore from '../../stores/use-feed-reset-store';
import usePostNumberStore from '../../stores/use-post-number-store'; import usePostNumberStore from '../../stores/use-post-number-store';
import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils'; import { alertChallengeVerificationFailed } from '../../lib/utils/challenge-utils';
import useQuotedByMap from '../../hooks/use-quoted-by-map'; import useQuotedByMap from '../../hooks/use-quoted-by-map';
import useProgressiveRender from '../../hooks/use-progressive-render';
import { REPLIES_PER_PAGE } from '../../lib/constants';
const { addChallenge } = useChallengesStore.getState(); const { addChallenge } = useChallengesStore.getState();
@@ -497,6 +499,7 @@ const PostMobile = ({
comment: post, comment: post,
sortType: 'old', sortType: 'old',
flat: true, flat: true,
repliesPerPage: REPLIES_PER_PAGE,
accountComments: { newerThan: Infinity, append: true }, accountComments: { newerThan: Infinity, append: true },
}); });
const { replies, hasMore, loadMore } = repliesResult; const { replies, hasMore, loadMore } = repliesResult;
@@ -537,6 +540,13 @@ const PostMobile = ({
const quotedByMap = useQuotedByMap(filteredReplies); const quotedByMap = useQuotedByMap(filteredReplies);
const visibleReplies = useProgressiveRender(filteredReplies, {
batchSize: 50,
intervalMs: 100,
resetKey: cid,
disabled: hasMore || !!targetReplyCid || !showAllReplies,
});
// Virtuoso scroll position management for infinite replies // Virtuoso scroll position management for infinite replies
const virtuosoRef = useRef<VirtuosoHandle | null>(null); const virtuosoRef = useRef<VirtuosoHandle | null>(null);
const virtuosoStateKey = `replies-mobile-${cid}`; const virtuosoStateKey = `replies-mobile-${cid}`;
@@ -670,7 +680,7 @@ const PostMobile = ({
!isInPendingPostView && !isInPendingPostView &&
showReplies && showReplies &&
!hasMore && !hasMore &&
filteredReplies.map((reply, index) => ( visibleReplies.map((reply, index) => (
<div key={index} className={styles.replyContainer}> <div key={index} className={styles.replyContainer}>
<Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} /> <Reply postReplyCount={replyCount} reply={reply} roles={roles} threadNumber={post?.number} quotedByMap={quotedByMap} />
</div> </div>
+62
View File
@@ -0,0 +1,62 @@
import { startTransition, useEffect, useState } from 'react';
interface UseProgressiveRenderOptions {
batchSize?: number;
intervalMs?: number;
resetKey?: string;
disabled?: boolean;
}
const DEFAULT_BATCH_SIZE = 50;
const DEFAULT_INTERVAL_MS = 100;
/**
* Progressively reveals items in batches via startTransition to avoid blocking the UI
* when mounting many heavy components (e.g. 500 reply components).
* When disabled or once fully caught up, returns the full array immediately.
*/
const useProgressiveRender = <T>(items: T[], options: UseProgressiveRenderOptions = {}): T[] => {
const { batchSize = DEFAULT_BATCH_SIZE, intervalMs = DEFAULT_INTERVAL_MS, resetKey, disabled = false } = options;
const [progressState, setProgressState] = useState<{ key: string | undefined; visibleCount: number }>({
key: resetKey,
visibleCount: batchSize,
});
const isResetKeyChanged = progressState.key !== resetKey;
const visibleCount = isResetKeyChanged ? batchSize : progressState.visibleCount;
useEffect(() => {
if (disabled || items.length <= batchSize) {
return;
}
const current = visibleCount >= items.length ? items.length : visibleCount;
if (current >= items.length) {
return;
}
const next = Math.min(current + batchSize, items.length);
const timer = window.setTimeout(() => {
startTransition(() => {
setProgressState({
key: resetKey,
visibleCount: next,
});
});
}, intervalMs);
return () => window.clearTimeout(timer);
}, [items.length, visibleCount, batchSize, intervalMs, disabled, resetKey]);
if (disabled || items.length <= batchSize) {
return items;
}
if (visibleCount >= items.length) {
return items;
}
return items.slice(0, visibleCount);
};
export default useProgressiveRender;
+2
View File
@@ -0,0 +1,2 @@
/** Max replies loaded per page from useReplies. Threads with > this use Virtuoso. */
export const REPLIES_PER_PAGE = 500;
+4
View File
@@ -456,6 +456,8 @@
.replyDesktop { .replyDesktop {
padding-top: 4px; padding-top: 4px;
content-visibility: auto;
contain-intrinsic-size: auto 120px;
} }
.replyDesktop .sideArrows { .replyDesktop .sideArrows {
@@ -490,6 +492,8 @@
.replyMobile { .replyMobile {
padding-top: 7px; padding-top: 7px;
content-visibility: auto;
contain-intrinsic-size: auto 120px;
} }
.replyMobile .replyContainer { .replyMobile .replyContainer {