Skip to content

Commit 5d1e727

Browse files
committed
perf: implement chunked API loading to prevent fetching all notes on scroll
The previous implementation fetched all 3,633 note metadata records at once on initial load, even though only 50 were displayed. When scrolling triggered pagination, the UI would hang while processing all notes. This change implements proper chunked loading using the existing backend chunkSize and chunkCursor API parameters: - Initial load fetches only first 50 notes - Scrolling triggers incremental fetches of 50 notes at a time - Cursor is stored in sync state to track pagination position - Notes are updated incrementally using existing store actions This prevents the UI hang and significantly improves performance for users with large note collections.
1 parent 65bf3d6 commit 5d1e727

File tree

4 files changed

+98
-49
lines changed

4 files changed

+98
-49
lines changed

src/App.vue

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -130,36 +130,40 @@ export default {
130130
},
131131
132132
methods: {
133-
loadNotes() {
134-
fetchNotes()
135-
.then(data => {
136-
if (data === null) {
137-
// nothing changed
138-
return
139-
}
140-
if (data && data.noteIds) {
141-
this.error = false
142-
// Route to default note (lastViewedNote not available with chunked API)
143-
// Users will need to manually select a note
144-
this.routeDefault(0)
145-
} else if (this.loading.notes) {
146-
// only show error state if not loading in background
147-
this.error = data?.errorMessage || true
148-
} else {
149-
console.error('Server error while updating list of notes: ' + (data?.errorMessage || 'Unknown error'))
150-
}
151-
})
152-
.catch((err) => {
133+
async loadNotes() {
134+
try {
135+
// Load only the first chunk on initial load (50 notes)
136+
// Subsequent chunks will be loaded on-demand when scrolling
137+
const data = await fetchNotes(50, null)
138+
139+
if (data === null) {
140+
// nothing changed (304 response)
141+
return
142+
}
143+
144+
if (data && data.noteIds) {
145+
this.error = false
146+
// Route to default note after first chunk
147+
this.routeDefault(0)
148+
149+
// Store cursor for next chunk (will be used by scroll handler)
150+
store.commit('setNotesChunkCursor', data.chunkCursor || null)
151+
} else if (this.loading.notes) {
153152
// only show error state if not loading in background
154-
if (this.loading.notes) {
155-
this.error = true
156-
}
157-
console.error('Failed to load notes:', err)
158-
})
159-
.then(() => {
160-
this.loading.notes = false
161-
this.startRefreshTimer(config.interval.notes.refresh)
162-
})
153+
this.error = data?.errorMessage || true
154+
} else {
155+
console.error('Server error while updating list of notes: ' + (data?.errorMessage || 'Unknown error'))
156+
}
157+
} catch (err) {
158+
// only show error state if not loading in background
159+
if (this.loading.notes) {
160+
this.error = true
161+
}
162+
console.error('Failed to load notes:', err)
163+
} finally {
164+
this.loading.notes = false
165+
this.startRefreshTimer(config.interval.notes.refresh)
166+
}
163167
},
164168
165169
startRefreshTimer(seconds) {

src/NotesService.js

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const getDashboardData = () => {
7373
})
7474
}
7575

76-
export const fetchNotes = async () => {
76+
export const fetchNotes = async (chunkSize = 50, chunkCursor = null) => {
7777
const lastETag = store.state.sync.etag
7878
const lastModified = store.state.sync.lastModified
7979
const headers = {}
@@ -95,25 +95,39 @@ export const fetchNotes = async () => {
9595
}
9696
}
9797

98-
// Load ALL notes metadata excluding content for performance
98+
// Load notes metadata in chunks excluding content for performance
9999
// Content is loaded on-demand when user selects a note
100100
const params = new URLSearchParams()
101101
if (lastModified) {
102102
params.append('pruneBefore', lastModified)
103103
}
104104
params.append('exclude', 'content') // Exclude heavy content field
105+
params.append('chunkSize', chunkSize.toString()) // Request chunked data
106+
if (chunkCursor) {
107+
params.append('chunkCursor', chunkCursor) // Continue from previous chunk
108+
}
105109

106110
const response = await axios.get(
107111
generateUrl('/apps/notes/api/v1/notes' + (params.toString() ? '?' + params.toString() : '')),
108112
{ headers },
109113
)
110114

111-
// Process all notes - API v1 returns array directly
112-
if (Array.isArray(response.data)) {
113-
const notes = response.data
114-
const noteIds = notes.map(note => note.id)
115-
116-
// Update all notes at once (content will be loaded on-demand)
115+
const data = response.data
116+
const notes = data.notes || []
117+
const noteIds = data.noteIds || notes.map(note => note.id)
118+
const nextCursor = data.chunkCursor || null
119+
const isLastChunk = !nextCursor
120+
121+
// Update notes incrementally
122+
if (chunkCursor) {
123+
// Subsequent chunk - use incremental update
124+
store.dispatch('updateNotesIncremental', { notes, isLastChunk })
125+
if (isLastChunk) {
126+
// Final chunk - clean up deleted notes
127+
store.dispatch('finalizeNotesUpdate', noteIds)
128+
}
129+
} else {
130+
// First chunk - use full update
117131
store.dispatch('updateNotes', { noteIds, notes })
118132
}
119133

@@ -122,7 +136,11 @@ export const fetchNotes = async () => {
122136
store.commit('setSyncLastModified', response.headers['last-modified'])
123137
store.commit('setNotesLoadingInProgress', false)
124138

125-
return { noteIds: response.data.map(n => n.id) }
139+
return {
140+
noteIds,
141+
chunkCursor: nextCursor,
142+
isLastChunk,
143+
}
126144
} catch (err) {
127145
store.commit('setNotesLoadingInProgress', false)
128146
if (err?.response?.status === 304) {

src/components/NotesView.vue

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
NcTextField,
6969
} from '@nextcloud/vue'
7070
import { categoryLabel } from '../Util.js'
71+
import { fetchNotes } from '../NotesService.js'
7172
import NotesList from './NotesList.vue'
7273
import NotesCaption from './NotesCaption.vue'
7374
import store from '../store.js'
@@ -207,28 +208,48 @@ export default {
207208
}
208209
},
209210
210-
onEndOfNotes(isVisible) {
211+
async onEndOfNotes(isVisible) {
211212
// Prevent rapid-fire loading by checking if we're already loading a batch
212-
if (!isVisible || this.isLoadingMore || this.displayedNotesCount >= this.filteredNotes.length) {
213+
if (!isVisible || this.isLoadingMore) {
213214
return
214215
}
215216
216217
// Set loading flag to prevent concurrent loads
217218
this.isLoadingMore = true
218219
219-
// Use nextTick to ensure the loading flag is set before incrementing
220-
this.$nextTick(() => {
221-
// Load 50 more notes at a time
222-
this.displayedNotesCount = Math.min(
223-
this.displayedNotesCount + 50,
224-
this.filteredNotes.length
225-
)
220+
try {
221+
// Check if there are more notes to fetch from the server
222+
const chunkCursor = store.state.sync.chunkCursor
226223
227-
// Reset loading flag after DOM update
224+
if (chunkCursor) {
225+
// Fetch next chunk from the API
226+
const data = await fetchNotes(50, chunkCursor)
227+
228+
if (data && data.noteIds) {
229+
// Update cursor for next fetch
230+
store.commit('setNotesChunkCursor', data.chunkCursor || null)
231+
232+
// Increment display count to show newly loaded notes
233+
this.displayedNotesCount = Math.min(
234+
this.displayedNotesCount + 50,
235+
this.filteredNotes.length
236+
)
237+
}
238+
} else if (this.displayedNotesCount < this.filteredNotes.length) {
239+
// No more chunks to fetch, but we have cached notes to display
240+
this.$nextTick(() => {
241+
this.displayedNotesCount = Math.min(
242+
this.displayedNotesCount + 50,
243+
this.filteredNotes.length
244+
)
245+
})
246+
}
247+
} finally {
248+
// Reset loading flag after operation completes
228249
this.$nextTick(() => {
229250
this.isLoadingMore = false
230251
})
231-
})
252+
}
232253
},
233254
234255
onCategorySelected(category) {

src/store/sync.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const state = {
1010
etag: null,
1111
lastModified: 0,
1212
active: false,
13+
chunkCursor: null,
1314
// TODO add list of notes with changes during sync
1415
}
1516

@@ -43,11 +44,16 @@ const mutations = {
4344
clearSyncCache(state) {
4445
state.etag = null
4546
state.lastModified = 0
47+
state.chunkCursor = null
4648
},
4749

4850
setSyncActive(state, active) {
4951
state.active = active
5052
},
53+
54+
setNotesChunkCursor(state, cursor) {
55+
state.chunkCursor = cursor
56+
},
5157
}
5258

5359
const actions = {

0 commit comments

Comments
 (0)