Skip to content

Render only the visible variables in the variables list, and make its rows denser - #9006

Merged
4ian merged 6 commits into
masterfrom
feat/virtualized-variables-list-843f
Aug 25, 2026
Merged

Render only the visible variables in the variables list, and make its rows denser#9006
4ian merged 6 commits into
masterfrom
feat/virtualized-variables-list-843f

Conversation

@4ian

@4ian 4ian commented Aug 24, 2026

Copy link
Copy Markdown
Owner

A scene with 500 variables made the properties panel (and the variables dialog) slow to display and slow to react to any change, because every row of the list was rendered. The list now renders only the rows that are on screen, and its rows were redesigned to be denser.

Rendering only the visible rows

The scroll of the variables list is not always handled by the list itself: in the properties panel, the panel scrolls all its sections at once. So instead of using a virtualized list component (which would own the scroll), the list keeps rendering into a container as tall as all its rows, and only fills it with the rows that are on screen:

  • FlattenVariables.js flattens the tree of variables (both the inherited ones and the ones of the container) into the list of rows to display, applying the search filter and hiding the children of folded variables.
  • useVisibleRowsRange.js computes which of these rows are on screen. It does not care about which ancestor scrolls: it walks up the DOM once to collect the ancestors that clip their content, and intersects their rectangles with the window to know the band of the container that can actually be seen. A capturing scroll listener on window catches the scroll of any of them, and a ResizeObserver catches the panel being resized.
  • All the rows are absolutely positioned at index * VARIABLE_ROW_HEIGHT in that container, so a row keeps its DOM node when the visible range changes — which matters when a variable is added at the end of a long list: the list scrolls to it and its name field is focused, and it must not be remounted in between.

Every row therefore has the exact same height, which is what makes a row's position computable without rendering the ones before it.

Denser and tidier rows

A row used to be laid out on one or two lines depending on the available width, with a text field showing a border at all times. It is now always a single line of 30px (instead of 36px in a dialog and 53px in the properties panel):

  • the type of a variable uses CompactSelectField, the select field used in the rest of the editor, and is reduced to its icon (with a tooltip, and a box appearing on hover) when the list is too narrow for the name of the type to fit;
  • the text fields only show their underline when hovered or focused;
  • the indentation is smaller, and capped so that it never eats more than 30% of the width;
  • the rows have rounded corners, a hover state and a selected state taken from the theme;
  • how tightly a row is laid out is decided from the width the list is measured to have, instead of it being a "compact" list, since the properties panel can be resized.

The variables of an instance in a dialog, before and after

The variables of a scene in the properties panel, before and after

Checks

500 variables (900 rows once the structures and arrays are expanded): 23 rows rendered instead of 900, and a change in the list (a keystroke, a type changed, a variable folded) takes ~20ms instead of ~2.1s.

variables_list_500_variables_scrolling_adding_and_searching.mp4

The Storybook stories got two additions used to check this: WithManyVariables (a scene with 500 variables) and CompactWithManyVariablesInScrollingPanel (the same variables in a narrow panel which is the one handling the scroll). Both were driven in a real browser to check that scrolling anywhere in the list displays exactly the expected variables, that no more rows than needed are rendered, that all the rows have the same height, that the type and value columns stay aligned whatever the indentation, that adding a variable scrolls to it and focuses its name, that the search filters correctly, and that drag and drop still moves variables.

FlattenVariables.spec.js covers the flattening itself: the order of the rows, folded variables hiding their children, inherited variables being displayed first without the ones being overridden, and what a search displays.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

The whole tree of variables is flattened into a list of fixed height rows, and
only the rows that are visible on screen are rendered. The scroll can be handled
by the variables list itself (in the variables dialog) or by any ancestor (the
properties panel scrolling all its sections at once), which is why the closest
clipping ancestors are found in the DOM to know what is visible.

The rows are also redesigned: a single line of a fixed height (instead of one or
two lines depending on the available width), the type displayed with the compact
select field used in the rest of the editor (or only its icon when the width is
scarce), a smaller indentation and lighter text fields.
All the rows are now positioned by their index in the container holding all of
them, instead of being laid out in a block positioned at the first visible row:
this way a variable that is added at the end of a long list is not remounted -
and does not lose the focus - when the list scrolls to it.

The focus of a newly displayed variable is also given by the code scrolling to
it, once its row exists, and the fields of unmounted rows are forgotten (the
pointers of deleted variables get reused).
The properties panel can be resized, so whether the rows are laid out tightly
(smaller indentation, type displayed as an icon only) now depends on the width
the list is measured to have, instead of it being a compact list.

Also add unit tests of the flattening of the variables into rows.
The box had the same color as the row, so it was invisible: it now only appears
on hover, to show that clicking the icon of the type changes it.
@cursor

cursor Bot commented Aug 24, 2026

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

The labels of a row (the value of a boolean, the type of a variable that can't
be edited) are plain elements, and the font is otherwise only set by the Text
components: they were falling back to the font of the browser.
@4ian
4ian marked this pull request as ready for review August 25, 2026 13:20
@4ian
4ian merged commit 9a08f27 into master Aug 25, 2026
7 checks passed
@4ian
4ian deleted the feat/virtualized-variables-list-843f branch August 25, 2026 13:22
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