From 0a64e291a88eb667465be8af489cd429fd766d86 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 15 Jul 2024 17:04:54 +0800 Subject: [PATCH] feat: performance optimize Signed-off-by: Innei --- .../components/common/LoadMoreIndicator.tsx | 19 ++++++ .../src/modules/search/cmdk.module.css | 5 ++ src/renderer/src/modules/search/cmdk.tsx | 58 ++++++++++++++++--- 3 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 src/renderer/src/components/common/LoadMoreIndicator.tsx diff --git a/src/renderer/src/components/common/LoadMoreIndicator.tsx b/src/renderer/src/components/common/LoadMoreIndicator.tsx new file mode 100644 index 000000000..18102bfb6 --- /dev/null +++ b/src/renderer/src/components/common/LoadMoreIndicator.tsx @@ -0,0 +1,19 @@ +import { useInView } from "react-intersection-observer" + +import { LoadingCircle } from "../ui/loading" + +export const LoadMoreIndicator: Component<{ + onLoading: () => void +}> = ({ onLoading, children, className }) => { + const { ref } = useInView({ + rootMargin: "1px", + onChange(inView) { + if (inView) onLoading() + }, + }) + return ( +
+ {children ?? } +
+ ) +} diff --git a/src/renderer/src/modules/search/cmdk.module.css b/src/renderer/src/modules/search/cmdk.module.css index 517d1e935..378f82f9c 100644 --- a/src/renderer/src/modules/search/cmdk.module.css +++ b/src/renderer/src/modules/search/cmdk.module.css @@ -10,3 +10,8 @@ animation: move 2s steps(60) infinite; } } + +.content-visually { + content-visibility: auto; + contain-intrinsic-size: auto 38px; +} diff --git a/src/renderer/src/modules/search/cmdk.tsx b/src/renderer/src/modules/search/cmdk.tsx index 98e6130f4..939deeebf 100644 --- a/src/renderer/src/modules/search/cmdk.tsx +++ b/src/renderer/src/modules/search/cmdk.tsx @@ -1,4 +1,5 @@ import { setAppSearchOpen, useAppSearchOpen } from "@renderer/atoms/app" +import { LoadMoreIndicator } from "@renderer/components/common/LoadMoreIndicator" import { EmptyIcon } from "@renderer/components/icons/empty" import { Logo } from "@renderer/components/icons/logo" import { SiteIcon } from "@renderer/components/site-icon" @@ -32,7 +33,15 @@ const SearchCmdKContext = React.createContext | null>( export const SearchCmdK: React.FC = () => { const open = useAppSearchOpen() - const searchInstance = useMemo(() => searchActions.createLocalDbSearch(), []) + const [searchInstance, setSearchInstance] = React.useState(() => + searchActions.createLocalDbSearch(), + ) + React.useEffect(() => { + if (!open) return + // Refresh data + setPage(0) + setSearchInstance(() => searchActions.createLocalDbSearch()) + }, [open]) const entries = useSearchStore((s) => s.entries) const feeds = useSearchStore((s) => s.feeds) @@ -65,6 +74,7 @@ export const SearchCmdK: React.FC = () => { const handleSearch = React.useCallback( async (value: string) => { const { search } = await searchInstance + setPage(0) startTransition(() => { search(value) const $scrollView = scrollViewRef.current @@ -75,6 +85,34 @@ export const SearchCmdK: React.FC = () => { }, [searchInstance], ) + // Performance optimization + const [page, setPage] = React.useState(0) + const pageSize = 16 + const renderedEntries = useMemo( + () => entries.slice(0, (page + 1) * pageSize), + [entries, page], + ) + + const renderedFeeds = useMemo(() => { + const delta = entries.length - renderedEntries.length + if (delta > pageSize) return [] + + const entriesTotalPage = Math.ceil(entries.length / pageSize) + const right = + entriesTotalPage === page + 1 ? delta : pageSize * page - entries.length + return feeds.slice(0, right) + }, [entries.length, feeds, page, renderedEntries.length]) + const totalCount = entries.length + feeds.length + const renderedTotalCount = renderedEntries.length + renderedFeeds.length + const loadMore = React.useCallback(() => { + const totalPage = Math.ceil((entries.length + feeds.length) / pageSize) + setPage((p) => { + if (p + 1 < totalPage) return p + 1 + return p + }) + }, [entries.length, feeds.length]) + + const canLoadMore = totalCount > renderedTotalCount return ( { - {entries.length > 0 && ( + {renderedEntries.length > 0 && ( { )} className="flex w-full min-w-0 flex-col py-2" > - {entries.map((entry, index) => { + {renderedEntries.map((entry, index) => { const feed = getFeedById(entry.feedId) return ( { })} )} - {feeds.length > 0 && ( + {renderedFeeds.length > 0 && ( { )} className="py-2" > - {feeds.map((feed, index) => ( + {renderedFeeds.map((feed, index) => ( { ))} )} + {canLoadMore && + } + @@ -193,6 +234,7 @@ const SearchItem = memo(function Item({ "before:z-0 hover:before:bg-zinc-200/60 dark:hover:before:bg-zinc-800/80", "data-[selected=true]:before:bg-zinc-200/60 data-[selected=true]:dark:before:bg-zinc-800/80", "min-w-0 max-w-full", + styles["content-visually"], )} key={item.id} onSelect={() => { @@ -209,7 +251,7 @@ const SearchItem = memo(function Item({ {item.title} - + {item.subtitle} @@ -227,7 +269,7 @@ const SearchGroupHeading: FC<{ icon: string, title: string }> = ({ ) -const SearchOptions = () => { +const SearchOptions = memo(() => { const searchType = useSearchStore((s) => s.searchType) const searchInstance = React.useContext(SearchCmdKContext) @@ -282,7 +324,7 @@ const SearchOptions = () => { )} ) -} +}) const SearchPlaceholder = () => { const hasKeyword = useSearchStore((s) => !!s.keyword)