Skip to content

feat(network-activity-plugin): virtualize large code blocks#279

Merged
V3RON merged 3 commits into
callstackincubator:mainfrom
burczu:feat/network-activity-plugin-code-block-virtualization
May 19, 2026
Merged

feat(network-activity-plugin): virtualize large code blocks#279
V3RON merged 3 commits into
callstackincubator:mainfrom
burczu:feat/network-activity-plugin-code-block-virtualization

Conversation

@burczu
Copy link
Copy Markdown
Contributor

@burczu burczu commented May 18, 2026

Part of #244 — "Improve response viewing in Network Activity." Makes the response panel's CodeBlock threshold-aware so multi-megabyte text bodies don't pin hundreds of thousands of DOM nodes.

Summary

  • CodeBlock becomes threshold-aware. When children is a string longer than 50,000 characters, the body renders inside a react-virtuoso <Virtuoso> and only the rows currently visible inside a 500 px window stay in the DOM. Below the threshold (and for any non-string children — <JsonTree>, <XmlTree> wrapped in CodeBlock for the monospace-on-dark frame), rendering is unchanged: a flat <pre>. The dispatch is a runtime typeof children === 'string' branch — zero changes required at any of the 11+ existing CodeBlock call sites (json / xml / svg / text-fallback renderers, malformed-fallback paths, RequestBody).
  • Virtuoso configuration. Fixed style={{ height: 500 }} matches the visual scale of HexView and the iframe elsewhere in the response panel. <div> rows (Virtuoso positions rows absolutely and needs block-level items). Per-row whitespace-pre-wrap wrap-anywhere preserves today's wrapping UX so long URLs / base64 / minified-on-one-line content wrap visually instead of forcing horizontal scroll. Outer styling reuses the existing codeBlockClassNames constant so the dark bg / monospace / border / padding are identical between the two branches.
  • Known shape limitation. A 50 KB body with no newlines virtualizes to a single row containing the entire payload. Browser-level wrapping keeps the layout sane but there's no real virtualization benefit for that shape — the perf win shows up once the body has many lines.
  • UX trade-off worth knowing. Today's <pre> expands to content (height grows with body length). After this change, content above 50 KB scrolls inside a 500 px window. Deliberate, for snappy rendering on multi-megabyte responses.
  • Adds react-virtuoso ^4.6.0 to @rozenite/network-activity-plugin's dependencies.
  • Playground. A new "Large text" main-tab on NetworkTestScreen with two pre-verified URLs straddling the threshold so reviewers can see both branches: Alice in Wonderland from Gutenberg (~174 KB, virtualizes) and the GPL-3 license text (~35 KB, stays flat).

Test plan

  • pnpm --filter @rozenite/network-activity-plugin test — all tests pass (8 new in CodeBlock.test.tsx).
  • pnpm --filter @rozenite/network-activity-plugin typecheck — clean.
  • pnpm --filter @rozenite/network-activity-plugin lint — clean.
  • npx prettier --check on changed files — clean.
  • Playground (pnpm --filter @rozenite/playground ios or :android) → Network Test → Large text tab:
    • Alice in Wonderland (~174 KB) — Response body scrolls inside a 500 px window. NOT an unbounded <pre>. Scrolling is smooth at the end of the ~3300-line file. Dark bg / monospace / border look identical to a small <pre>.
    • GPL-3 license (~35 KB) — Response body renders as a flat <pre>, height grows with content, no internal scrolling. (Control: confirms the threshold gating works.)
  • JSON tree responses unchanged — open any JSON response (HTTP Test → User-list). Renders as JsonTree wrapped in CodeBlock as before. No virtualization (children is a React element, not a string).
  • SVG Raw view unchanged — Images tab → SVG → toggle to Raw. Renders as flat <pre> with SVG source (SVG bodies are well under threshold).
  • Toggle behavior unchanged — sticky Preview/Raw preference, override button presence, content-type label all work as before.

New test surface

components/__tests__/CodeBlock.test.tsx (8 tests):

  • Small string content renders as <pre> with no virtuoso-mock present.
  • Exactly 50,000 chars stays on the flat path (inclusive-boundary semantics — the branch condition is >, not >=).
  • 50,001 chars switches to the virtualized rendering.
  • Content survives the threshold split (recognizable head and tail tokens both visible in the DOM after virtualization).
  • Newlines in virtualized content produce one row per line.
  • React-element children stay on the <pre> path even when the wrapped element contains a 50,001-char string internally (the typeof children === 'string' check takes precedence).
  • className forwarding on both the flat and virtualized branches.

vitest.setup.ts adds a vi.mock('react-virtuoso') passthrough so jsdom-based tests can assert content correctness. jsdom can't drive Virtuoso's resize observation otherwise — without the stub any test importing a virtualizing component fails to render rows. Production code is unaffected.

Followups (deliberately not in this PR)

  • Search inside virtualized content.
  • Truncation modes.
  • Virtualizing trees (JsonTree, XmlTree) — would require forking react-json-tree or rewriting from scratch.
  • Per-row line numbers in the virtualized branch.
  • Syntax highlighting (cross-cutting feature; would also benefit XML / JSON / HTML Raw views).
  • Programmatic line-wrapping for single-line content longer than the threshold (would require choosing a column width).

Coordination notes

@burczu burczu marked this pull request as ready for review May 18, 2026 09:45
@burczu burczu force-pushed the feat/network-activity-plugin-code-block-virtualization branch from 9210b04 to 9d9983e Compare May 19, 2026 07:53
burczu added 3 commits May 19, 2026 10:24
`CodeBlock` grows a threshold-aware rendering path. When `children`
is a string longer than 50,000 characters, the body renders inside
a `react-virtuoso` `<Virtuoso>` so only the rows visible in the
500px viewport are kept in the DOM — large pretty-printed JSON,
minified bundles served as text, oversized logs, and similar
multi-megabyte payloads no longer pin a `<pre>` node containing
hundreds of thousands of lines.

The dispatch is a runtime branch on `typeof children === 'string'`.
React-element children (the `<JsonTree>` and `<XmlTree>` trees
wrapped in `<CodeBlock>` to inherit the monospace-on-dark frame)
flow through the existing `<pre>` unchanged regardless of nested
content size — the type-check on the children prop itself takes
precedence over anything the wrapped element contains. String
children below the threshold also continue to use `<pre>`, so the
vast majority of responses see no behavior change.

Zero changes are required at any existing `CodeBlock` call site:
the 11+ consumers in the plugin (json / xml / svg / text-fallback
renderers, malformed-fallback paths, RequestBody) pass strings or
trees today and pick up virtualization automatically when a body
is large enough to need it.

Virtuoso configuration:
- `style={{ height: 500 }}` — matches HexView and the iframe in
  other parts of the response panel for visual consistency.
- `<div>` rows (not `<span>`) because Virtuoso positions rows
  absolutely and needs block-level items.
- `whitespace-pre-wrap wrap-anywhere` per row preserves today's
  `<pre>` wrapping UX — long URLs, base64, minified-on-one-line
  content wrap visually instead of forcing horizontal scroll.
  Rows have variable height; Virtuoso measures them.
- Outer styling via the existing `codeBlockClassNames` so the
  dark bg / monospace / border / padding are identical between
  the two branches.

Known shape limitation noted in code: a single 50KB-line body
with no newlines virtualizes to one row containing the entire
payload. Browser wrapping keeps layout sane but there's no real
virtualization benefit — the size win shows up once the body has
many lines. Programmatic line-wrapping (choosing a column width
to split very long lines into multiple rows) is out of scope.

UX trade-off worth knowing: today's `<pre>` expands to content
(height grows with body length). Post-change, content above 50KB
scrolls within a 500px window. Deliberate, for snappy rendering
on multi-megabyte responses.

Adds `react-virtuoso ^4.6.0` to the plugin's `dependencies`.

Tests:
- `vitest.setup.ts` adds a `vi.mock('react-virtuoso')` passthrough
  that renders every row regardless of viewport. jsdom can't drive
  Virtuoso's resize observation, so without the stub any test
  importing a virtualizing component fails to render rows. The
  passthrough lets RTL queries assert content correctness; the
  production build uses real Virtuoso unchanged.
- `__tests__/CodeBlock.test.tsx` covers the branch (small → `<pre>`,
  large → `data-testid="virtuoso-mock"`), inclusive-boundary
  semantics at exactly 50,000 chars, the 50,001-char trigger,
  content preservation across the threshold, newline-based row
  splitting, React-element children staying on the `<pre>` path
  regardless of nested size, and `className` forwarding on both
  branches.
A new "Large text" main-tab on NetworkTestScreen, modeled after
the existing Images tab. Two buttons that straddle the 50 KB
virtualization threshold so reviewers can see both branches of
CodeBlock without leaving the playground:

- "Alice in Wonderland (~174 KB)" —
  https://www.gutenberg.org/cache/epub/11/pg11.txt (200 OK,
  text/plain; charset=utf-8, 174 KB). Stable URL, routes via
  text-fallback, exercises the virtualized 500 px scrollable
  window.
- "GPL-3 license (~35 KB)" — https://www.gnu.org/licenses/
  gpl-3.0.txt (200 OK, text/plain, 35 KB). Stays under the
  threshold, renders as the flat <pre> for direct comparison.
Minor bump for @rozenite/network-activity-plugin. The Response
Viewing section in network-activity.mdx gains a single paragraph
explaining that text response bodies above 50 KB render inside a
virtualized 500 px scrollable window, while smaller bodies stay
on the flat-block path. Wording emphasises the no-truncation
guarantee.
@burczu burczu force-pushed the feat/network-activity-plugin-code-block-virtualization branch from 9d9983e to 6728d85 Compare May 19, 2026 08:41
@V3RON V3RON merged commit 571d273 into callstackincubator:main May 19, 2026
1 check passed
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