diff --git a/web/src/components/MemoSkeleton.tsx b/web/src/components/MemoSkeleton.tsx
new file mode 100644
index 000000000..59435a6c1
--- /dev/null
+++ b/web/src/components/MemoSkeleton.tsx
@@ -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) => (
+
+ {/* Header section */}
+
+
+ {showCreator ? (
+
+ {/* Avatar skeleton */}
+
+
+ {/* Creator name skeleton */}
+
+ {/* Timestamp skeleton */}
+
+
+
+ ) : (
+
+ )}
+
+ {/* Action buttons skeleton */}
+
+
+
+ {/* Content section */}
+
+ {/* Text content skeleton - varied heights for realism */}
+
+
+
+ {index % 2 === 0 &&
}
+
+
+
+ ))}
+ >
+ );
+};
+
+export default MemoSkeleton;
diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx
index bc04fcc3a..5eb716616 100644
--- a/web/src/components/PagedMemoList/PagedMemoList.tsx
+++ b/web/src/components/PagedMemoList/PagedMemoList.tsx
@@ -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,33 +143,43 @@ const PagedMemoList = observer((props: Props) => {
const children = (
-
: undefined}
- listMode={viewStore.state.layout === "LIST"}
- />
-
- {/* Loading indicator */}
- {isRequesting && (
-
-
-
- )}
-
- {/* Empty state or back-to-top button */}
- {!isRequesting && (
+ {/* Show skeleton loader during initial load */}
+ {isRequesting && sortedMemoList.length === 0 ? (
<>
- {!nextPageToken && sortedMemoList.length === 0 ? (
-
-
-
{t("message.no-data")}
-
- ) : (
-
-
+ {showMemoEditor &&
}
+
+ >
+ ) : (
+ <>
+
: undefined}
+ listMode={viewStore.state.layout === "LIST"}
+ />
+
+ {/* Loading indicator for pagination */}
+ {isRequesting && (
+
+
)}
+
+ {/* Empty state or back-to-top button */}
+ {!isRequesting && (
+ <>
+ {!nextPageToken && sortedMemoList.length === 0 ? (
+
+
+
{t("message.no-data")}
+
+ ) : (
+
+
+
+ )}
+ >
+ )}
>
)}
diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx
index bcc41f6ec..bf551024b 100644
--- a/web/src/pages/Explore.tsx
+++ b/web/src/pages/Explore.tsx
@@ -30,6 +30,7 @@ const Explore = observer(() => {
)
}
orderBy={viewStore.state.orderByTimeAsc ? "display_time asc" : "display_time desc"}
+ showCreator
/>
diff --git a/web/src/store/user.ts b/web/src/store/user.ts
index 518bd62cd..1e656fbaa 100644
--- a/web/src/store/user.ts
+++ b/web/src/store/user.ts
@@ -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;