mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
4.1 KiB
4.1 KiB
Spy acceleration
Replace O(N) DOM scans in session view
Summary
The session scroll-spy currently scans the DOM with querySelectorAll and walks message nodes, which becomes expensive as message count grows. We’ll replace the scan with an observer-based or indexed approach that scales smoothly.
Goals
- Remove repeated full DOM scans during scroll in the session view
- Keep “current message” tracking accurate during streaming and layout shifts
- Provide a safe fallback path for older browsers and edge cases
Non-goals
- Visual redesign of the session page
- Changing message rendering structure or IDs
- Perfect accuracy during extreme layout thrash
Current state
packages/app/src/pages/session.tsxusesquerySelectorAll('[data-message-id]')for scroll-spy.- The page is large and handles many responsibilities, increasing the chance of perf regressions.
Proposed approach
Implement a two-tier scroll-spy:
- Primary:
IntersectionObserverto track which message elements are visible, updated incrementally. - Secondary: binary search over precomputed offsets when observer is unavailable or insufficient.
- Use
ResizeObserver(and a lightweight “dirty” flag) to refresh offsets only when layout changes.
Phased implementation steps
- Extract a dedicated scroll-spy module
- Create
packages/app/src/pages/session/scroll-spy.ts(or similar) that exposes:register(el, id)andunregister(id)getActiveId()signal/store
- Keep DOM operations centralized and easy to profile.
- Add IntersectionObserver tracking
- Observe each
[data-message-id]element once, on mount. - Maintain a small map of
id -> intersectionRatio(or visible boolean). - Pick the active id by:
- highest intersection ratio, then
- nearest to top of viewport as a tiebreaker
- Add binary search fallback
- Maintain an ordered list of
{ id, top }positions. - On scroll (throttled via
requestAnimationFrame), compute target Y and binary search to find nearest message. - Refresh the positions list on:
- message list mutations (new messages)
- container resize events (ResizeObserver)
- explicit “layout changed” events after streaming completes
- Remove
querySelectorAllhot path
- Keep a one-time initial query only as a bridge during rollout, then remove it.
- Ensure newly rendered messages are registered via refs rather than scanning the whole DOM.
- Add a feature flag and fallback
- Add
session.scrollSpyOptimizedflag. - If observer setup fails, fall back to the existing scan behavior temporarily.
Data migration / backward compatibility
- No persisted data changes.
- IDs remain sourced from existing
data-message-idattributes.
Risk + mitigations
- Risk: observer ordering differs from previous “active message” logic.
- Mitigation: keep selection rules simple, document them, and add a small tolerance for tie cases.
- Risk: layout shifts cause incorrect offset indexing.
- Mitigation: refresh offsets with ResizeObserver and after message streaming batches.
- Risk: performance regressions from observing too many nodes.
- Mitigation: prefer one observer instance and avoid per-node observers.
Validation plan
- Manual scenarios:
- very long sessions (hundreds of messages) and continuous scrolling
- streaming responses that append content and change heights
- resizing the window and toggling side panels
- Add a dev-only profiler hook to log time spent in scroll-spy updates per second.
Rollout plan
- Land extracted module first, still using the old scan internally.
- Add observer implementation behind
session.scrollSpyOptimizedoff by default. - Enable flag for internal testing, then default on after stability.
- Keep fallback code for one release cycle, then remove scan path.
Open questions
- What is the exact definition of “active” used elsewhere (URL hash, sidebar highlight, breadcrumb)?
- Are messages virtualized today, or are all DOM nodes mounted at once?
- Which container is the scroll root (window vs an inner div), and does it change by layout mode?