perf: optimize list rendering

This commit is contained in:
codytseng
2025-02-14 21:44:13 +08:00
parent 41d46b1a13
commit 978244dd22
3 changed files with 60 additions and 46 deletions

View File

@@ -20,9 +20,9 @@ import PullToRefresh from 'react-simple-pull-to-refresh'
import NoteCard from '../NoteCard' import NoteCard from '../NoteCard'
import PictureNoteCard from '../PictureNoteCard' import PictureNoteCard from '../PictureNoteCard'
const NORMAL_RELAY_LIMIT = 100 const LIMIT = 100
const ALGO_RELAY_LIMIT = 500 const ALGO_LIMIT = 500
const PICTURE_NOTE_LIMIT = 30 const SHOW_COUNT = 20
export default function NoteList({ export default function NoteList({
relayUrls, relayUrls,
@@ -45,22 +45,17 @@ export default function NoteList({
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([]) const [newEvents, setNewEvents] = useState<Event[]>([])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const [hasMore, setHasMore] = useState<boolean>(true) const [hasMore, setHasMore] = useState<boolean>(true)
const [refreshing, setRefreshing] = useState(true) const [refreshing, setRefreshing] = useState(true)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode()) const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
const isPictures = useMemo(() => listMode === 'pictures', [listMode]) const isPictures = useMemo(() => listMode === 'pictures', [listMode])
const noteFilter = useMemo(() => { const noteFilter = useMemo(() => {
if (isPictures) {
return {
kinds: [PICTURE_EVENT_KIND],
limit: PICTURE_NOTE_LIMIT,
...filter
}
}
return { return {
kinds: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND], kinds: isPictures
limit: NORMAL_RELAY_LIMIT, ? [PICTURE_EVENT_KIND]
: [kinds.ShortTextNote, kinds.Repost, PICTURE_EVENT_KIND],
...filter ...filter
} }
}, [JSON.stringify(filter), isPictures]) }, [JSON.stringify(filter), isPictures])
@@ -80,12 +75,11 @@ export default function NoteList({
const relayInfos = await relayInfoService.getRelayInfos(relayUrls) const relayInfos = await relayInfoService.getRelayInfos(relayUrls)
areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo)) areAlgoRelays = relayInfos.every((relayInfo) => checkAlgoRelay(relayInfo))
} }
const filter = areAlgoRelays ? { ...noteFilter, limit: ALGO_RELAY_LIMIT } : noteFilter
let eventCount = 0 let eventCount = 0
const { closer, timelineKey } = await client.subscribeTimeline( const { closer, timelineKey } = await client.subscribeTimeline(
[...relayUrls], [...relayUrls],
filter, { ...noteFilter, limit: areAlgoRelays ? ALGO_LIMIT : LIMIT },
{ {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
if (eventCount > events.length) return if (eventCount > events.length) return
@@ -124,23 +118,25 @@ export default function NoteList({
}, [JSON.stringify(relayUrls), noteFilter, refreshCount]) }, [JSON.stringify(relayUrls), noteFilter, refreshCount])
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
if (!timelineKey || refreshing || !hasMore) return if (showCount < events.length) {
setShowCount((prev) => prev + SHOW_COUNT)
return
}
if (!timelineKey || refreshing || !hasMore) return
const newEvents = await client.loadMoreTimeline( const newEvents = await client.loadMoreTimeline(
timelineKey, timelineKey,
events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(), events.length ? events[events.length - 1].created_at - 1 : dayjs().unix(),
noteFilter.limit LIMIT
) )
if (newEvents.length === 0) { if (newEvents.length === 0) {
setHasMore(false) setHasMore(false)
return return
} }
setEvents((oldEvents) => [...oldEvents, ...newEvents]) setEvents((oldEvents) => [...oldEvents, ...newEvents])
}, [timelineKey, refreshing, hasMore, events, noteFilter]) }, [timelineKey, refreshing, hasMore, events, noteFilter, showCount])
useEffect(() => { useEffect(() => {
if (refreshing) return
const options = { const options = {
root: null, root: null,
rootMargin: '10px', rootMargin: '10px',
@@ -164,7 +160,7 @@ export default function NoteList({
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [refreshing, loadMore]) }, [loadMore])
const showNewEvents = () => { const showNewEvents = () => {
setEvents((oldEvents) => [...newEvents, ...oldEvents]) setEvents((oldEvents) => [...newEvents, ...oldEvents])
@@ -177,6 +173,7 @@ export default function NoteList({
listMode={listMode} listMode={listMode}
setListMode={(listMode) => { setListMode={(listMode) => {
setListMode(listMode) setListMode(listMode)
setShowCount(SHOW_COUNT)
topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' }) topRef.current?.scrollIntoView({ behavior: 'instant', block: 'end' })
storage.setNoteListMode(listMode) storage.setNoteListMode(listMode)
}} }}
@@ -190,27 +187,29 @@ export default function NoteList({
pullingContent="" pullingContent=""
> >
<div> <div>
{newEvents.filter((event: Event) => { {events.length > 0 &&
return ( newEvents.filter((event: Event) => {
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) && return (
(listMode !== 'posts' || !isReplyNoteEvent(event)) (!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
) (listMode !== 'posts' || !isReplyNoteEvent(event))
}).length > 0 && ( )
<div className="flex justify-center w-full my-2"> }).length > 0 && (
<Button size="lg" onClick={showNewEvents}> <div className="flex justify-center w-full my-2">
{t('show new notes')} <Button size="lg" onClick={showNewEvents}>
</Button> {t('show new notes')}
</div> </Button>
)} </div>
)}
{isPictures ? ( {isPictures ? (
<PictureNoteCardMasonry <PictureNoteCardMasonry
className="px-2 sm:px-4 mt-2" className="px-2 sm:px-4 mt-2"
columnCount={isLargeScreen ? 3 : 2} columnCount={isLargeScreen ? 3 : 2}
events={events} events={events.slice(0, showCount)}
/> />
) : ( ) : (
<div> <div>
{events {events
.slice(0, showCount)
.filter((event: Event) => listMode !== 'posts' || !isReplyNoteEvent(event)) .filter((event: Event) => listMode !== 'posts' || !isReplyNoteEvent(event))
.map((event) => ( .map((event) => (
<NoteCard <NoteCard

View File

@@ -26,6 +26,7 @@ import { FormattedTimestamp } from '../FormattedTimestamp'
import UserAvatar from '../UserAvatar' import UserAvatar from '../UserAvatar'
const LIMIT = 100 const LIMIT = 100
const SHOW_COUNT = 30
const NotificationList = forwardRef((_, ref) => { const NotificationList = forwardRef((_, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -34,6 +35,7 @@ const NotificationList = forwardRef((_, ref) => {
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined) const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
const [refreshing, setRefreshing] = useState(true) const [refreshing, setRefreshing] = useState(true)
const [notifications, setNotifications] = useState<Event[]>([]) const [notifications, setNotifications] = useState<Event[]>([])
const [showCount, setShowCount] = useState(SHOW_COUNT)
const [until, setUntil] = useState<number | undefined>(dayjs().unix()) const [until, setUntil] = useState<number | undefined>(dayjs().unix())
const bottomRef = useRef<HTMLDivElement | null>(null) const bottomRef = useRef<HTMLDivElement | null>(null)
useImperativeHandle( useImperativeHandle(
@@ -70,15 +72,23 @@ const NotificationList = forwardRef((_, ref) => {
onEvents: (events, eosed) => { onEvents: (events, eosed) => {
if (eventCount > events.length) return if (eventCount > events.length) return
eventCount = events.length eventCount = events.length
setUntil(events.length >= LIMIT ? events[events.length - 1].created_at - 1 : undefined)
setNotifications(events.filter((event) => event.pubkey !== pubkey)) setNotifications(events.filter((event) => event.pubkey !== pubkey))
if (eosed) { if (eosed) {
setRefreshing(false) setRefreshing(false)
setUntil(events.length >= 0 ? events[events.length - 1].created_at - 1 : undefined)
} }
}, },
onNew: (event) => { onNew: (event) => {
if (event.pubkey === pubkey) return if (event.pubkey === pubkey) return
setNotifications((oldEvents) => [event, ...oldEvents]) setNotifications((oldEvents) => {
const index = oldEvents.findIndex(
(oldEvent) => oldEvent.created_at < event.created_at
)
if (index === -1) {
return [...oldEvents, event]
}
return [...oldEvents.slice(0, index), event, ...oldEvents.slice(index)]
})
} }
} }
) )
@@ -93,26 +103,30 @@ const NotificationList = forwardRef((_, ref) => {
}, [pubkey, refreshCount]) }, [pubkey, refreshCount])
const loadMore = useCallback(async () => { const loadMore = useCallback(async () => {
if (showCount < notifications.length) {
setShowCount((count) => count + SHOW_COUNT)
return
}
if (!pubkey || !timelineKey || !until || refreshing) return if (!pubkey || !timelineKey || !until || refreshing) return
const notifications = await client.loadMoreTimeline(timelineKey, until, LIMIT)
if (notifications.length === 0) { const newNotifications = await client.loadMoreTimeline(timelineKey, until, LIMIT)
if (newNotifications.length === 0) {
setUntil(undefined) setUntil(undefined)
return return
} }
if (notifications.length > 0) { if (newNotifications.length > 0) {
setNotifications((oldNotifications) => [ setNotifications((oldNotifications) => [
...oldNotifications, ...oldNotifications,
...notifications.filter((event) => event.pubkey !== pubkey) ...newNotifications.filter((event) => event.pubkey !== pubkey)
]) ])
} }
setUntil(notifications[notifications.length - 1].created_at - 1) setUntil(newNotifications[newNotifications.length - 1].created_at - 1)
}, [pubkey, timelineKey, until, refreshing]) }, [pubkey, timelineKey, until, refreshing, showCount, notifications])
useEffect(() => { useEffect(() => {
if (refreshing) return
const options = { const options = {
root: null, root: null,
rootMargin: '10px', rootMargin: '10px',
@@ -136,7 +150,7 @@ const NotificationList = forwardRef((_, ref) => {
observerInstance.unobserve(currentBottomRef) observerInstance.unobserve(currentBottomRef)
} }
} }
}, [refreshing, loadMore]) }, [loadMore])
return ( return (
<PullToRefresh <PullToRefresh
@@ -147,7 +161,7 @@ const NotificationList = forwardRef((_, ref) => {
pullingContent="" pullingContent=""
> >
<div> <div>
{notifications.map((notification) => ( {notifications.slice(0, showCount).map((notification) => (
<NotificationItem key={notification.id} notification={notification} /> <NotificationItem key={notification.id} notification={notification} />
))} ))}
<div className="text-center text-sm text-muted-foreground"> <div className="text-center text-sm text-muted-foreground">

View File

@@ -320,7 +320,8 @@ class ClientService extends EventTarget {
timeline.refs = newRefs.concat(timeline.refs) timeline.refs = newRefs.concat(timeline.refs)
onEvents(newEvents.concat(cachedEvents), true) onEvents(newEvents.concat(cachedEvents), true)
} }
} },
eoseTimeout: 10000 // 10s
}) })
} }
}) })