PostThread perf: reduce time wasted on rendering skeletons (#10092)

This commit is contained in:
Samuel Newman
2026-03-25 12:44:36 -07:00
committed by GitHub
parent cc5580848c
commit c06312f09a
2 changed files with 45 additions and 27 deletions
@@ -1,4 +1,4 @@
import {memo, useCallback, useMemo} from 'react' import {memo, useMemo} from 'react'
import {Text as RNText, View} from 'react-native' import {Text as RNText, View} from 'react-native'
import { import {
AppBskyFeedDefs, AppBskyFeedDefs,
@@ -9,6 +9,7 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
@@ -245,7 +246,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
} }
}, [postSource]) }, [postSource])
const onPressReply = useCallback(() => { const onPressReply = useNonReactiveCallback(() => {
openComposer({ openComposer({
replyTo: { replyTo: {
uri: post.uri, uri: post.uri,
@@ -268,15 +269,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({
reqId: postSource.post.reqId, reqId: postSource.post.reqId,
}) })
} }
}, [ })
openComposer,
post,
record,
onPostSuccess,
moderation,
postSource,
feedFeedback,
])
const onOpenAuthor = () => { const onOpenAuthor = () => {
ax.metric('post:clickthroughAuthor', { ax.metric('post:clickthroughAuthor', {
+41 -16
View File
@@ -1,9 +1,17 @@
import {useCallback, useEffect, useMemo, useRef, useState} from 'react' import {
startTransition,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import {useWindowDimensions, View} from 'react-native' import {useWindowDimensions, View} from 'react-native'
import Animated, {useAnimatedStyle} from 'react-native-reanimated' import Animated, {useAnimatedStyle} from 'react-native-reanimated'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
import {useFeedFeedback} from '#/state/feed-feedback' import {useFeedFeedback} from '#/state/feed-feedback'
@@ -98,7 +106,7 @@ export function PostThread({uri}: {uri: string}) {
const trackThreadItemView = usePostViewTracking('PostThreadItem') const trackThreadItemView = usePostViewTracking('PostThreadItem')
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const optimisticOnPostReply = useCallback( const optimisticOnPostReply = useNonReactiveCallback(
(payload: OnPostSuccessData) => { (payload: OnPostSuccessData) => {
if (payload) { if (payload) {
const {replyToUri, posts} = payload const {replyToUri, posts} = payload
@@ -107,9 +115,8 @@ export function PostThread({uri}: {uri: string}) {
} }
} }
}, },
[thread],
) )
const onReplyToAnchor = useCallback(() => { const onReplyToAnchor = useNonReactiveCallback(() => {
if (anchor?.type !== 'threadPost') { if (anchor?.type !== 'threadPost') {
return return
} }
@@ -136,13 +143,7 @@ export function PostThread({uri}: {uri: string}) {
reqId: anchorPostSource.post.reqId, reqId: anchorPostSource.post.reqId,
}) })
} }
}, [ })
anchor,
openComposer,
optimisticOnPostReply,
anchorPostSource,
feedFeedback,
])
const isRoot = !!anchor && anchor.value.post.record.reply === undefined const isRoot = !!anchor && anchor.value.post.record.reply === undefined
const canReply = !anchor?.value.post?.viewer?.replyDisabled const canReply = !anchor?.value.post?.viewer?.replyDisabled
@@ -389,12 +390,34 @@ export function PostThread({uri}: {uri: string}) {
return results return results
}, [thread, deferParents, maxParentCount, maxChildrenCount]) }, [thread, deferParents, maxParentCount, maxChildrenCount])
/**
* Defer rendering reply skeletons so that the anchor post (from cache)
* can paint without being blocked by skeleton layout work. On mount,
* skeletons are filtered out. After the first render, they're added
* back via a low-priority transition.
*/
const [showReplySkeletons, setShowReplySkeletons] = useState(false)
useEffect(() => {
if (thread.state.isPlaceholderData && !showReplySkeletons) {
startTransition(() => {
setShowReplySkeletons(true)
})
}
}, [thread.state.isPlaceholderData, showReplySkeletons])
const deferredSlices = useMemo(() => {
if (showReplySkeletons) return slices
return slices.filter(
item => !(item.type === 'skeleton' && item.item === 'reply'),
)
}, [slices, showReplySkeletons])
const isTombstoneView = useMemo(() => { const isTombstoneView = useMemo(() => {
if (slices.length > 1) return false if (deferredSlices.length > 1) return false
return slices.every( return deferredSlices.every(
s => s.type === 'threadPostBlocked' || s.type === 'threadPostNotFound', s => s.type === 'threadPostBlocked' || s.type === 'threadPostNotFound',
) )
}, [slices]) }, [deferredSlices])
const renderItem = useCallback( const renderItem = useCallback(
({item, index}: {item: ThreadItem; index: number}) => { ({item, index}: {item: ThreadItem; index: number}) => {
@@ -547,7 +570,7 @@ export function PostThread({uri}: {uri: string}) {
) : ( ) : (
<List <List
ref={listRef} ref={listRef}
data={slices} data={deferredSlices}
renderItem={renderItem} renderItem={renderItem}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
onContentSizeChange={platform({ onContentSizeChange={platform({
@@ -594,8 +617,10 @@ export function PostThread({uri}: {uri: string}) {
initialNumToRender={initialNumToRender} initialNumToRender={initialNumToRender}
/** /**
* Default: 21 * Default: 21
*
* Smaller for placeholder data so we don't waste time rendering skeletons
*/ */
windowSize={7} windowSize={thread.state.isPlaceholderData ? 1 : 7}
/** /**
* Default: 10 * Default: 10
*/ */