Skip to content

perf: memoize TimeTrackingContext provider, isolate 30s timer tick#237

Merged
AdamJ merged 1 commit into
mainfrom
perf/219-p0-context-memoization
Jul 3, 2026
Merged

perf: memoize TimeTrackingContext provider, isolate 30s timer tick#237
AdamJ merged 1 commit into
mainfrom
perf/219-p0-context-memoization

Conversation

@AdamJ

@AdamJ AdamJ commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the two P0 items from #219 (performance audit): unmemoized provider value causing app-wide re-render fan-out, and a shared 30s timer tick forcing every page to re-render regardless of relevance.

Type of Change

  • Update to existing feature (performance)

Related Issue

Addresses P0 items in #219 (not closing — issue also tracks P1/P2 items not covered here: React.memo on list rows, bundle chunking, per-call map rebuilding, PWA icon size)

Changes Made

  • src/contexts/TimeTrackingContext.tsx — provider value object wrapped in useMemo; every previously-unwrapped handler (startDay, endDay, updateTask, addProject, addClient, updateArchivedDay, etc. — ~40 total) wrapped in useCallback with correct dependency arrays (verified via react-hooks/exhaustive-deps, zero warnings). Ref-based mutations (project/client/planned-task CRUD, which already synced via projectsRef/clientsRef/plannedTasksRef) got empty-dep callbacks; handlers that read live state (tasks, currentTask, dayStartTime, etc.) got those as explicit deps.
  • currentTime state + its 30s setInterval removed from the context entirely, along with the getTotalDayDuration/getCurrentTaskDuration context methods that depended on it.
  • src/hooks/useCurrentTime.ts (new) — small local hook, its own useState + 30s interval. Each caller owns an independent timer instead of sharing one through the mega-context.
  • src/utils/calculationUtils.ts — added pure getCurrentTaskDuration(currentTask, now) and getTotalDayDuration(tasks, currentTask, now), taking "now" as a parameter instead of reading shared state.
  • src/components/AppSidebar.tsx, src/components/Navigation.tsx, src/pages/Index.tsx — the only three consumers that display a live running duration; each now calls useCurrentTime() locally and the new pure calc functions. Every other page (Clients, Categories, Projects, Archive, Settings, Kanban/TaskList) no longer re-renders on the 30s tick.

Checklist

Documentation

  • N/A — no README changes

General

  • Branch is up to date with main
  • No unrelated files included in this PR
  • Tested locally by invoking the pnpm lint and pnpm build (did not run pnpm test — not requested this session)

Notes for Reviewers

This touches the most-imported context in the app, so I manually walked the golden path in a running dev server rather than relying on lint/build alone: start day → add task → end day → post to archive → Archive page → Kanban board → Settings, no crashes, no new console errors. One pre-existing unrelated console warning (motion/Card forwardRef in TaskItem) shows up but predates this change and isn't touched by it.

pnpm lint (react-hooks/exhaustive-deps included) and pnpm build both pass clean. Full pnpm test suite was not run per this session's instructions — flagging in case you want it run before merge given the size of this refactor.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying timetrackerpro with  Cloudflare Pages  Cloudflare Pages

Latest commit: cc4f9be
Status: ✅  Deploy successful!
Preview URL: https://27b0992e.timetrackerpro.pages.dev
Branch Preview URL: https://perf-219-p0-context-memoizat.timetrackerpro.pages.dev

View logs

@AdamJ AdamJ added this to Timetraked Jul 3, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Timetraked Jul 3, 2026
@AdamJ AdamJ moved this from Backlog to In review in Timetraked Jul 3, 2026
Every consumer of useTimeTracking() re-rendered on any state change
anywhere in the 1700-line context because the provider value was a
fresh object literal each render with mostly unmemoized handlers.
Wrap the value in useMemo and every handler in useCallback so
identity only changes when the relevant state does.

Also pull the 30s currentTime tick out of the context entirely —
it forced a full-tree re-render every 30s regardless of whether a
page showed a running duration. AppSidebar/Navigation/Index (the
only consumers of a live duration) now use a local useCurrentTime
hook plus pure getTotalDayDuration/getCurrentTaskDuration helpers;
every other page no longer re-renders on the timer tick.
@AdamJ AdamJ force-pushed the perf/219-p0-context-memoization branch from f1bb374 to cc4f9be Compare July 3, 2026 20:03
@AdamJ AdamJ merged commit 104d2f5 into main Jul 3, 2026
3 checks passed
@AdamJ AdamJ deleted the perf/219-p0-context-memoization branch July 3, 2026 20:06
@github-project-automation github-project-automation Bot moved this from In review to Done in Timetraked Jul 3, 2026
AdamJ added a commit that referenced this pull request Jul 3, 2026
…uilds

Addresses the P1 findings from #219: TaskItem, ArchiveItem, and
PlannedTaskCard now wrapped in React.memo so an unrelated update in
one row doesn't re-render every other row in the list — this only
pays off now that #237 made the context's mutation handlers stable
via useCallback. Archive.tsx's handleEdit was still a fresh closure
per render, which would have defeated ArchiveItem's new memo, so
it's wrapped in useCallback too.

vite.config.ts now has build.rollupOptions.output.manualChunks
splitting recharts, @supabase/supabase-js, @radix-ui, motion, and
the react-markdown dependency chain into their own vendor chunks.
Drops the main bundle from ~1MB to ~296KB and clears the "chunk
larger than 500kB" build warning entirely.

Index.tsx's billable/non-billable hours calculation was calling
getBillableHoursForDay/getNonBillableHoursForDay per archived day in
two separate reduce() passes, each rebuilding projectMap/categoryMap
from scratch — now a single pass using getDayStats with maps built
once via useMemo.

Skipped: extracting+memoizing ClientManagement.tsx's inline client
rows. Unlike TaskItem/ArchiveItem/PlannedTaskCard, ClientManagement
renders its list inline (no existing row component) and isn't in a
hot re-render loop like the dashboard/kanban board — the refactor
cost doesn't match the payoff here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant