Skip to content

perf: recurring event date expansion creates thousands of Date objects per render #87

@laughable-9

Description

@laughable-9

Problem

Calendar views expand recurring events by iterating day-by-day from start_date to end_date, creating a new Date object for each day. This happens on every render in components that don't memoize the transformation.

Affected files

  • `sroapp/src/pages/Dashboard.jsx` (lines 102-124) — NOT memoized
  • `sroapp/src/components/ui/UnifiedActivitiesCalendar.jsx` (lines 83-124) — NOT memoized
  • `sroapp/src/pages/admin/AdminPanel.jsx` (lines 159-178) — already wrapped in `useMemo` ✅

The expensive loop

```javascript
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
const dayName = d.toLocaleDateString('en-US', { weekday: 'long' });
if (recurringDays[dayName]) {
events.push({
...activity,
date: new Date(d).toISOString().split('T')[0],
// ... more fields
});
}
}
```

Impact

  • A recurring event spanning 6 months (180 days) creates 180 Date objects + 180 string conversions
  • With 100 recurring activities: 18,000+ Date objects created per render
  • On old phones (the target demographic — UP students with budget devices), this causes visible lag when navigating to the dashboard or calendar page
  • The loop also mutates the Date object in-place (`d.setDate()`), which is a subtle correctness risk

Proposed fix

Wrap the event expansion in `useMemo` in both `Dashboard.jsx` and `UnifiedActivitiesCalendar.jsx`, keyed on the activities data array.

Dashboard.jsx

The `fetchActivities` effect already stores the result in state. The event expansion should be a `useMemo` that depends on the activities state, not computed inline during the effect.

UnifiedActivitiesCalendar.jsx

Same approach — the component receives activities as a prop, so the expansion should be:
```javascript
const expandedEvents = useMemo(() => {
return activities.flatMap(activity => expandRecurring(activity));
}, [activities]);
```

Optionally, replace the day-by-day loop with `date-fns/eachDayOfInterval` for cleaner code and better performance:
```javascript
import { eachDayOfInterval, format, getDay } from 'date-fns';

const days = eachDayOfInterval({ start, end });
const matchingDays = days.filter(d => recurringDays[format(d, 'EEEE')]);
```

Why we deferred this

The fix requires refactoring the event transformation logic in 2 files. `Dashboard.jsx` mixes the expansion with data fetching in a `useEffect`, so it needs to be separated into a `useMemo` that runs after the data is stored in state. `UnifiedActivitiesCalendar.jsx` has a similar structure.

The `AdminPanel.jsx` version (the most-used admin view) is already memoized, so the impact is primarily on the student Dashboard and the shared calendar component. These pages are less frequently loaded than the admin dashboard.

Not a breaking issue — just causes slower renders on low-end devices with many recurring activities.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions