Skip to content

Fix tab-switch rendering latency and optimize SVG processing - #46

Merged
marota merged 10 commits into
mainfrom
claude/optimize-network-rendering-4anLl
Mar 10, 2026
Merged

marota merged 10 commits into
mainfrom
claude/optimize-network-rendering-4anLl

Conversation

@marota

@marota marota commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR addresses critical rendering performance issues when switching between diagram tabs on large networks (11k+ lines). The main problems were a blocking useLayoutEffect in usePanZoom that prevented the browser from painting, and inefficient DOM queries during highlight application. The fix includes dependency array corrections, DOM caching, Web Worker offloading for SVG processing, and several micro-optimizations.

Key Changes

Critical Performance Fixes

  • Fix blocking useLayoutEffect in usePanZoom: Added [initialViewBox] dependency array so the hook only runs when a new diagram loads, not on every render. This was preventing the browser from painting the new tab until all layout effects completed.
  • Add useLayoutEffect for viewBox sync on tab activation: Ensures the SVG displays the correct zoom state before the first paint when switching tabs.
  • Move SVG processing to Web Worker: Offloads expensive DOMParser/XMLSerializer operations to a background thread via processSvgAsync, preventing UI blocking during diagram load.

DOM Query Optimizations

  • Implement cached ID map (getIdMap/invalidateIdMapCache): Replaces repeated querySelectorAll('[id]') calls with a WeakMap-backed cache that rebuilds only when SVG content changes. Eliminates ~3-7ms of scanning per highlight operation.
  • Cache getScreenCTM() calls: Within a single highlight pass, reuse the background layer's CTM instead of recalculating it for each element.
  • Add SVG boost cache: LRU-style cache for boosted SVG strings (keyed by length + first 200 chars + vlCount) to avoid re-running DOMParser on identical diagrams.

Tab Synchronization & State Management

  • Change tab sync to useLayoutEffect: Ensures viewBox is applied before paint, preventing visual glitches when switching tabs.
  • Parallel diagram prefetching: Start fetching action variant diagrams in parallel with simulation results to reduce perceived latency.
  • Fix state merge regression: Preserve pdf_url and other fields when merging analysis results by spreading previous state first (...prev, ...pending).

Code Quality & Testing

  • Add comprehensive test coverage: New tests for getIdMap, applyOverloadedHighlights, applyDeltaVisuals, usePanZoom hook behavior, and merge logic.
  • Improve code comments: Document performance-critical sections (CSS containment, stroke scaling, useLayoutEffect timing).
  • Add documentation: New markdown files explaining rendering optimization strategy and action/topology management.

Implementation Details

  • CSS containment: Added contain: layout style paint to .svg-container to tell the browser this subtree's layout is independent, reducing work during viewBox changes.
  • Stroke scaling fix: Ensured SVG strokes use vector-effect="non-scaling-stroke" to remain visible at all zoom levels without breaking pypowsybl native colors.
  • Worker fallback: processSvgAsync gracefully falls back to synchronous processing if workers are unavailable.
  • Cache invalidation: DOM caches are explicitly invalidated when diagram content changes via useEffect hooks on diagram state changes.

Performance Impact

On a France network (11,225 lines, 500+ voltage levels):

  • Tab switching latency reduced from 1-3s to near-instant (paint happens before highlights apply)
  • Highlight application: ~50-200ms saved via DOM caching and CTM reuse
  • SVG processing: Moved off main thread, no longer blocks UI during diagram load

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT

claude and others added 10 commits March 9, 2026 20:10
- Move SVG processing (boostSvgForLargeGrid) to a Web Worker so
  DOMParser/XMLSerializer work doesn't block the main thread
- Only apply highlights and voltage range filter to the active tab;
  defer inactive tabs and catch up on tab switch
- Cache the DOM id-map (querySelectorAll('[id]')) per container with
  WeakMap, invalidated when SVG content changes, avoiding repeated
  O(n) scans on every highlight/filter application

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
- Port id-map caching (getIdMap/invalidateIdMapCache) to standalone_interface.html
- Port deferred highlights and voltage filter (active-tab-only updates
  with stale tracking and catch-up on tab switch) to standalone_interface.html
- Add 16 new tests: getIdMap caching and invalidation, applyOverloadedHighlights,
  applyDeltaVisuals (delta class application, text replacement, restoration),
  and processSvgAsync worker client

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
…llel fetch

Three performance improvements for large grids (11K+ lines):

1. Deferred tab-switch decorations: Highlights and voltage filter now use
   requestAnimationFrame when triggered by a tab switch, letting the browser
   paint the new tab instantly before doing DOM decoration work. Data changes
   still apply synchronously. Removed the separate catch-up effect to avoid
   redundant double-application.

2. boostSvgForLargeGrid cache: DOMParser→mutate→XMLSerializer is expensive
   on large SVGs. Results are now cached by (length + first 200 chars + vlCount)
   so revisiting a tab with the same diagram skips the boost entirely.

3. Parallel manual simulation: handleAddManualAction now fires the diagram
   fetch in parallel with the simulation API call, instead of sequentially.
   When simulation completes, the pre-fetched diagram is used directly.

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
Root cause analysis revealed 5 bottlenecks in the post-paint decoration
pipeline on large grids (11K+ lines). Fixes applied to both standalone
interface and React frontend:

1. useLayoutEffect in usePanZoom had NO dependency array, running 3x per
   render and BLOCKING paint. Added [initialViewBox] dep so it only runs
   when a new diagram loads.

2. Voltage filter iterated 33,675 style.display writes even when the kV
   range covered all voltages (default case). Added early-return check.

3. applyDeltaVisuals ran 4 querySelectorAll cleanup scans on the entire
   SVG even when no deltas had been applied. Added data-deltas-applied
   flag to skip cleanup when unnecessary.

4. getScreenCTM() on the background layer forced layout recalculation on
   every highlight call. Cached the bgCTM per highlight pass.

5. applyOverloadedHighlights and highlight functions used querySelector
   for element lookups. Switched to getIdMap() for O(1) cached lookups.

6. Deferred highlights and voltage filter to requestAnimationFrame on tab
   switch (vs synchronous on data change) in both standalone and React.

Also saved the optimization analysis as docs/rendering-optimization-plan.md.

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
…useLayoutEffect

- Fix A: Remove global vector-effect: non-scaling-stroke CSS (caused thick
  lines at zoom-out and per-element screen-coordinate paint cost). Kept only
  for .nad-action-target halo circles/rects.
- Fix B: Move applyViewBox() from useEffect to useLayoutEffect([active]) in
  usePanZoom so the viewBox is correct before the browser paints on tab switch.
- Fix C: Change zoom sync from useEffect to useLayoutEffect([activeTab]) in
  both standalone_interface.html and App.tsx so target tab renders with the
  correct viewport on the first frame.

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
handleDisplayPrioritizedActions was replacing the entire result with
pendingAnalysisResult, losing pdf_url (set by a separate streaming event).
Fix: spread prev first (...prev, ...pendingAnalysisResult) so existing
fields like pdf_url are preserved when pendingAnalysisResult doesn't
include them.

Also adds tests for:
- usePanZoom hook (viewBox sync, active/inactive transitions, useLayoutEffect)
- mergeAnalysisResult logic (pdf_url preservation, manual action merging)
- VisualizationPanel overflow tab visibility across result updates

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
Restores `vector-effect: non-scaling-stroke` on SVG paths/lines/polylines/rects
that was removed in 6d03b24. Without it:
- Lines become subpixel at full zoom-out, making native pypowsybl colors invisible
- Zooming in makes strokes extremely thick, causing expensive rendering and lag
- Tab switches trigger heavy repaint with variable-width strokes

Also adds `contain: layout style paint` to standalone .svg-container to match
the React frontend, telling the browser the SVG subtree is independent.

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
New tests (23 new, 136 total):
- cssRegression.test.ts: Verifies critical CSS rules (non-scaling-stroke,
  CSS containment, text-hidden, highlight styles, delta styles) are present
  in both App.css and standalone_interface.html. Also checks usePanZoom
  useLayoutEffect deps, voltage filter early-return, deferred highlights,
  and boost cache.
- usePanZoom.test.tsx: Adds text visibility toggle tests (hidden at
  zoom-out, visible at zoom-in, no toggle on non-large grids) and rapid
  tab switching regression test.

Documentation:
- docs/rendering-optimization-plan.md: Comprehensive documentation of all
  rendering features (non-scaling-stroke, CSS containment, text culling,
  usePanZoom architecture, tab-switch optimization, highlight caching,
  voltage filter short-circuit, SVG boost cache) with rationale, regression
  risks, and do's/don'ts.

Also excludes test files from tsconfig.app.json build to allow Node API
imports in test files.

https://claude.ai/code/session_018qY95WeqFBdiVQeT541YcT
@marota
marota merged commit 2929882 into main Mar 10, 2026
1 of 2 checks passed
@marota marota mentioned this pull request Mar 10, 2026
10 of 11 tasks
marota pushed a commit that referenced this pull request Apr 30, 2026
Rewrites Phase 1 from the abstract CHANGELOG summary into a six-week
chronology grounded in PR descriptions:

- 1.1 Bootstrap (pre-#1) — scaffolding pushed direct to main
- 1.2 End-to-end loop (PRs #1–#15) — NAD scaling, multi-tab pan/zoom,
  manual actions, action variants, action scoring, svgUtils + usePanZoom
- 1.3 UX maturation (PRs #16–#24) — settings panel, OverloadPanel,
  monitoring factor, action filters, state-switching SLDs
- 1.4 Robustness + CI (PRs #25–#46) — backend/frontend test suites,
  CircleCI + GitHub Actions, lazy loading, divergence UI
- 1.5 Persistence + complex actions (PRs #47–#56) — save/reload sessions,
  combined actions w/ persistence, islanding MW, App.tsx Phase 1 hooks
- 1.6 Two-step + replay logging + rebrand (PRs #57–#65) — split analysis,
  config.json persistence, load shedding, MW Start, SLD highlights,
  InteractionLogger, ExpertAssist → Co-Study4Grid

Sections 2–4 left untouched (will be reconciled in a follow-up).

https://claude.ai/code/session_01Pg7fuCUG2edfm5PyHS6SbN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants