Skip to content

components: fixes and improvements for components, change detection and defer blocks #344

Closed

Description

@erkamyaman

Context

Fixes and improvements from a code audit of the components inspector, change detection recording, defer blocks and control flow, done in October 2026. Roadmap work lives in #340, #341, #342 and #343. Items marked (to confirm) were not reproduced; check them before fixing.

The components inspector

Problems

Medium

  • Picking leaks pointer events to the app: only click calls preventDefault and stopPropagation. pointerdown, mousedown, pointerup and mouseup still reach the page, so menus, drag handles and CDK overlays that react to them open or close while you pick. Touch picking doesn't work either. Capture those events too, outside the popup, and resolve the pick on pointerup (packages/devtools/src/component-pick.ts:58-63, 97-102).

Low

  • SVG component hosts can't be highlighted while picking: onHover keeps the host only when it is an HTMLElement, so a component on an svg:g selector gets no box. showHighlight already takes an Element, so accept Element here (packages/devtools/src/component-pick.ts:47).
  • Highlight box ignores shadow-root children: when a host has an empty rect, highlightBox only walks el.children. A display: contents host whose content is in its shadow root gets no box. Also walk el.shadowRoot?.children (packages/devtools/src/page-highlight.ts:32).
  • No source location for directives, and the fallback can be wrong: componentSource reads only ɵcmp.debugInfo, so the directive entries in the detail never get a file. Without debugInfo, sourceFor falls back to matches[0] when two classes share a name, and the panel shows that file as if it were certain. Read ɵdir.debugInfo too, fill source on each directive in the detail, and mark guessed locations as "likely" (packages/devtools/src/component-source.ts:17, packages/devtools/src/component-tree.ts:275-283, app/src/pages/component-tree.ts:1695-1699).
  • Changed values aren't visible: properties are snapshots, so you can't tell what changed between ticks, and a kind: 'signal' property has no link to the Signals page. Flash values that changed since the last detail, and add a "Show in Signals" action for signal properties (app/src/pages/component-tree.ts, packages/devtools/src/component-tree.ts).

Improvements

  • Open the template file and line as well as the class. The source scan doesn't return templateUrl for components yet, so add it to the scan and to SourceComponent (packages/devtools/src/rpc/get-components.ts, app/src/pages/component-tree.ts:36-46).
  • Pick by keyboard: once picking, arrow keys move between component hosts on the page and Enter selects (packages/devtools/src/component-pick.ts).
  • "Copy as JSON" and "Copy host path" actions on the selected component, for bug reports and agents (app/src/pages/component-tree.ts).

Already tracked elsewhere

Change detection recording

Problems

Medium

  • A second start wipes a running recording: setRecording(true) calls recorder.start() every time, and start() calls reset(). An agent record: "start" with no page is broadcast to every tab, so it erases the user's panel recording. A double click does the same. Make on: true a no-op when already recording, and clear only on clear: true. Consider requiring page for agent start and clear when more than one tab is connected (packages/devtools/src/cd-overlay.ts:56-61, packages/devtools/src/cd-recorder.ts:228-232, packages/devtools/src/devframe.ts:1949-1954).
  • Stale output trigger: lastOutput stays set until the next ChangeDetectionStart. An output that started no tick (zoneless with no signal change, or an output nobody listens to) is then credited to a later tick caused by a timer, HTTP or a signal. Stamp lastOutput with now() and drop it when change detection starts more than about one frame later, or clear it in a microtask after the output ends (packages/devtools/src/cd-recorder.ts:119-132).

Low

  • Wrong "needs Angular 20" message: supported becomes false whenever addProfilerListener returns null. That also happens when ng is not available yet, when the page has no [ng-version] (createApplication, Angular Elements, where angularMajor returns 0), or when ɵsetProfiler throws. The panel and the agent tool then show a fixed "needs Angular 20" text, and nothing retries until the user toggles again. Report a reason (no-ng-yet, angular<20, no-profiler-api), retry the attach on the next refresh while recording is wanted, and read the version another way when ng-version is missing (packages/devtools/src/cd-overlay.ts:58-60, packages/devtools/src/change-detection.ts:55-59, app/src/pages/cd-recording.ts:66-68, packages/devtools/src/rpc/cd-tools.ts:161-163).
  • Start is confirmed without an answer from the page: change-detection-record is broadcast with optional: true, and the panel and agent tool say "Recording started" as soon as the hub accepts the request. "Could not reach the page" only shows when the hub itself fails. Wait up to about 1 s for a push with recording: true (or supported: false) from that pageId, and say when the page did not answer (packages/devtools/src/devframe.ts:1256-1263, packages/devtools/src/devframe.ts:1955-1961, app/src/pages/cd-recording.ts:313-332).
  • Limits are hit silently: totals stop taking new classes at 1000, host WeakRefs stop at 2000 instances, and a failed getHostElement is cached as null forever. Tree badges and totals then go missing with no note, unlike cycles, which carry dropped. Add droppedClasses and droppedHosts to CdRecording, show them in LimitNote and the agent text, and do not cache a null host id (packages/devtools/src/cd-recorder.ts:165, packages/devtools/src/cd-recorder.ts:180-187, packages/devtools/src/cd-overlay.ts:42-54).
  • Nested tick replaces the cycle in progress: a ChangeDetectionStart while current is set overwrites it and resets the frame stack, so the outer cycle is lost. Keep a cycle stack, or count a nested tick as a pass (packages/devtools/src/cd-recorder.ts:122-133) (to confirm).
  • Panel shows less than the agent tool: the agent answer has a "Most often checked" list, the panel does not. Cycle rows can't be expanded to their top 10 components, component names don't link to their tree row or highlight, and the zone mode is not shown next to Record. The slowest table uses track $index instead of c.name (app/src/pages/cd-recording.ts:75-126, app/src/pages/cd-recording.ts:89, packages/devtools/src/rpc/cd-tools.ts:171-186).
  • Duplicated types: the panel declares CdCheck, CdCycle, CdComponentStat and CdPage again instead of importing them, so the copies can drift without a compile error. Export them from @pangular-inspector/devtools/config (or a shared types entry), as limit-note.ts and network-inspector.ts already do (app/src/pages/cd-recording.ts:8-38, packages/devtools/src/cd-recorder.ts:17-53, packages/devtools/src/cd-overlay.ts:18-22).

Improvements

  • Keep recording across a reload: store a "recording wanted" flag in sessionStorage per pageId, like storedInstrumented() does for the router, so a reload profile with bootstrap and the first cycles can be captured (packages/devtools/src/cd-overlay.ts, packages/devtools/src/router-actions.ts:91).
  • Slow cycle budget: add limits.cdSlowMs (default 16 ms). Highlight cycles over budget in the panel and count them in the agent answer, so an agent can check "no frame-dropping ticks" after a change (packages/devtools/src/config.ts, packages/devtools/src/rpc/cd-tools.ts, app/src/pages/cd-recording.ts).

Already tracked elsewhere

Defer blocks and control flow

Problems

Medium

  • Stuck placeholder noise: deferNotes flags every block that sits on its placeholder for 10s as "Needs attention", even with a viewport, interaction or hover trigger, where waiting is normal. Only flag idle, immediate, timer, when or no triggers, and give the others a plain "waiting for trigger" note outside the attention list (packages/devtools/src/rpc/component-tools.ts:121-136, 164).
  • No runtime @let: @let values never show in component detail; the only handling is a source regex. If the debug API exposes @let slots in the component's LView, list them read-only through serialize and redaction; otherwise document the gap in components.md (packages/devtools/src/rpc/get-pipes.ts:333) (to confirm).

Low

  • Missing hydratedAt: it is set only when the tracker saw the block dehydrated on an earlier read, so blocks that hydrate before the first collect, or between two reads, never get a time. Mark blocks first seen hydrated as "hydrated before inspection", or catch the transition another way (packages/devtools/src/defer-blocks.ts:96-100).
  • Silent 500-block cap: collect returns early at MAX_BLOCKS and the server slices to 500 again, with no flag. Return a truncated flag and say so in deferBlocksText and the panel pill (packages/devtools/src/defer-blocks.ts:79, packages/devtools/src/rpc/component-tools.ts:42).
  • Unfocusable rows: rows without an owner use [disabled], so keyboard users can't focus them to highlight the block while mouse users can hover them. Use aria-disabled with a guard in showOwner (app/src/pages/component-tree.ts:521-530).
  • Panel hides data it has: the panel's DeferBlock type drops since, hydratedAt, loading/placeholder timings, hasErrorBlock and the notes the agent tool computes, and the section is hidden both when there are no blocks and when the util is missing. Add an expandable row detail, reuse deferNotes for badges, and show a one-line notice when deferBlocks is undefined (app/src/pages/component-tree.ts:87-95, 513-548).
  • Clock skew: deferBlocksText compares the server's Date.now() with since from the page clock, so ages are wrong on remote or Angular Native devices whose clock differs. Send the page's now() with each report, or an age per block (packages/devtools/src/rpc/component-tools.ts:123-125, packages/devtools/src/devframe.ts:1917) (to confirm).
  • Mismatch "component" is a node name: hydration mismatches set component to node.nodeName, for example div or #text. Resolve the owning component and send the node name, component class and element id (packages/devtools/src/http-hydration.ts:59).
  • Partial block highlight: highlightBlock uses only rootIds[0], so blocks with several roots are under-highlighted. Let request-page-highlight take several ids and draw the union (app/src/pages/component-tree.ts:1921-1923, packages/devtools/src/defer-blocks.ts:111-114).

Improvements

  • Badges on component tree nodes for the control flow blocks they own ("2 defer, 1 for") (app/src/pages/component-tree.ts, packages/devtools/src/defer-blocks.ts).
  • An action tool to trigger or force-load a defer block, like DeferBlockFixture.render(state), listed in ACTION_TOOLS (packages/devtools/src/devframe.ts, packages/devtools/src/config.ts).
  • Record defer state transitions with timestamps and add them as a layer of the timeline in phase 3 of 4: a shell you live in #342, so chunk loads tie to defer triggers (packages/devtools/src/defer-blocks.ts).
  • Link hydration mismatches on the Network page to the component tree: click to select and highlight the owner (packages/devtools/src/http-hydration.ts, app/src/pages).
  • Include the defer blocks a component owns in the pangular:component-tree detail of that instance, so agents need one call (packages/devtools/src/rpc/component-tools.ts).

Already tracked elsewhere


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or shows wrong datafeatureA feature request or a pull request that adds one

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions