perf: memoize TimeTrackingContext provider, isolate 30s timer tick#237
Merged
Conversation
Deploying timetrackerpro with
|
| 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 |
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.
f1bb374 to
cc4f9be
Compare
This was referenced 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Related Issue
Addresses P0 items in #219 (not closing — issue also tracks P1/P2 items not covered here:
React.memoon list rows, bundle chunking, per-call map rebuilding, PWA icon size)Changes Made
src/contexts/TimeTrackingContext.tsx— providervalueobject wrapped inuseMemo; every previously-unwrapped handler (startDay,endDay,updateTask,addProject,addClient,updateArchivedDay, etc. — ~40 total) wrapped inuseCallbackwith correct dependency arrays (verified viareact-hooks/exhaustive-deps, zero warnings). Ref-based mutations (project/client/planned-task CRUD, which already synced viaprojectsRef/clientsRef/plannedTasksRef) got empty-dep callbacks; handlers that read live state (tasks,currentTask,dayStartTime, etc.) got those as explicit deps.currentTimestate + its 30ssetIntervalremoved from the context entirely, along with thegetTotalDayDuration/getCurrentTaskDurationcontext methods that depended on it.src/hooks/useCurrentTime.ts(new) — small local hook, its ownuseState+ 30s interval. Each caller owns an independent timer instead of sharing one through the mega-context.src/utils/calculationUtils.ts— added puregetCurrentTaskDuration(currentTask, now)andgetTotalDayDuration(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 callsuseCurrentTime()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
General
mainpnpm lintandpnpm build(did not runpnpm 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/CardforwardRef inTaskItem) shows up but predates this change and isn't touched by it.pnpm lint(react-hooks/exhaustive-depsincluded) andpnpm buildboth pass clean. Fullpnpm testsuite was not run per this session's instructions — flagging in case you want it run before merge given the size of this refactor.