diff --git a/src/components/CalendarHeader.vue b/src/components/CalendarHeader.vue index f82fa43..1ddb646 100644 --- a/src/components/CalendarHeader.vue +++ b/src/components/CalendarHeader.vue @@ -96,8 +96,8 @@ const weekdayNames = 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') +})