Refactor ChatView into memoized UI subcomponents#58
Conversation
- Extract header, error, approvals, and timeline into memoized components - Memoize `ChatMarkdown`, pickers, and handlers to reduce avoidable re-renders - Reduce running-time ticker frequency from 250ms to 1000ms
WalkthroughWrapped ChatMarkdown with React.memo. Refactored ChatView by extracting inline UI into multiple memoized subcomponents (ChatHeader, ThreadErrorBanner, PendingApprovalsPanel, MessagesTimeline, ModelPicker, ReasoningEffortPicker, OpenInPicker), adjusted callback wiring and timer interval. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant ChatView as ChatView
participant Header as ChatHeader
participant Timeline as MessagesTimeline
participant Native as NativeApi
User->>ChatView: interact (send message / open image / pick model)
ChatView->>Header: render header, actions (onToggleDiff, OpenInPicker)
Header->>ChatView: emit action callbacks
ChatView->>Timeline: render messages, handle image expand / work-group toggle
Timeline->>Native: request image data / interaction
ChatView->>Native: session-scoped actions (respondToApproval, ensureSession)
Native-->>ChatView: responses / confirmations
ChatView-->>User: UI updates
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
Refactor
|
Greptile SummaryThis PR refactors the monolithic
Confidence Score: 3/5
Important Files Changed
Flowchartflowchart TD
CV[ChatView] -->|"title, project, diffOpen"| CH[ChatHeader]
CV -->|"error string"| TEB[ThreadErrorBanner]
CV -->|"approvals, respondingIds, onRespond"| PAP[PendingApprovalsPanel]
CV -->|"activeThread, timelineEntries, nowIso, ..."| MT[MessagesTimeline]
MT -->|"text"| CM[ChatMarkdown]
CV -->|"model, onModelChange"| MP[ModelPicker]
CV -->|"effort, onEffortChange"| REP[ReasoningEffortPicker]
CH -->|"keybindings"| OIP[OpenInPicker]
CH -->|"api, gitCwd"| GAC[GitActionsControl]
style CH fill:#2d6a4f,stroke:#40916c
style TEB fill:#2d6a4f,stroke:#40916c
style PAP fill:#e76f51,stroke:#f4a261
style MT fill:#e76f51,stroke:#f4a261
style CM fill:#2d6a4f,stroke:#40916c
style MP fill:#2d6a4f,stroke:#40916c
style REP fill:#2d6a4f,stroke:#40916c
style OIP fill:#2d6a4f,stroke:#40916c
style GAC fill:#264653,stroke:#2a9d8f
Last reviewed commit: bc7eae3 |
| setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId)); | ||
| } | ||
| }, | ||
| [activeThread?.id, activeThread?.session, api, dispatch], |
There was a problem hiding this comment.
activeThread?.session defeats memo on PendingApprovalsPanel
activeThread?.session is a ProviderSession object that gets replaced with a new reference on every provider event (via evolveSession in the store reducer). Since this callback only uses activeThread.session.sessionId (a string), the dependency should be narrowed to activeThread?.session?.sessionId. As-is, onRespondToApproval is recreated on every incoming event, which means PendingApprovalsPanel's memo wrapper is never effective during active streaming — exactly when memoization matters most.
| [activeThread?.id, activeThread?.session, api, dispatch], | |
| [activeThread?.id, activeThread?.session?.sessionId, api, dispatch], |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/web/src/components/ChatView.tsx (2)
500-508: Timer interval increase may affect streaming UX smoothness.The interval change from 250ms to 1000ms reduces render frequency, but elapsed time displays during streaming (used in
formatMessageMetaon lines 1599-1607) will now update only once per second instead of four times. This may make the elapsed time appear to "jump" rather than count smoothly.Consider whether this trade-off aligns with UX requirements, particularly for longer-running responses where users watch the elapsed time.
Based on learnings: "Preserve fast time-to-first-delta and smooth streaming in chat message updates."
1548-1556: Non-null assertion is safe but could be cleaner.The
image.previewUrl!assertion on line 1554 is safe because it's inside the truthy branch ofimage.previewUrl ?. TypeScript doesn't narrow types inside inline arrow functions, necessitating the assertion.An alternative would be to capture the value before the callback:
♻️ Optional: Avoid non-null assertion
- {image.previewUrl ? ( + {image.previewUrl ? (() => { + const previewUrl = image.previewUrl; + return ( <img src={image.previewUrl} alt={image.name} className="h-full max-h-[220px] w-full cursor-zoom-in object-cover" onClick={() => - onImageExpand({ src: image.previewUrl!, name: image.name }) + onImageExpand({ src: previewUrl, name: image.name }) } /> + ); + })() : ( - ) : (
- Derive `activeSessionId` and `activeThreadId` before approval callbacks - Avoid stale thread/session captures when submitting approval decisions - Pass `hasMessages` to `MessagesTimeline` instead of the full thread
Desktop Dev Perf Trace
Interaction Run
Input Event Metrics
Scheduler/Event Hotspots
Threshold Check
Heap Counters
Top User Timing Marks
Top Duration Events
|
Not sure why the compiler isn't doing it's magic but this manual memoization shows significant perf wins...
Before vs after (old 00:17 trace -> new 00:49 trace):
keypress avg: 24.66ms -> 3.60ms (-85.4%)
textInput avg: 24.58ms -> 3.50ms (-85.8%)
input avg: 23.97ms -> 2.99ms (-87.5%)
worst keypress spike: 68.73ms -> 5.50ms (-92.0%)
total EventDispatch time: 6467ms -> 816ms (-87.4%)
dispatchDiscreteEvent time: 2116ms -> 245ms (-88.4%)
EventDispatch >= 50ms: 24 -> 0
Summary
ChatViewinto focused memoized subcomponents (ChatHeader,ThreadErrorBanner,PendingApprovalsPanel,MessagesTimeline) to reduce render churn and improve maintainability.ChatMarkdown,ModelPicker,ReasoningEffortPicker,OpenInPicker).useCallback(approval responses, diff toggle, work-group expand, image expand) to support memoization.Testing
Summary by CodeRabbit