Skip to content

Commit 2600c08

Browse files
perf(studio): memoize Data Designer builder panes (#1018)
Wrap the builder's presentational panes in React.memo and stabilize the callbacks the build route hands them, so editing a column no longer re-renders the toolbar, palette, and config pane on every keystroke. SchemaRow/SchemaList now take id-taking handlers instead of pre-bound closures, so memoized rows keep referential equality across renders. Signed-off-by: Sean Teramae <steramae@nvidia.com>
1 parent 399a202 commit 2600c08

7 files changed

Lines changed: 70 additions & 62 deletions

File tree

web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderConfigPane.tsx

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { Flex, Text } from '@nvidia/foundations-react-core';
55
import { ColumnConfigPanel } from '@studio/components/ColumnConfigPanel';
66
import { ModelConfigPanel } from '@studio/components/ModelConfigPanel';
7-
import type { FC } from 'react';
7+
import { type FC, memo } from 'react';
88

99
export interface BuilderConfigPaneProps {
1010
selectedColumnId: string | null;
@@ -16,35 +16,37 @@ export interface BuilderConfigPaneProps {
1616
onModelClose: () => void;
1717
}
1818

19-
export const BuilderConfigPane: FC<BuilderConfigPaneProps> = ({
19+
export const BuilderConfigPane: FC<BuilderConfigPaneProps> = memo(function BuilderConfigPane({
2020
selectedColumnId,
2121
selectedModelId,
2222
workspace,
2323
onColumnRemove,
2424
onColumnClose,
2525
onModelRemove,
2626
onModelClose,
27-
}) => (
28-
<div className="w-[240px] shrink-0 border-l border-base bg-surface-base">
29-
{selectedColumnId ? (
30-
<ColumnConfigPanel
31-
columnId={selectedColumnId}
32-
onRemove={onColumnRemove}
33-
onClose={onColumnClose}
34-
/>
35-
) : selectedModelId ? (
36-
<ModelConfigPanel
37-
modelId={selectedModelId}
38-
workspace={workspace}
39-
onRemove={onModelRemove}
40-
onClose={onModelClose}
41-
/>
42-
) : (
43-
<Flex align="center" justify="center" className="h-full p-density-lg">
44-
<Text kind="body/regular/sm" className="text-secondary text-center">
45-
Select a column or model to configure it, or add one from the left.
46-
</Text>
47-
</Flex>
48-
)}
49-
</div>
50-
);
27+
}) {
28+
return (
29+
<div className="w-[240px] shrink-0 border-l border-base bg-surface-base">
30+
{selectedColumnId ? (
31+
<ColumnConfigPanel
32+
columnId={selectedColumnId}
33+
onRemove={onColumnRemove}
34+
onClose={onColumnClose}
35+
/>
36+
) : selectedModelId ? (
37+
<ModelConfigPanel
38+
modelId={selectedModelId}
39+
workspace={workspace}
40+
onRemove={onModelRemove}
41+
onClose={onModelClose}
42+
/>
43+
) : (
44+
<Flex align="center" justify="center" className="h-full p-density-lg">
45+
<Text kind="body/regular/sm" className="text-secondary text-center">
46+
Select a column or model to configure it, or add one from the left.
47+
</Text>
48+
</Flex>
49+
)}
50+
</div>
51+
);
52+
});

web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderDetailsPanel.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useStickToBottom } from '@nemo/common/src/hooks/useStickToBottom';
55
import { Banner, Button, CodeSnippet, Flex, Stack } from '@nvidia/foundations-react-core';
66
import { formatPreviewLogsForDisplay } from '@studio/components/NewDataDesignerJobForm/previewApi';
77
import { ChevronDown, ChevronRight } from 'lucide-react';
8-
import type { FC } from 'react';
8+
import { type FC, memo } from 'react';
99

1010
export interface BuilderDetailsPanelProps {
1111
validationErrors: string[];
@@ -14,13 +14,13 @@ export interface BuilderDetailsPanelProps {
1414
isOpen: boolean;
1515
onToggle: () => void;
1616
}
17-
export const BuilderDetailsPanel: FC<BuilderDetailsPanelProps> = ({
17+
export const BuilderDetailsPanel: FC<BuilderDetailsPanelProps> = memo(function BuilderDetailsPanel({
1818
validationErrors,
1919
submitError,
2020
previewLogs,
2121
isOpen,
2222
onToggle,
23-
}) => {
23+
}) {
2424
const { ref: logsScrollRef } = useStickToBottom<HTMLDivElement>({
2525
enabled: isOpen && !!previewLogs,
2626
});
@@ -80,4 +80,4 @@ export const BuilderDetailsPanel: FC<BuilderDetailsPanelProps> = ({
8080
)}
8181
</div>
8282
);
83-
};
83+
});

web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderPalette.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
JobBuilderFormValues,
1111
PaletteTab,
1212
} from '@studio/routes/DataDesignerJobBuildRoute/useJobBuilder';
13-
import type { FC } from 'react';
13+
import { type FC, memo } from 'react';
1414
import { useFormContext, useWatch } from 'react-hook-form';
1515

1616
export interface BuilderPaletteProps {
@@ -24,15 +24,15 @@ export interface BuilderPaletteProps {
2424
}
2525

2626
// Tabs only swap what you're adding — column and model configs both open in the right pane.
27-
export const BuilderPalette: FC<BuilderPaletteProps> = ({
27+
export const BuilderPalette: FC<BuilderPaletteProps> = memo(function BuilderPalette({
2828
tab,
2929
onTabChange,
3030
selectedModelId,
3131
workspace,
3232
onAddColumn,
3333
onAddModel,
3434
onSelectModel,
35-
}) => {
35+
}) {
3636
const { control, getValues } = useFormContext<JobBuilderFormValues>();
3737
const models = useWatch({ control, name: 'models' });
3838
const hasSeedColumn = getValues('columns').some(
@@ -69,4 +69,4 @@ export const BuilderPalette: FC<BuilderPaletteProps> = ({
6969
</div>
7070
</aside>
7171
);
72-
};
72+
});

web/packages/studio/src/routes/DataDesignerJobBuildRoute/BuilderToolbar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Button, Flex, SegmentedControl, Tag, Text } from '@nvidia/foundations-r
77
import type { StartOptionTag } from '@studio/components/CreateFilesetStart/types';
88
import type { JobBuilderFormValues } from '@studio/routes/DataDesignerJobBuildRoute/useJobBuilder';
99
import { FileJson, ListTree, Pencil, SplinePointer } from 'lucide-react';
10-
import { type FC, useState } from 'react';
10+
import { type FC, memo, useState } from 'react';
1111
import { useFormContext, useWatch } from 'react-hook-form';
1212

1313
/** Which renderer the center pane shows: the flat schema list or the DAG canvas. */
@@ -26,7 +26,7 @@ export interface BuilderToolbarProps {
2626
isSubmitting: boolean;
2727
}
2828

29-
export const BuilderToolbar: FC<BuilderToolbarProps> = ({
29+
export const BuilderToolbar: FC<BuilderToolbarProps> = memo(function BuilderToolbar({
3030
templateTag,
3131
columnCount,
3232
viewMode,
@@ -35,7 +35,7 @@ export const BuilderToolbar: FC<BuilderToolbarProps> = ({
3535
isPreviewing,
3636
onSubmit,
3737
isSubmitting,
38-
}) => {
38+
}) {
3939
const [isEditingName, setIsEditingName] = useState(false);
4040
const { control } = useFormContext<JobBuilderFormValues>();
4141
const name = useWatch({ control, name: 'name' }) ?? '';
@@ -126,4 +126,4 @@ export const BuilderToolbar: FC<BuilderToolbarProps> = ({
126126
</Flex>
127127
</Flex>
128128
);
129-
};
129+
});

web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaList.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useFormContext, useWatch } from 'react-hook-form';
1313

1414
export interface SchemaListProps {
1515
selectedId: string | null;
16-
onSelect: (id: string | null) => void;
16+
onSelect: (id: string) => void;
1717
onDelete: (id: string) => void;
1818
}
1919

@@ -63,8 +63,8 @@ export const SchemaList: FC<SchemaListProps> = ({ selectedId, onSelect, onDelete
6363
column={column}
6464
references={referencesById.get(column.id) ?? []}
6565
selected={column.id === selectedId}
66-
onSelect={() => onSelect(column.id)}
67-
onDelete={() => onDelete(column.id)}
66+
onSelect={onSelect}
67+
onDelete={onDelete}
6868
/>
6969
))}
7070
</Stack>

web/packages/studio/src/routes/DataDesignerJobBuildRoute/SchemaRow.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CardIconBadge } from '@studio/components/common/SelectableCard';
66
import type { BuilderColumn } from '@studio/routes/DataDesignerJobBuildRoute/columns';
77
import { describeColumn } from '@studio/routes/DataDesignerJobBuildRoute/describeColumn';
88
import { Box, Trash2 } from 'lucide-react';
9-
import type { FC } from 'react';
9+
import { type FC, memo } from 'react';
1010

1111
/** Accent color → NVIDIA Foundations text token, matching the DAG node icon styling. */
1212
const ACCENT_ICON_CLASS: Record<string, string> = {
@@ -24,22 +24,22 @@ export interface SchemaRowProps {
2424
/** Names of columns this one references, shown inline as `{{ name }}` relationship tags. */
2525
references: string[];
2626
selected: boolean;
27-
onSelect: () => void;
28-
onDelete: () => void;
27+
onSelect: (id: string) => void;
28+
onDelete: (id: string) => void;
2929
}
3030

3131
/**
3232
* One column rendered as a row in the schema list: a generation-step number, an icon badge,
3333
* the column name, a type badge, a one-line summary, and its relationship tags. Selecting the
3434
* row opens the same config pane the DAG canvas uses; the trailing button deletes the column.
3535
*/
36-
export const SchemaRow: FC<SchemaRowProps> = ({
36+
export const SchemaRow: FC<SchemaRowProps> = memo(function SchemaRow({
3737
column,
3838
references,
3939
selected,
4040
onSelect,
4141
onDelete,
42-
}) => {
42+
}) {
4343
const { option } = column;
4444
const { typeLabel, detail } = describeColumn(column);
4545
const Icon = option.icon ?? Box;
@@ -54,7 +54,7 @@ export const SchemaRow: FC<SchemaRowProps> = ({
5454
<button
5555
type="button"
5656
data-select=""
57-
onClick={onSelect}
57+
onClick={() => onSelect(column.id)}
5858
aria-pressed={selected}
5959
className="flex min-w-0 flex-1 items-center gap-density-md px-density-lg py-density-md text-left focus-visible:outline-none cursor-pointer"
6060
>
@@ -93,7 +93,7 @@ export const SchemaRow: FC<SchemaRowProps> = ({
9393
kind="tertiary"
9494
color="danger"
9595
size="tiny"
96-
onClick={onDelete}
96+
onClick={() => onDelete(column.id)}
9797
aria-label={`Delete ${column.name || option.label}`}
9898
className="h-full rounded-none opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100"
9999
>
@@ -102,4 +102,4 @@ export const SchemaRow: FC<SchemaRowProps> = ({
102102
</Flex>
103103
</Flex>
104104
);
105-
};
105+
});

web/packages/studio/src/routes/DataDesignerJobBuildRoute/index.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,16 @@ export const DataDesignerJobBuildRoute: FC = () => {
140140
getCurrentConfig,
141141
});
142142

143-
const handlePreview = () => {
143+
const handlePreview = useCallback(() => {
144144
if (validateAndCollectErrors().length > 0) return;
145145
setIsDetailsOpen(true);
146146
void runPreview();
147-
};
147+
}, [validateAndCollectErrors, runPreview]);
148148

149149
const createJob = useDataDesignerCreateJob();
150150
const submitError = createJob.error ? getErrorMessage(createJob.error) : null;
151151

152-
const handleSubmit = async () => {
152+
const handleSubmit = useCallback(async () => {
153153
if (validateAndCollectErrors().length > 0) return;
154154
const { columns, models, name, rows } = builder.getBuilderValues();
155155

@@ -173,7 +173,17 @@ export const DataDesignerJobBuildRoute: FC = () => {
173173
setIsDetailsOpen(true);
174174
// Error surfaced via createJob.error / submitError below.
175175
}
176-
};
176+
}, [validateAndCollectErrors, builder, createJob, workspace, servedModelNames, navigate]);
177+
178+
const toggleDetails = useCallback(() => setIsDetailsOpen((open) => !open), []);
179+
const onColumnRemove = useCallback(() => {
180+
if (builder.selectedColumnId) builder.removeColumn(builder.selectedColumnId);
181+
}, [builder]);
182+
const onColumnClose = useCallback(() => builder.selectColumn(null), [builder]);
183+
const onModelRemove = useCallback(() => {
184+
if (builder.selectedModelId) builder.removeModel(builder.selectedModelId);
185+
}, [builder]);
186+
const onModelClose = useCallback(() => builder.selectModel(null), [builder]);
177187

178188
return (
179189
<AccessibleTitle title={heading}>
@@ -195,7 +205,7 @@ export const DataDesignerJobBuildRoute: FC = () => {
195205
submitError={submitError}
196206
previewLogs={previewLogs}
197207
isOpen={isDetailsOpen}
198-
onToggle={() => setIsDetailsOpen((open) => !open)}
208+
onToggle={toggleDetails}
199209
/>
200210

201211
<Flex className="min-h-0 border-t border-base h-full">
@@ -229,14 +239,10 @@ export const DataDesignerJobBuildRoute: FC = () => {
229239
selectedColumnId={builder.selectedColumnId}
230240
selectedModelId={builder.selectedModelId}
231241
workspace={workspace}
232-
onColumnRemove={() =>
233-
builder.selectedColumnId && builder.removeColumn(builder.selectedColumnId)
234-
}
235-
onColumnClose={() => builder.selectColumn(null)}
236-
onModelRemove={() =>
237-
builder.selectedModelId && builder.removeModel(builder.selectedModelId)
238-
}
239-
onModelClose={() => builder.selectModel(null)}
242+
onColumnRemove={onColumnRemove}
243+
onColumnClose={onColumnClose}
244+
onModelRemove={onModelRemove}
245+
onModelClose={onModelClose}
240246
/>
241247
</Flex>
242248
</Stack>

0 commit comments

Comments
 (0)