Calendar view lazily updated instead of reflectivity, for improved performance.

This commit is contained in:
Leo Vasanko 2025-08-24 21:18:21 -06:00
parent cb7a111020
commit 50c79ff99f
3 changed files with 77 additions and 13 deletions

View File

@ -114,22 +114,49 @@ const todayString = computed(() => {
return formatTodayString(d)
})
const visibleWeeks = computed(() => {
// PERFORMANCE: Maintain a manual cache of computed weeks instead of relying on
// deep reactive tracking of every event & day object. We rebuild lazily when
// (a) scrolling changes the needed range or (b) eventsMutation counter bumps.
const visibleWeeks = ref([])
let lastScrollRange = { startVW: null, endVW: null }
let pendingRebuild = false
function scheduleRebuild(reason) {
if (pendingRebuild) return
pendingRebuild = true
// Use requestIdleCallback when available, else fallback to rAF
const cb = () => {
pendingRebuild = false
rebuildVisibleWeeks(reason)
}
if ('requestIdleCallback' in window) {
requestIdleCallback(cb, { timeout: 120 })
} else {
requestAnimationFrame(cb)
}
}
function rebuildVisibleWeeks(reason) {
const buffer = 10
const startIdx = Math.floor((scrollTop.value - buffer * rowHeight.value) / rowHeight.value)
const endIdx = Math.ceil(
(scrollTop.value + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value,
)
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
const weeks = []
for (let vw = startVW; vw <= endVW; vw++) {
weeks.push(createWeek(vw))
if (
reason === 'scroll' &&
lastScrollRange.startVW === startVW &&
lastScrollRange.endVW === endVW &&
visibleWeeks.value.length
) {
return
}
return weeks
})
const weeks = []
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
visibleWeeks.value = weeks
lastScrollRange = { startVW, endVW }
}
const contentHeight = computed(() => {
return totalVirtualWeeks.value * rowHeight.value
@ -448,9 +475,8 @@ function calculateSelection(anchorStr, otherStr) {
}
const onScroll = () => {
if (viewport.value) {
scrollTop.value = viewport.value.scrollTop
}
if (viewport.value) scrollTop.value = viewport.value.scrollTop
scheduleRebuild('scroll')
}
const handleJogwheelScrollTo = (newScrollTop) => {
@ -473,6 +499,9 @@ onMounted(() => {
calendarStore.updateCurrentDate()
}, 60000)
// Initial build after mount & measurement
scheduleRebuild('init')
onBeforeUnmount(() => {
clearInterval(timer)
})
@ -532,9 +561,25 @@ watch(
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
scrollTop.value = newScroll
if (viewport.value) viewport.value.scrollTop = newScroll
scheduleRebuild('first-day-change')
})
},
)
// Watch lightweight mutation counter only (not deep events map) and rebuild lazily
watch(
() => calendarStore.events,
() => {
scheduleRebuild('events')
},
{ deep: true },
)
// Rebuild if viewport height changes (e.g., resize)
window.addEventListener('resize', () => {
if (viewport.value) viewportHeight.value = viewport.value.clientHeight
scheduleRebuild('resize')
})
</script>
<template>

View File

@ -15,6 +15,9 @@ export const useCalendarStore = defineStore('calendar', {
today: toLocalString(new Date(), DEFAULT_TZ),
now: new Date().toISOString(),
events: new Map(),
// Lightweight mutation counter so views can rebuild in a throttled / idle way
// without tracking deep reactivity on every event object.
eventsMutation: 0,
weekend: getLocaleWeekendDays(),
_holidayConfigSignature: null,
_holidaysInitialized: false,
@ -107,6 +110,14 @@ export const useCalendarStore = defineStore('calendar', {
return 'e-' + Math.random().toString(36).slice(2, 10) + '-' + Date.now().toString(36)
},
notifyEventsChanged() {
// Bump simple counter (wrapping to avoid overflow in extreme long sessions)
this.eventsMutation = (this.eventsMutation + 1) % 1_000_000_000
},
touchEvents() {
this.notifyEventsChanged()
},
createEvent(eventData) {
const singleDay = eventData.startDate === eventData.endDate
const event = {
@ -125,6 +136,7 @@ export const useCalendarStore = defineStore('calendar', {
isRepeating: eventData.repeat && eventData.repeat !== 'none',
}
this.events.set(event.id, { ...event, isSpanning: event.startDate < event.endDate })
this.notifyEventsChanged()
return event.id
},
@ -155,6 +167,7 @@ export const useCalendarStore = defineStore('calendar', {
deleteEvent(eventId) {
this.events.delete(eventId)
this.notifyEventsChanged()
},
deleteFirstOccurrence(baseId) {
@ -186,6 +199,7 @@ export const useCalendarStore = defineStore('calendar', {
base.endDate = newEndStr
if (numericCount !== Infinity) base.repeatCount = String(Math.max(1, numericCount - 1))
this.events.set(baseId, { ...base, isSpanning: base.startDate < base.endDate })
this.notifyEventsChanged()
},
deleteSingleOccurrence(ctx) {
@ -231,6 +245,7 @@ export const useCalendarStore = defineStore('calendar', {
repeatCount: remainingCount,
repeatWeekdays: snapshot.repeatWeekdays,
})
this.notifyEventsChanged()
},
deleteFromOccurrence(ctx) {
@ -242,6 +257,7 @@ export const useCalendarStore = defineStore('calendar', {
return
}
this._terminateRepeatSeriesAtIndex(baseId, occurrenceIndex)
this.notifyEventsChanged()
},
setEventRange(eventId, newStartStr, newEndStr, { mode = 'auto' } = {}) {
@ -283,6 +299,7 @@ export const useCalendarStore = defineStore('calendar', {
}
}
this.events.set(eventId, { ...snapshot, isSpanning: snapshot.startDate < snapshot.endDate })
this.notifyEventsChanged()
},
splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) {
@ -359,6 +376,7 @@ export const useCalendarStore = defineStore('calendar', {
repeatCount: remainingCount,
repeatWeekdays,
})
this.notifyEventsChanged()
},
splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr) {
@ -395,6 +413,7 @@ export const useCalendarStore = defineStore('calendar', {
const rc = parseInt(ev.repeatCount, 10)
if (!isNaN(rc)) ev.repeatCount = String(Math.min(rc, index))
}
this.notifyEventsChanged()
},
},
persist: {