Skip to content

perf(nad): lazyMount helper to defer off-screen viewer construction - #9

Draft
gautierbureau wants to merge 1 commit into
integration/nad_create_svg_adaptive_zoomfrom
claude/perf-nad-lazy-mount
Draft

perf(nad): lazyMount helper to defer off-screen viewer construction#9
gautierbureau wants to merge 1 commit into
integration/nad_create_svg_adaptive_zoomfrom
claude/perf-nad-lazy-mount

Conversation

@gautierbureau

@gautierbureau gautierbureau commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Context

Layered on the adaptive-zoom SVG-writer branch (PR powsybl#423 upstream, imported here as integration/nad_create_svg_adaptive_zoom). Last of three complementary performance PRs stacked on that branch.

Problem

A page that stacks several diagrams constructs all of them on load, including the off-screen ones. Each NAD costs real synchronous work on the main thread, so an N-viewer page pays ~N× that cost up front before the user can see anything.

Change

Add lazyMount(container, factory, options?): it defers factory until container is at (or near, via rootMargin, default 200px) the viewport, using an IntersectionObserver. Each viewer is built only when scrolled into view.

lazyMount(container, () => {
    new NetworkAreaDiagramViewer(container, svg, metadata, options);
});
  • factory runs at most once; the returned function cancels a still-pending mount.
  • Falls back to running factory immediately when IntersectionObserver is unavailable (non-DOM environments), so callers never branch on support.
  • Handles a factory that returns a promise (async construction) and routes sync/async errors to onError.
  • Gotcha documented on the helper: the container must reserve layout space (e.g. min-height); a zero-height box collapses into the viewport and mounts immediately, defeating the purpose.

Measured impact

Real headless Chromium, N case1354pegase viewers stacked in a one-viewport scroll container, eager (build all on load) vs lazy (IntersectionObserver, build on view) — via benchmarks/nad-lazy-mount-bench.mjs (PR #11):

construction path N per-viewer eager on load lazy on load saved ratio
raw SVG inject 8 ~41 ms 326 ms 81 ms (2 built) ~244 ms 4.0×
raw SVG inject 16 ~41 ms 658 ms 82 ms (2 built) ~575 ms 8.0×
powsybl#423 client writer 8 ~373 ms 2985 ms 746 ms (2 built) ~2.2 s 4.0×

Relationship to powsybl#423

This is the outer visibility gate — which whole viewers to build. It stacks cleanly with powsybl#423's inner gate — which elements to draw at the current zoom. Orthogonal axes of "don't build what you can't see"; no shared code, mergeable in any order.

Tests / surface

  • 5 unit tests: defer-until-intersecting, build-once, cancel, observer disconnect, sync + async error handling, and the no-observer fallback.
  • Exported from both entry points (@powsybl/network-viewer-core and the root package). Purely additive — no existing behaviour changes.

Draft, base is the in-progress adaptive-zoom branch.

🤖 Generated with Claude Code

A page that stacks several diagrams constructs them all on load, even the
off-screen ones; each NAD costs ~100 ms of synchronous first-paint work, so an
N-viewer page pays N× that up front. lazyMount(container, factory) defers the
factory until its container is (near) the viewport via an IntersectionObserver,
building each viewer only when scrolled into view. On a page showing ~1 of N,
initial construction work drops roughly N× and the first visible diagram
appears sooner.

The factory runs at most once; the returned function cancels a still-pending
mount. Falls back to running the factory immediately when IntersectionObserver
is unavailable (non-DOM environments), so callers never branch on support. The
container must reserve layout space or a zero-height box collapses into the
viewport and mounts right away (documented on the helper).

This is the outer visibility gate; it stacks with the adaptive-zoom writer's
per-zoom element gating (whole-viewer vs per-element deferral, orthogonal).
Exported from both entry points. 5 unit tests cover defer/build-once/cancel/
disconnect, sync + async error handling, and the no-observer fallback.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T1fMV9gtgHLBKoSAShYSdA
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