Skip to content

perf(viz): lazy-load SignalR via dynamic import (−54 KB main bundle) - #82

Merged
WomB0ComB0 merged 2 commits into
mainfrom
feat/defer-signalr
Apr 29, 2026
Merged

WomB0ComB0 merged 2 commits into
mainfrom
feat/defer-signalr

Conversation

@WomB0ComB0

@WomB0ComB0 WomB0ComB0 commented Apr 29, 2026

Copy link
Copy Markdown
Member

Summary

The main bundle has been creeping against the 800 KiB cap (98.8% after #79). The next non-trivial main-bundle feature would have failed CI. Defer @microsoft/signalr until start() runs.

Bundle delta

Asset Before After Δ
main 809.0 KB 753.6 KB −55 KB
esm (new lazy) 55.3 KB +55 KB
sensor 20.5 KB 20.5 KB 0
% of cap 98.8 % 92.0 % −6.8 pp

Mechanics

  • Eager import { HubConnectionBuilder, LogLevel } from '@microsoft/signalr'import type { HubConnection } (type-only, zero runtime cost).
  • connection is a module-scoped let HubConnection | null, initialized inside start() via await import('@microsoft/signalr'). Subsequent reconnects reuse the cached module AND the same HubConnection instance.
  • Handler registration moved into _wireConnection(c) so it runs once after the lazy build.

Cost

~50 ms one-time chunk fetch on first start(). Overlaps with the existing async boot path (_autoSpawnIfEmpty, terrain loading) so user-visible startup time is unchanged in practice.

Test plan

  • npm run build passes; main bundle 753.6 KB, well under 819.2 KB cap
  • tsc --noEmit clean
  • Browser smoke test: reload / and confirm SignalR connection comes up; check Network tab for the deferred esm-*.js chunk
  • CI: client-budget reports 92% of budget (or whatever measure step hits)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Optimized real-time communication initialization by deferring module loading to first use instead of application startup.

The main bundle has been creeping up against the 800 KiB cap (98.8 %
after PR #79) and the next non-trivial feature would have failed CI.
Defer the `@microsoft/signalr` runtime until `start()` runs.

- Replace eager `import { HubConnectionBuilder, LogLevel } from
  '@microsoft/signalr'` with `import type { HubConnection }` (type-only,
  zero runtime cost).
- Make `connection` a module-scoped `let` initialised inside `start()`,
  via `await import('@microsoft/signalr')`. First start triggers a
  separate chunk fetch; subsequent reconnects reuse the cached module
  AND the same `HubConnection` instance.
- Wrap handler registration in `_wireConnection(c)` so it runs once
  after the lazy build, before the first `connection.start()` call.

Bundle delta:
  main:   809.0 → 753.6 KB  (−55 KB; 98.8 % → 92.0 % of cap)
  esm:        — →  55.3 KB  (new lazy chunk for SignalR)
  sensor:  20.5 →  20.5 KB  (unchanged)

The first paint no longer blocks on parsing 55 KB of SignalR; the
~50 ms chunk fetch overlaps with the existing async boot path
(`_autoSpawnIfEmpty`, terrain loading) so user-visible startup time
is unchanged in practice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@WomB0ComB0 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 53 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4420f738-1e32-4cdc-b2d4-c8e079e3a524

📥 Commits

Reviewing files that changed from the base of the PR and between ff4b695 and 6546185.

📒 Files selected for processing (1)
  • CLAUDE.md
📝 Walkthrough

Walkthrough

SignalR initialization has been refactored from eager loading at module startup to lazy loading on first start() call. Event handlers are now wired through a dedicated _wireConnection() helper function, and the runtime module is imported dynamically, improving startup performance and deferring dependencies.

Changes

Cohort / File(s) Summary
SignalR Lazy Initialization
src/ResQ.Viz.Web/client/app.ts
Converted SignalR from eager module-load initialization to lazy on-demand initialization. Extracted connection event wiring into _wireConnection() helper. Replaced static import with dynamic import('@microsoft/signalr') during first start() call. Connection instance is now cached for reuse on subsequent starts.

Sequence Diagram(s)

sequenceDiagram
    participant Client as App Client
    participant Module as SignalR Module
    participant Hub as HubConnection

    Note over Client,Hub: Previous (Eager Initialization)
    Client->>Module: import at module load
    Module-->>Client: SignalR library
    Client->>Hub: new HubConnectionBuilder().build()
    Hub-->>Client: connection instance
    Note over Client: Connection ready but unused until start()

    Note over Client,Hub: New (Lazy Initialization)
    Client->>Client: Module loads, connection = null
    Client->>Client: start() called
    Client->>Module: dynamic import('@microsoft/signalr')
    Module-->>Client: SignalR library
    Client->>Hub: new HubConnectionBuilder().build()
    Hub-->>Client: connection instance
    Client->>Client: _wireConnection(connection)
    Note over Client: Event handlers wired
    Client->>Hub: connect()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A lazy rabbit knows the way,
Why rush your load on startup's day?
We've deferred the SignalR dance,
Till first start() gives us a chance—
Cached connections, handlers wired tight,
Lazy loading done just right! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: lazy-loading SignalR via dynamic import with a quantified performance benefit (−54 KB main bundle reduction).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/defer-signalr

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 44 minutes and 53 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ResQ.Viz.Web/client/app.ts`:
- Around line 743-748: The project currently uses SignalR client code
(HubConnectionBuilder, LogLevel and the connection creation logic) that is fine,
but package.json declares "@microsoft/signalr": "^10.0.0"; update
src/ResQ.Viz.Web/package.json to set "@microsoft/signalr": "^8.0.0" to comply
with the TypeScript client requirement, then reinstall dependencies (npm/Yarn
install) so the HubConnectionBuilder/LogLevel imports resolve against SignalR
8.x.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6bd3c2bf-1eaf-4a8e-a578-a0095d9d2ac3

📥 Commits

Reviewing files that changed from the base of the PR and between a30f8a1 and ff4b695.

📒 Files selected for processing (1)
  • src/ResQ.Viz.Web/client/app.ts

Comment thread src/ResQ.Viz.Web/client/app.ts

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements lazy loading for the SignalR runtime in the web client to improve initial load performance and reduce the main bundle size. The @microsoft/signalr dependency is now dynamically imported within the start function, and connection event handlers have been refactored into a _wireConnection helper. I have no feedback to provide.

CodeRabbit flagged this PR for declaring `@microsoft/signalr` ^10
because CLAUDE.md still said the project uses signalr 8. The
package was bumped to 10 alongside the .NET 10 backend (older PR);
CLAUDE.md just wasn't updated. Bring the line into sync with reality
and note that signalr now ships as a lazy chunk per this PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@WomB0ComB0
WomB0ComB0 merged commit 3853139 into main Apr 29, 2026
37 checks passed
@WomB0ComB0
WomB0ComB0 deleted the feat/defer-signalr branch April 29, 2026 05:25
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
Surfaces the LosQueryStats added in PRs #78/#79 in a small dev/audit
panel. Lets an operator confirm the WebGPU sensor primitive is
healthy without dropping into devtools — useful both for the
2026-05-12 audit routine (P2 #5) and during dev when tuning ring
sizing or world extents.

- New `client/sensorStatsOverlay.ts` — `SensorStatsOverlay` class
  builds a fixed bottom-left panel, hidden by default. Press 'i'
  (KeyI; ignored when typing in inputs) to toggle.
- Two sections (mesh-link, lidar) showing totalQueries / totalRays
  / peakSlotDepth / raysOutsideWorld. Cells colour amber when peak
  slot depth > 1 (callers queueing) or outside-AABB > 0 (sim is
  outside the brick map's coverage; project memory documents the
  4 km terrain vs 1 km sensor world gap).
- `update()` is called once per `ReceiveFrame`; short-circuits when
  the panel is hidden, so the closed-overlay cost is one boolean.
- Falls back gracefully when `getSensorContext()` returns null
  (no-WebGPU browser, init failure) — the panel just shows "offline".

Bundle: 809.0 → 811.3 KB (+2.3 KB; 99.0 % of the 800 KiB cap). Lands
much more comfortably on top of #82 (defer-signalr), which would
take this combined to ~756 KB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WomB0ComB0 added a commit that referenced this pull request Apr 29, 2026
* feat(viz): sensor-stats overlay (toggle with 'i')

Surfaces the LosQueryStats added in PRs #78/#79 in a small dev/audit
panel. Lets an operator confirm the WebGPU sensor primitive is
healthy without dropping into devtools — useful both for the
2026-05-12 audit routine (P2 #5) and during dev when tuning ring
sizing or world extents.

- New `client/sensorStatsOverlay.ts` — `SensorStatsOverlay` class
  builds a fixed bottom-left panel, hidden by default. Press 'i'
  (KeyI; ignored when typing in inputs) to toggle.
- Two sections (mesh-link, lidar) showing totalQueries / totalRays
  / peakSlotDepth / raysOutsideWorld. Cells colour amber when peak
  slot depth > 1 (callers queueing) or outside-AABB > 0 (sim is
  outside the brick map's coverage; project memory documents the
  4 km terrain vs 1 km sensor world gap).
- `update()` is called once per `ReceiveFrame`; short-circuits when
  the panel is hidden, so the closed-overlay cost is one boolean.
- Falls back gracefully when `getSensorContext()` returns null
  (no-WebGPU browser, init failure) — the panel just shows "offline".

Bundle: 809.0 → 811.3 KB (+2.3 KB; 99.0 % of the 800 KiB cap). Lands
much more comfortably on top of #82 (defer-signalr), which would
take this combined to ~756 KB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viz): tighten overlay keybind + align CSS with design tokens

Address Gemini review on PR #83:

- Keybind: extend the input-bail list to cover SELECT (the operator
  may type 'i' to jump-search options), and require no shift key
  (uppercase 'I' shouldn't toggle a dev panel from within prose
  typed elsewhere).
- CSS: replace `--fg` / `--muted` with the project's canonical
  `--text` / `--text-muted` (defined in `:root` lines 18-36); fix the
  `--warning` fallback to the palette colour `#d29922`.
- Add `body.investor-mode .sensor-stats-overlay { display: none }` so
  the panel disappears during cinematic recordings — matches the
  visibility rule for the event log and telemetry strip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant