mirror of https://github.com/usememos/memos.git
feat(web): improve loading performance with skeleton screens and parallel fetches
- Add MemoSkeleton component for smooth initial page load experience - Integrate skeleton loader into PagedMemoList during initial fetch - Parallelize user settings and shortcuts API calls (~50% faster session init) - Batch-fetch memo creators in parallel to eliminate individual loading spinners - Pass showCreator prop to Explore page for proper skeleton rendering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2371bbb1b7
commit
d794c0bf8b
|
|
@ -0,0 +1,56 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
showCreator?: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const MemoSkeleton = ({ showCreator = false, count = 6 }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative flex flex-col justify-start items-start bg-card w-full px-4 py-3 mb-2 gap-2 rounded-lg border border-border animate-pulse"
|
||||
>
|
||||
{/* Header section */}
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<div className="w-auto max-w-[calc(100%-8rem)] grow flex flex-row justify-start items-center">
|
||||
{showCreator ? (
|
||||
<div className="w-full flex flex-row justify-start items-center gap-2">
|
||||
{/* Avatar skeleton */}
|
||||
<div className="w-8 h-8 rounded-full bg-muted shrink-0" />
|
||||
<div className="w-full flex flex-col justify-center items-start gap-1">
|
||||
{/* Creator name skeleton */}
|
||||
<div className="h-4 w-24 bg-muted rounded" />
|
||||
{/* Timestamp skeleton */}
|
||||
<div className="h-3 w-16 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-4 w-32 bg-muted rounded" />
|
||||
)}
|
||||
</div>
|
||||
{/* Action buttons skeleton */}
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
<div className="w-4 h-4 bg-muted rounded" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content section */}
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{/* Text content skeleton - varied heights for realism */}
|
||||
<div className="space-y-2">
|
||||
<div className={cn("h-4 bg-muted rounded", index % 3 === 0 ? "w-full" : index % 3 === 1 ? "w-4/5" : "w-5/6")} />
|
||||
<div className={cn("h-4 bg-muted rounded", index % 2 === 0 ? "w-3/4" : "w-4/5")} />
|
||||
{index % 2 === 0 && <div className="h-4 w-2/3 bg-muted rounded" />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoSkeleton;
|
||||
|
|
@ -7,13 +7,14 @@ import { Button } from "@/components/ui/button";
|
|||
import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
import { Routes } from "@/router";
|
||||
import { memoStore, viewStore } from "@/store";
|
||||
import { memoStore, userStore, viewStore } from "@/store";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import Empty from "../Empty";
|
||||
import MasonryView, { MemoRenderContext } from "../MasonryView";
|
||||
import MemoEditor from "../MemoEditor";
|
||||
import MemoSkeleton from "../MemoSkeleton";
|
||||
|
||||
interface Props {
|
||||
renderer: (memo: Memo, context?: MemoRenderContext) => JSX.Element;
|
||||
|
|
@ -22,6 +23,7 @@ interface Props {
|
|||
orderBy?: string;
|
||||
filter?: string;
|
||||
pageSize?: number;
|
||||
showCreator?: boolean;
|
||||
}
|
||||
|
||||
const PagedMemoList = observer((props: Props) => {
|
||||
|
|
@ -55,6 +57,13 @@ const PagedMemoList = observer((props: Props) => {
|
|||
});
|
||||
|
||||
setNextPageToken(response?.nextPageToken || "");
|
||||
|
||||
// Batch-fetch creators in parallel to avoid individual fetches in MemoView
|
||||
// This significantly improves perceived performance by pre-populating the cache
|
||||
if (response?.memos && props.showCreator) {
|
||||
const uniqueCreators = Array.from(new Set(response.memos.map((memo) => memo.creator)));
|
||||
await Promise.allSettled(uniqueCreators.map((creator) => userStore.getOrFetchUserByName(creator)));
|
||||
}
|
||||
} finally {
|
||||
setIsRequesting(false);
|
||||
}
|
||||
|
|
@ -134,6 +143,14 @@ const PagedMemoList = observer((props: Props) => {
|
|||
|
||||
const children = (
|
||||
<div className="flex flex-col justify-start items-start w-full max-w-full">
|
||||
{/* Show skeleton loader during initial load */}
|
||||
{isRequesting && sortedMemoList.length === 0 ? (
|
||||
<>
|
||||
{showMemoEditor && <MemoEditor className="mb-2" cacheKey="home-memo-editor" />}
|
||||
<MemoSkeleton showCreator={props.showCreator} count={6} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MasonryView
|
||||
memoList={sortedMemoList}
|
||||
renderer={props.renderer}
|
||||
|
|
@ -141,7 +158,7 @@ const PagedMemoList = observer((props: Props) => {
|
|||
listMode={viewStore.state.layout === "LIST"}
|
||||
/>
|
||||
|
||||
{/* Loading indicator */}
|
||||
{/* Loading indicator for pagination */}
|
||||
{isRequesting && (
|
||||
<div className="w-full flex flex-row justify-center items-center my-4">
|
||||
<LoaderIcon className="animate-spin text-muted-foreground" />
|
||||
|
|
@ -163,6 +180,8 @@ const PagedMemoList = observer((props: Props) => {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ const Explore = observer(() => {
|
|||
)
|
||||
}
|
||||
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
|
||||
showCreator
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -194,8 +194,11 @@ const userStore = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
const { settings } = await userServiceClient.listUserSettings({ parent: state.currentUser });
|
||||
const { shortcuts } = await shortcutServiceClient.listShortcuts({ parent: state.currentUser });
|
||||
// Fetch settings and shortcuts in parallel for better performance
|
||||
const [{ settings }, { shortcuts }] = await Promise.all([
|
||||
userServiceClient.listUserSettings({ parent: state.currentUser }),
|
||||
shortcutServiceClient.listShortcuts({ parent: state.currentUser }),
|
||||
]);
|
||||
|
||||
// Extract and store each setting type
|
||||
const generalSetting = settings.find((s) => s.generalSetting)?.generalSetting;
|
||||
|
|
|
|||
Loading…
Reference in New Issue