Skip to content

Commit 5846bd7

Browse files
authored
Perf: improve rendering performance of Notes with big number of Notes (#357)
perf: improve rendering performance of Notes with big number of Notes
1 parent f13225c commit 5846bd7

File tree

11 files changed

+259
-105
lines changed

11 files changed

+259
-105
lines changed

src/components/CodeSyncProvider/CodeSyncProvider.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface ICodeSyncContext {
1919
sourceVersion: string,
2020
targetVersion: string,
2121
): Promise<ISelectionParams | null>;
22+
isSynctexLoaded: boolean;
2223
}
2324

2425
const BLOCK_MATCHING_TOLERANCE_AS_FRACTION_OF_PAGE_WIDTH = 0.00375;
@@ -196,6 +197,8 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {
196197
[synctexStore, texStore],
197198
);
198199

200+
const isSynctexLoaded = !!synctexData;
201+
199202
const context = useMemo(
200203
() => ({
201204
getSynctexBlockAtLocation,
@@ -204,6 +207,7 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {
204207
getSectionTitleAtSynctexBlock,
205208
getSubsectionTitleAtSynctexBlock,
206209
migrateSelection,
210+
isSynctexLoaded,
207211
}),
208212
[
209213
getSynctexBlockAtLocation,
@@ -212,6 +216,7 @@ export function CodeSyncProvider({ children }: PropsWithChildren) {
212216
getSectionTitleAtSynctexBlock,
213217
getSubsectionTitleAtSynctexBlock,
214218
migrateSelection,
219+
isSynctexLoaded,
215220
],
216221
);
217222

src/components/NoteManager/NoteManager.tsx

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import { cn } from "@fluffylabs/shared-ui";
55
import { twMerge } from "tailwind-merge";
66
import { useLatestCallback } from "../../hooks/useLatestCallback";
77
import { type ILocationContext, LocationContext } from "../LocationProvider/LocationProvider";
8-
import { type INotesContext, NotesContext } from "../NotesProvider/NotesProvider";
98
import type { IDecoratedNote } from "../NotesProvider/types/DecoratedNote";
109
import type { IStorageNote } from "../NotesProvider/types/StorageNote";
1110
import { areSelectionsEqual } from "../NotesProvider/utils/areSelectionsEqual";
1211
import { type ISelectionContext, SelectionContext } from "../SelectionProvider/SelectionProvider";
1312
import { InactiveNoteSkeleton } from "./components/InactiveNoteSkeleton";
1413
import { NewNote } from "./components/NewNote";
1514
import { NotesList } from "./components/NotesList";
15+
import { useNoteManagerNotes } from "./useNoteManagerNotes";
1616

1717
const DEFAULT_AUTHOR = "";
1818

@@ -28,29 +28,34 @@ const MemoizedNotesList = memo(NotesList);
2828

2929
function Notes() {
3030
const { locationParams, setLocationParams } = useContext(LocationContext) as ILocationContext;
31-
const { notesReady, activeNotes, notes, handleAddNote, handleDeleteNote, handleUpdateNote } = useContext(
32-
NotesContext,
33-
) as INotesContext;
31+
const {
32+
notesManagerNotes: notes,
33+
activeNotes,
34+
latestHandleAddNote,
35+
sectionTitlesLoaded,
36+
notesReady,
37+
deleteNote,
38+
updateNote,
39+
} = useNoteManagerNotes();
3440
const { selectedBlocks, pageNumber, handleClearSelection } = useContext(SelectionContext) as ISelectionContext;
3541
const keepShowingNewNote = useRef<{ selectionEnd: ISynctexBlockId; selectionStart: ISynctexBlockId }>(undefined);
36-
37-
const latestHandleAddNote = useLatestCallback(handleAddNote);
38-
const latestDeleteNote = useLatestCallback(handleDeleteNote);
39-
const latestUpdateNote = useLatestCallback(handleUpdateNote);
42+
const latestHandleClearSelection = useLatestCallback(handleClearSelection);
4043

4144
const memoizedHandleDeleteNote = useCallback(
4245
(note: IDecoratedNote) => {
43-
latestDeleteNote.current(note);
44-
handleClearSelection();
46+
deleteNote(note);
47+
latestHandleClearSelection.current();
4548
},
46-
[handleClearSelection, latestDeleteNote],
49+
[latestHandleClearSelection, deleteNote],
4750
);
4851

4952
const memoizedHandleUpdateNote = useCallback(
5053
(note: IDecoratedNote, newNote: IStorageNote) => {
51-
latestUpdateNote.current(note, newNote);
54+
// NOTE(optimistic): intentional mutation for immediate UI feedback; be aware this bypasses immutability
55+
note.original.content = newNote.content;
56+
updateNote(note, newNote);
5257
},
53-
[latestUpdateNote],
58+
[updateNote],
5459
);
5560

5661
const handleNewNoteCancel = useCallback(() => {
@@ -117,22 +122,24 @@ function Notes() {
117122
[],
118123
);
119124

120-
const isActiveNotes = notes.some((note) => activeNotes.has(note));
125+
const isActiveNotes = notes.some((note) => activeNotes.has(note.noteObject));
126+
127+
const readyAndLoaded = notesReady && sectionTitlesLoaded;
121128

122129
useEffect(() => {
123-
if (notesReady) {
130+
if (readyAndLoaded) {
124131
keepShowingNewNote.current = undefined;
125132
}
126-
}, [notesReady]);
133+
}, [readyAndLoaded]);
127134

128135
return (
129-
<div className={cn("note-manager flex flex-col gap-2.5", !notesReady && "opacity-30 pointer-events-none")}>
136+
<div className={cn("note-manager flex flex-col gap-2.5", !readyAndLoaded && "opacity-30 pointer-events-none")}>
130137
{locationParams.selectionEnd &&
131138
locationParams.selectionStart &&
132139
pageNumber !== null &&
133140
selectedBlocks.length > 0 &&
134141
!isActiveNotes &&
135-
(notesReady || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
142+
(readyAndLoaded || areSelectionsEqual(locationParams, keepShowingNewNote.current)) && (
136143
<NewNote
137144
selectionStart={locationParams.selectionStart}
138145
selectionEnd={locationParams.selectionEnd}
@@ -142,7 +149,7 @@ function Notes() {
142149
/>
143150
)}
144151

145-
{!notesReady && notes.length === 0 && (
152+
{!readyAndLoaded && notes.length === 0 && (
146153
<>
147154
<InactiveNoteSkeleton />
148155
<InactiveNoteSkeleton />
@@ -151,7 +158,9 @@ function Notes() {
151158
</>
152159
)}
153160

154-
{notesReady && notes.length === 0 && <div className="no-notes text-sidebar-foreground">No notes available</div>}
161+
{readyAndLoaded && notes.length === 0 && (
162+
<div className="no-notes text-sidebar-foreground">No notes available</div>
163+
)}
155164

156165
{notes.length > 0 && (
157166
<MemoizedNotesList

src/components/NoteManager/components/NewNote.tsx

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { ISynctexBlockId } from "@fluffylabs/links-metadata";
22
import { Button } from "@fluffylabs/shared-ui";
3-
import { type ChangeEvent, type ChangeEventHandler, useCallback, useMemo, useRef, useState } from "react";
3+
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
44
import { useLatestCallback } from "../../../hooks/useLatestCallback";
55
import { validateMath } from "../../../utils/validateMath";
6+
import { CodeSyncContext, type ICodeSyncContext } from "../../CodeSyncProvider/CodeSyncProvider";
67
import { type IDecoratedNote, NoteSource } from "../../NotesProvider/types/DecoratedNote";
78
import type { INoteV3 } from "../../NotesProvider/types/StorageNote";
89
import type { ISingleNoteContext } from "./NoteContext";
@@ -18,7 +19,6 @@ type NewNoteProps = {
1819
};
1920

2021
export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEnd }: NewNoteProps) => {
21-
const [noteContent, setNoteContent] = useState("");
2222
const [noteContentError, setNoteContentError] = useState<string | null>(null);
2323
const [labels, setLabels] = useState<string[]>(["local"]);
2424

@@ -30,6 +30,8 @@ export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEn
3030
}, [latestOnCancel]);
3131

3232
const handleSaveClick = useCallback(() => {
33+
const noteContent = textAreaRef.current?.value ?? "";
34+
3335
const mathValidationError = validateMath(noteContent);
3436
if (mathValidationError) {
3537
setNoteContentError(mathValidationError);
@@ -41,25 +43,19 @@ export const NewNote = ({ version, onCancel, onSave, selectionStart, selectionEn
4143
}
4244

4345
latestOnSave.current({ noteContent, labels });
44-
}, [noteContent, latestOnSave, labels]);
46+
}, [latestOnSave, labels]);
4547

4648
const currentVersionLink = "";
4749
const originalVersionLink = "";
4850

49-
const handleNoteContentChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
50-
setNoteContent(event.target.value);
51-
setNoteContentError(null);
52-
}, []);
53-
54-
const noteDirty = useDumbDirtyNoteObj({ noteContent, labels, version });
51+
const noteDirty = useDumbDirtyNoteObj({ labels, version });
5552
const note = useDumbNoteObj({ version, selectionStart, selectionEnd });
5653

5754
const noteLayoutContext = useNewNoteLayoutContext({
5855
note,
5956
noteDirty,
6057
currentVersionLink,
6158
handleCancelClick,
62-
handleNoteContentChange,
6359
handleNoteLabelsChange: setLabels,
6460
handleSaveClick,
6561
originalVersionLink,
@@ -140,51 +136,76 @@ const useDumbNoteObj = ({
140136
);
141137

142138
const useDumbDirtyNoteObj = ({
143-
noteContent,
144139
labels,
145140
version,
146141
}: {
147-
noteContent: string;
148142
labels: string[];
149143
version: string;
150144
}) =>
151145
useMemo(
152146
() =>
153147
({
154148
author: "",
155-
content: noteContent,
149+
content: "",
156150
labels,
157151
date: 0,
158152
noteVersion: 3,
159153
selectionEnd: createDumbISyntexBlockId(),
160154
selectionStart: createDumbISyntexBlockId(),
161155
version,
162156
}) satisfies INoteV3,
163-
[noteContent, version, labels],
157+
[version, labels],
164158
);
165159

166160
const useNewNoteLayoutContext = ({
167161
handleSaveClick,
168162
handleCancelClick,
169163
note,
170164
noteDirty,
171-
handleNoteContentChange,
172165
handleNoteLabelsChange,
173166
currentVersionLink,
174167
originalVersionLink,
168+
selectionEnd,
169+
selectionStart,
175170
}: {
176171
handleSaveClick: () => void;
177172
handleCancelClick: () => void;
178173
note: IDecoratedNote;
179174
noteDirty: INoteV3;
180-
handleNoteContentChange: ChangeEventHandler<HTMLTextAreaElement>;
181175
handleNoteLabelsChange: (labels: string[]) => void;
182176
currentVersionLink: string;
183177
originalVersionLink: string;
184178
selectionStart: ISynctexBlockId;
185179
selectionEnd: ISynctexBlockId;
186-
}) =>
187-
useMemo(
180+
}) => {
181+
const { getSectionTitleAtSynctexBlock, getSubsectionTitleAtSynctexBlock } = useContext(
182+
CodeSyncContext,
183+
) as ICodeSyncContext;
184+
185+
const [sectionTitles, setSectionTitles] = useState<{ sectionTitle: string; subSectionTitle: string }>({
186+
sectionTitle: "",
187+
subSectionTitle: "",
188+
});
189+
190+
useEffect(() => {
191+
let cancelled = false;
192+
(async () => {
193+
if (selectionStart && selectionEnd) {
194+
const newSectionTitle = (await getSectionTitleAtSynctexBlock(selectionStart)) ?? "";
195+
const newSubSectionTitle = (await getSubsectionTitleAtSynctexBlock(selectionStart)) ?? "";
196+
if (cancelled) return;
197+
setSectionTitles({
198+
sectionTitle: newSectionTitle,
199+
subSectionTitle: newSubSectionTitle,
200+
});
201+
}
202+
})();
203+
return () => {
204+
cancelled = true;
205+
};
206+
}, [selectionStart, selectionEnd, getSectionTitleAtSynctexBlock, getSubsectionTitleAtSynctexBlock]);
207+
208+
const context = useMemo(
188209
() =>
189210
({
190211
active: true,
@@ -196,21 +217,24 @@ const useNewNoteLayoutContext = ({
196217
isEditing: true,
197218
note,
198219
noteDirty,
199-
handleNoteContentChange,
200220
handleNoteLabelsChange,
201221
handleSelectNote: () => {},
202222
noteOriginalVersionShort: "",
203223
currentVersionLink,
204224
originalVersionLink,
225+
sectionTitles,
205226
}) satisfies ISingleNoteContext,
206227
[
207228
noteDirty,
208-
handleNoteContentChange,
209229
handleNoteLabelsChange,
210230
note,
211231
handleCancelClick,
212232
handleSaveClick,
213233
currentVersionLink,
214234
originalVersionLink,
235+
sectionTitles,
215236
],
216237
);
238+
239+
return context;
240+
};

0 commit comments

Comments
 (0)