Skip to content

Commit 51e925b

Browse files
committed
optimize & preload curate
1 parent dd4f982 commit 51e925b

4 files changed

Lines changed: 72 additions & 21 deletions

File tree

apps/extension/src/Curate/Curate.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,18 @@ import {
3333
} from "@phosphor-icons/react";
3434
import { ClockIcon, LightningBoltIcon } from "@radix-ui/react-icons";
3535
import { AnimatePresence, motion } from "framer-motion";
36-
import { ReactNode, useEffect, useMemo, useRef, useState } from "react";
36+
import { ReactNode, useEffect, useRef, useState } from "react";
3737
import { useHotkeys } from "react-hotkeys-hook";
3838

3939
import { AnimatedNumberBadge } from "../components/AnimatedNumberBadge";
4040
import { SavedTab } from "../models";
41-
import { bookmarkStoreActions, useBookmarkStore } from "../store/bookmarkStore";
41+
import { bookmarkStoreActions } from "../store/bookmarkStore";
4242
import {
4343
curateStoreActions,
4444
InclusionReason,
4545
InclusionResult,
4646
useCurateStore,
47+
useCurateTabsById,
4748
} from "../store/curateStore";
4849
import { pluralize } from "../util";
4950
import { getUtcISO } from "../util/date";
@@ -54,6 +55,7 @@ import CurateSummary from "./CurateSummary";
5455
import Ruler from "./Ruler";
5556
import SwipeableCard, { Direction, SwipeableRef } from "./SwipeableCard";
5657
import TagList from "./TagList";
58+
import { useIsMounted } from "usehooks-ts";
5759

5860
interface Props {
5961
children?: ReactNode;
@@ -131,20 +133,13 @@ export function CurateTrigger({ children }: { children: ReactNode }) {
131133
export default function Curate({ children, maxCards = 5, curateQueueItems }: Props) {
132134
const open = useCurateStore((s) => s.open);
133135
const settings = useCurateStore((s) => s.settings);
134-
const tabs = useBookmarkStore((s) => s.tabs);
135136

136137
const { initialized, total, visibleQueue: queue, allQueue, kept, deleted, left, dequeue, unshift } = useCurateQueue(
137138
maxCards,
138139
curateQueueItems,
139140
);
140141

141-
const curateTabsById = useMemo(() => {
142-
const curateTabIds = new Set(curateQueueItems.map((result) => result.tabId));
143-
144-
return Object.fromEntries(
145-
tabs.filter((tab) => curateTabIds.has(tab.id)).map((tab) => [tab.id, tab]),
146-
);
147-
}, [tabs, curateQueueItems]);
142+
const curateTabsById = useCurateTabsById(curateQueueItems);
148143

149144
const swipeableRef = useRef<SwipeableRef | null>(null);
150145

@@ -306,6 +301,7 @@ export default function Curate({ children, maxCards = 5, curateQueueItems }: Pro
306301
constrained
307302
className={cn(
308303
"focus-ring absolute rounded-2xl perspective-dramatic transform-3d",
304+
{ 'will-change-auto': i < 3 }
309305
)}
310306
style={{
311307
zIndex: remap(i, 0, maxCards, maxCards, 0),

apps/extension/src/NavMenu.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,30 @@ import { BroomIcon } from "@phosphor-icons/react";
1919

2020
import { NumberNotificationBadge } from "./components/NumberNotificationBadge";
2121
import { Curate, CurateTrigger } from "./Curate";
22-
import { useCurateQueue, useCurateStore } from "./store/curateStore";
22+
import { useCurateQueue, useCurateStore, useCurateTabsById } from "./store/curateStore";
2323
import { settingStoreActions, useSettingStore } from "./store/settingStore";
24+
import { usePreloadTabInfo } from "./TabInfo/queries";
2425

2526
export default function NavMenu() {
2627
const queue = useCurateQueue();
2728
const open = useCurateStore((s) => s.open);
2829
const settingsOpen = useSettingStore((s) => s.open);
2930
const showCurateCount = queue.length > 10;
3031

32+
const curateTabsById = useCurateTabsById(queue);
33+
const preloadTabInfo = usePreloadTabInfo();
34+
35+
const handlePreloadCurate = () => {
36+
const firstQueueItem = queue[0];
37+
if (!firstQueueItem) return;
38+
39+
const tab = curateTabsById[firstQueueItem.tabId];
40+
if (tab) {
41+
preloadTabInfo({ tabId: tab.id, url: tab.url });
42+
}
43+
};
44+
45+
3146
return (
3247
<div className="flex gap-2">
3348
<Curate key={String(open)} curateQueueItems={queue}>
@@ -38,6 +53,8 @@ export default function NavMenu() {
3853
className="pointer-events-none">
3954
<CurateTrigger>
4055
<ButtonWithTooltip
56+
onMouseOver={handlePreloadCurate}
57+
onFocus={handlePreloadCurate}
4158
tooltipText="Curate"
4259
variant="outline"
4360
size="icon"

apps/extension/src/TabInfo/queries.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useQuery } from "@tanstack/react-query";
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
22

33
import type { TabMetadata } from "./models";
44
import { getTabInfoService } from "./TabInfoService";
@@ -9,18 +9,33 @@ interface UseTabInfoQueryOptions {
99
enabled?: boolean;
1010
}
1111

12+
async function fetchTabInfo({ tabId, url }: { tabId?: string; url: string }) {
13+
const service = getTabInfoService();
14+
const response = await service.fetchMetadata({ tabId, url });
15+
16+
if (!response.success) {
17+
throw new Error(response.error || "Failed to fetch metadata");
18+
}
19+
20+
return response.metadata || null;
21+
}
22+
23+
export function usePreloadTabInfo() {
24+
const queryClient = useQueryClient();
25+
26+
return ({ tabId, url }: { tabId?: string; url: string }) => {
27+
queryClient.prefetchQuery({
28+
queryKey: ["tab-info", url],
29+
queryFn: () => fetchTabInfo({ tabId, url }),
30+
});
31+
};
32+
}
33+
1234
export function useTabInfoQuery({ tabId, url, enabled = true }: UseTabInfoQueryOptions) {
1335
return useQuery<TabMetadata | null, Error>({
1436
queryKey: ["tab-info", url],
1537
queryFn: async () => {
16-
const service = getTabInfoService();
17-
const response = await service.fetchMetadata({ tabId, url });
18-
19-
if (!response.success) {
20-
throw new Error(response.error || "Failed to fetch metadata");
21-
}
22-
23-
return response.metadata || null;
38+
return fetchTabInfo({ tabId, url });
2439
},
2540
enabled: enabled && Boolean(url),
2641
staleTime: 1000 * 60 * 60,

apps/extension/src/store/curateStore.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { toast } from "@echotab/ui/Toast";
22
import { differenceInDays, differenceInMonths, differenceInWeeks } from "date-fns";
33
import dayjs from "dayjs";
4+
import { memoize } from "proxy-memoize";
45
import { useMemo } from "react";
56
import { create } from "zustand";
67
import { subscribeWithSelector } from "zustand/middleware";
@@ -9,7 +10,7 @@ import { getUtcISO } from "../util/date";
910
import { createLogger } from "../util/Logger";
1011
import { useBookmarkStore } from "./bookmarkStore";
1112
import { StoragePersistence } from "./persistence";
12-
import { Tag } from "./schema";
13+
import { SavedTab, Tag } from "./schema";
1314
import { unassignedTag, useTagsById } from "./tagStore";
1415

1516
const logger = createLogger("CurateStore");
@@ -217,6 +218,28 @@ export const useCurateQueue = ({ manualIds = [] }: { manualIds?: string[] } = {}
217218
return queue;
218219
};
219220

221+
const selectCurateTabsById = memoize(
222+
(state: { tabs: SavedTab[]; curateTabIds: Set<string> }): Record<string, SavedTab> => {
223+
const { tabs, curateTabIds } = state;
224+
const result: Record<string, SavedTab> = {};
225+
for (const tab of tabs) {
226+
if (curateTabIds.has(tab.id)) {
227+
result[tab.id] = tab;
228+
}
229+
}
230+
return result;
231+
},
232+
);
233+
234+
export const useCurateTabsById = (curateQueueItems: InclusionResult[]) => {
235+
const tabs = useBookmarkStore((s) => s.tabs);
236+
const curateTabIds = useMemo(
237+
() => new Set(curateQueueItems.map((result) => result.tabId)),
238+
[curateQueueItems],
239+
);
240+
return selectCurateTabsById({ tabs, curateTabIds });
241+
};
242+
220243
export const initStore = async () => {
221244
const stored = await persistence.load();
222245
if (stored) {

0 commit comments

Comments
 (0)