Skip to content

fix: virtualize tree children to fix UI lag with many nodes (Fixes #47)#51

Open
7vignesh wants to merge 5 commits intotsperf:mainfrom
7vignesh:fix/virtualize-tree-children
Open

fix: virtualize tree children to fix UI lag with many nodes (Fixes #47)#51
7vignesh wants to merge 5 commits intotsperf:mainfrom
7vignesh:fix/virtualize-tree-children

Conversation

@7vignesh
Copy link
Copy Markdown

@7vignesh 7vignesh commented Apr 5, 2026

🎯 Problem

The Type Complexity Tracer UI was rendering all child nodes at once in the DOM, causing severe UI lag when nodes had thousands of children. This made the extension nearly unusable for large projects like arktype.

Related Issue: Closes #47

✨ Solution

Implemented pagination for child node loading:

  • Backend now supports limit/offset parameters for fetching children
  • UI loads 50 children per page by default
  • Users can click "Load More" button to load additional pages
  • Shows accurate count: "Load More (50/1000)"

📝 Changes

Backend Changes

  • src/traceTree.ts: Modified getChildrenById() to accept limit and offset parameters, returns total count
  • src/handleMessages.ts: Updated message handler to pass pagination parameters to backend
  • shared/src/messages.ts: Extended childrenById message schema with pagination fields

Frontend Changes

  • ui/src/appState.ts: Added childrenTotalById map to track per-node children count
  • ui/components/TreeNode.vue:
    • Added pagination state (offset, limit)
    • Implemented loadMoreChildren() function
    • Added "Load More" button showing count progress

✅ Testing

  • Build succeeds with no errors
  • ESLint passes
  • No broken dependencies
  • Changes are backward compatible
  • Existing tree rendering still works

🔗 Related Issues

📦 Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

7vignesh added 5 commits April 6, 2026 01:35
Add limit and offset parameters to support paginated loading of children.
Returns total count of children for UI to implement load-more functionality.

- limit: number of children to fetch (default 50)
- offset: pagination offset (default 0)
- total: total number of children available
Prevents loading all children at once for nodes with thousands of kids.
Returns paginated results with total count.

- Accepts limit and offset parameters
- Returns children array and total count
- Fixes issue tsperf#47: UI lags when there are many child nodes
Add childrenTotalById map to store total children available.
Update message handler to set total count from server response.

This enables the UI to show load-more button with accurate count.
Implement paginated child rendering to prevent DOM explosion.
Default limit of 50 children per page keeps UI responsive.

Changes:
- Add offset and limit state (50 children per page)
- Request children with pagination parameters
- Show 'Load More' button when more children available
- Display count (e.g. 'Load More (50/1000)')
- Accumulate children as user loads more pages

Fixes issue tsperf#47: UI lags when there are many child nodes
Copilot AI review requested due to automatic review settings April 5, 2026 20:10
@7vignesh 7vignesh requested a review from danielroe as a code owner April 5, 2026 20:10
@7vignesh
Copy link
Copy Markdown
Author

7vignesh commented Apr 5, 2026

Hey @danielroe 👋 Just opened this PR to fix the UI lag issue (#47) by virtualizing the tree children — would love your feedback on the approach!

I've been exploring the codebase and I'm genuinely interested in contributing more. Happy to take on other open issues if you have any priorities in mind. Open to a discussion anytime!

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds server-assisted pagination for loading tree children to reduce DOM/rendering load and fix UI lag when nodes have many children (Fixes #47).

Changes:

  • Backend: extend childrenById to support limit/offset and return total child count.
  • Frontend: track per-node total child count and add “Load More” pagination UI.
  • Shared: update message schema to include pagination fields.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
ui/src/appState.ts Tracks per-node total child count from childrenById responses
ui/components/TreeNode.vue Adds pagination state + “Load More” control for incremental child loading
src/traceTree.ts Implements paginated child retrieval and returns { children, total }
src/handleMessages.ts Wires message handler to pass pagination args and return total
shared/src/messages.ts Extends childrenById schema with limit/offset and total
renovate.json Formatting-only change (newline/indent alignment)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +95
<div v-if="totalChildren !== undefined && children.length + offset < totalChildren" class="flex justify-center py-2 pl-8">
<button class="px-4 py-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] rounded hover:opacity-80 transition-opacity text-sm" @click="loadMoreChildren">
Load More ({{ children.length + offset }}/{{ totalChildren }})
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The “Load More” visibility/progress math is incorrect: children.length already represents the number of loaded children (since childrenById appends). Adding offset double-counts and will hide the button early (roughly after half the children) and display wrong progress after the first load-more.

Suggested change
<div v-if="totalChildren !== undefined && children.length + offset < totalChildren" class="flex justify-center py-2 pl-8">
<button class="px-4 py-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] rounded hover:opacity-80 transition-opacity text-sm" @click="loadMoreChildren">
Load More ({{ children.length + offset }}/{{ totalChildren }})
<div v-if="totalChildren !== undefined && children.length < totalChildren" class="flex justify-center py-2 pl-8">
<button class="px-4 py-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] rounded hover:opacity-80 transition-opacity text-sm" @click="loadMoreChildren">
Load More ({{ children.length }}/{{ totalChildren }})

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +26
// Pagination state
const offset = ref(0)
const limit = 50

function fetchChildren() {
if (children.value.length === 0)
sendMessage('childrenById', { id: props.tree.id })
sendMessage('childrenById', { id: props.tree.id, limit, offset: offset.value })
}

function loadMoreChildren() {
// Load next batch of children
const newOffset = offset.value + limit
offset.value = newOffset
sendMessage('childrenById', { id: props.tree.id, limit, offset: newOffset })
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

offset is kept as local component state but the actual loaded count is stored globally in childrenById. If this component is ever remounted/reused (e.g., collapse/unmount behavior), offset will reset to 0 while childrenById may already contain items, causing duplicate page fetches. Consider deriving the next offset from children.value.length (or syncing offset from it) instead of incrementing a standalone ref.

Copilot uses AI. Check for mistakes.
Comment on lines 5 to 7
export const childrenById = shallowReactive(new Map<number, Tree[]>())
export const childrenTotalById = shallowReactive(new Map<number, number>())
export const typesById = shallowReactive(new Map<number, TypeLine[]>())
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

childrenTotalById is introduced but none of the per-id caches (childrenById, typesById, childrenTotalById) are cleared when a new tree is shown or the trace file list is reset. Since node IDs restart from 1 for each new trace (src/traceTree.ts resets id = 0), stale cached children/total can be applied to unrelated nodes in later traces. Clear these maps when handling showTree step start and/or traceFileLoaded with resetFileList.

Copilot uses AI. Check for mistakes.
Comment on lines +136 to +141
export function getChildrenById(id: number, limit: number = 50, offset: number = 0) {
const nodes = treeIdNodes.get(id)?.children ?? []
const ret: typeof nodes = []
nodes.forEach((node) => {
const total = nodes.length
const paginatedNodes = nodes.slice(offset, offset + limit)
const ret: typeof paginatedNodes = []
paginatedNodes.forEach((node) => {
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

limit/offset are used directly to slice the children array without validation. Negative values or very large limits can produce unexpected paging behavior or reintroduce large payloads (UI freeze) if a webview sends a large limit. Clamp limit to a reasonable max and ensure offset >= 0 before slicing.

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +149
limit: z.number().optional().default(50),
offset: z.number().optional().default(0),
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The message schema accepts any numeric limit/offset. To avoid abuse and accidental UI freezes, constrain these (e.g., non-negative offset, reasonable max limit) at the schema level (zod refinements) and/or in the message handler before calling getChildrenById.

Suggested change
limit: z.number().optional().default(50),
offset: z.number().optional().default(0),
limit: z.number().int().min(0).max(500).optional().default(50),
offset: z.number().int().min(0).optional().default(0),

Copilot uses AI. Check for mistakes.
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.

UI lags when there are many child nodes

2 participants