From be81acb9e7e058cb83602236f7a989794a9178c3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 10 Jan 2026 16:11:15 +0200 Subject: [PATCH 1/4] feat(tree): respect motion settings instead of always disabling animation --- apps/client/src/widgets/note_tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 8f4ead199..b70421ba3 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -353,7 +353,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { this.$tree.fancytree({ titlesTabbable: true, keyboard: true, - toggleEffect: false, + toggleEffect: options.is("motionEnabled") ? undefined : false, extensions: ["dnd5", "clones", "filter"], source: treeData, scrollOfs: { From ddba0e823c11b52acd042ce308e9e7ada707b274 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 10 Jan 2026 17:07:02 +0200 Subject: [PATCH 2/4] fix(tree): tree is updated on note content updates --- apps/client/src/widgets/note_tree.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index b70421ba3..1844bf963 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1163,10 +1163,18 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const { movedActiveNode, parentsOfAddedNodes } = await this.#processBranchRows(branchRows, refreshCtx); for (const noteId of loadResults.getNoteIds()) { + const contentReloaded = loadResults.isNoteContentReloaded(noteId); + if (contentReloaded && !loadResults.isNoteReloaded(noteId, contentReloaded.componentId)) { + // Only the note content was reloaded, not the note itself. This would cause a redundant update on every few seconds while editing a note. + continue; + } + refreshCtx.noteIdsToUpdate.add(noteId); } - await this.#executeTreeUpdates(refreshCtx, loadResults); + if (refreshCtx.noteIdsToUpdate.size + refreshCtx.noteIdsToReload.size > 0) { + await this.#executeTreeUpdates(refreshCtx, loadResults); + } await this.#setActiveNode(activeNotePath, activeNodeFocused, movedActiveNode, parentsOfAddedNodes); From 0272189b22f3c91174aac50ce63100be29e9dd89 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 10 Jan 2026 17:24:34 +0200 Subject: [PATCH 3/4] fix(tree): performance issue due to batch update --- apps/client/src/widgets/note_tree.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 1844bf963..87d4e39e2 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -1306,7 +1306,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) { - await this.batchUpdate(async () => { + const batchUpdate = async () => { for (const noteId of refreshCtx.noteIdsToReload) { for (const node of this.getNodesByNoteId(noteId)) { await node.load(true); @@ -1322,7 +1322,14 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } } } - }); + }; + + if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= 10) { + // Despite the name, batchUpdate is actually slowing things down for smaller updates. + await this.batchUpdate(batchUpdate); + } else { + await batchUpdate(); + } // for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered) for (const noteId of refreshCtx.noteIdsToUpdate) { From 52d4083814b6cdd0abe893ccca5d07b699b40e21 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 10 Jan 2026 17:46:45 +0200 Subject: [PATCH 4/4] chore(tree): address requested changes --- apps/client/src/widgets/note_tree.ts | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index 87d4e39e2..3d0cf503f 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -153,7 +153,7 @@ const TPL = /*html*/` const MAX_SEARCH_RESULTS_IN_TREE = 100; // this has to be hanged on the actual elements to effectively intercept and stop click event -const cancelClickPropagation: (e: JQuery.ClickEvent | MouseEvent) => void = (e) => e.stopPropagation(); +const cancelClickPropagation: (e: Event) => void = (e) => e.stopPropagation(); // TODO: Fix once we remove Node.js API from public type Timeout = NodeJS.Timeout | string | number | undefined; @@ -190,6 +190,9 @@ export interface DragData { export const TREE_CLIPBOARD_TYPE = "application/x-fancytree-node"; +/** Entity changes below the given threshold will be processed without batching to avoid performance degradation. */ +const BATCH_UPDATE_THRESHOLD = 10; + export default class NoteTreeWidget extends NoteContextAwareWidget { private $tree!: JQuery; private $treeActions!: JQuery; @@ -1306,7 +1309,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } async #executeTreeUpdates(refreshCtx: RefreshContext, loadResults: LoadResults) { - const batchUpdate = async () => { + const performUpdates = async () => { for (const noteId of refreshCtx.noteIdsToReload) { for (const node of this.getNodesByNoteId(noteId)) { await node.load(true); @@ -1324,11 +1327,16 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } }; - if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= 10) { - // Despite the name, batchUpdate is actually slowing things down for smaller updates. - await this.batchUpdate(batchUpdate); + if (refreshCtx.noteIdsToReload.size + refreshCtx.noteIdsToUpdate.size >= BATCH_UPDATE_THRESHOLD) { + /** + * Batch updates are used for large number of updates to prevent multiple re-renders, however in the context of small updates (such as changing a note title) + * it can cause up to 400ms of delay for ~8k notes which is not acceptable. Therefore we use batching only for larger number of updates. + * Without batching, the updates would take a couple of milliseconds. + * We still keep the batching for potential cases where there are many updates, for example in a sync. + */ + await this.batchUpdate(performUpdates); } else { - await batchUpdate(); + await performUpdates(); } // for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered) @@ -1808,7 +1816,6 @@ function buildEnhanceTitle() { const createChildTemplate = document.createElement("span"); createChildTemplate.className = "tree-item-button tn-icon add-note-button bx bx-plus"; createChildTemplate.title = t("note_tree.create-child-note"); - createChildTemplate.addEventListener("click", cancelClickPropagation); return async function enhanceTitle(event: Event, data: { @@ -1859,7 +1866,9 @@ function buildEnhanceTitle() { && !note.isLaunchBarConfig() && !note.noteId.startsWith("_help") ) { - node.span.append(createChildTemplate.cloneNode()); + const createChildItem = createChildTemplate.cloneNode(); + createChildItem.addEventListener("click", cancelClickPropagation); + node.span.append(createChildItem); } if (isHoistedNote) {