fix: virtualize tree children to fix UI lag with many nodes (Fixes #47)#51
fix: virtualize tree children to fix UI lag with many nodes (Fixes #47)#517vignesh wants to merge 5 commits intotsperf:mainfrom
Conversation
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
|
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! |
There was a problem hiding this comment.
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
childrenByIdto supportlimit/offsetand returntotalchild 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.
| <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 }}) |
There was a problem hiding this comment.
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.
| <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 }}) |
| // 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 }) |
There was a problem hiding this comment.
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.
| export const childrenById = shallowReactive(new Map<number, Tree[]>()) | ||
| export const childrenTotalById = shallowReactive(new Map<number, number>()) | ||
| export const typesById = shallowReactive(new Map<number, TypeLine[]>()) |
There was a problem hiding this comment.
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.
| 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) => { |
There was a problem hiding this comment.
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.
| limit: z.number().optional().default(50), | ||
| offset: z.number().optional().default(0), |
There was a problem hiding this comment.
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.
| 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), |
🎯 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:
📝 Changes
Backend Changes
getChildrenById()to acceptlimitandoffsetparameters, returns total countchildrenByIdmessage schema with pagination fieldsFrontend Changes
childrenTotalByIdmap to track per-node children countloadMoreChildren()function✅ Testing
🔗 Related Issues
📦 Type of Change