Skip to content

Commit e768a3b

Browse files
perf(ui): use narrow selectors in adjustments to reduce rerenders
dramatically improves the feel of the sliders
1 parent 7273700 commit e768a3b

File tree

3 files changed

+118
-92
lines changed

3 files changed

+118
-92
lines changed

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerAdjustmentsPanel.tsx

Lines changed: 48 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,56 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerP
77
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
88
import {
99
rasterLayerAdjustmentsCancel,
10+
rasterLayerAdjustmentsCollapsedToggled,
11+
rasterLayerAdjustmentsEnabledToggled,
12+
rasterLayerAdjustmentsModeChanged,
1013
rasterLayerAdjustmentsReset,
1114
rasterLayerAdjustmentsSet,
1215
} from 'features/controlLayers/store/canvasSlice';
1316
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
14-
import { makeDefaultRasterLayerAdjustments } from 'features/controlLayers/store/util';
1517
import React, { memo, useCallback, useMemo } from 'react';
1618
import { useTranslation } from 'react-i18next';
1719
import { PiArrowCounterClockwiseBold, PiCaretDownBold, PiCheckBold, PiTrashBold } from 'react-icons/pi';
1820

1921
export const RasterLayerAdjustmentsPanel = memo(() => {
22+
const { t } = useTranslation();
2023
const dispatch = useAppDispatch();
2124
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
2225
const canvasManager = useCanvasManager();
23-
const selectAdjustments = useMemo(() => {
24-
return createSelector(selectCanvasSlice, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments);
26+
27+
const selectHasAdjustments = useMemo(() => {
28+
return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments));
2529
}, [entityIdentifier]);
2630

27-
const adjustments = useAppSelector(selectAdjustments);
28-
const { t } = useTranslation();
31+
const hasAdjustments = useAppSelector(selectHasAdjustments);
2932

30-
const hasAdjustments = Boolean(adjustments);
31-
const enabled = Boolean(adjustments?.enabled);
32-
const collapsed = Boolean(adjustments?.collapsed);
33-
const mode = adjustments?.mode ?? 'simple';
34-
35-
const onToggleEnabled = useCallback(
36-
(e: React.ChangeEvent<HTMLInputElement>) => {
37-
const v = e.target.checked;
38-
const current = adjustments ?? makeDefaultRasterLayerAdjustments(mode);
39-
dispatch(
40-
rasterLayerAdjustmentsSet({
41-
entityIdentifier,
42-
adjustments: { ...current, enabled: v },
43-
})
44-
);
45-
},
46-
[dispatch, entityIdentifier, adjustments, mode]
47-
);
33+
const selectMode = useMemo(() => {
34+
return createSelector(
35+
selectCanvasSlice,
36+
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple'
37+
);
38+
}, [entityIdentifier]);
39+
const mode = useAppSelector(selectMode);
40+
41+
const selectEnabled = useMemo(() => {
42+
return createSelector(
43+
selectCanvasSlice,
44+
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false
45+
);
46+
}, [entityIdentifier]);
47+
const enabled = useAppSelector(selectEnabled);
48+
49+
const selectCollapsed = useMemo(() => {
50+
return createSelector(
51+
selectCanvasSlice,
52+
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.collapsed ?? false
53+
);
54+
}, [entityIdentifier]);
55+
const collapsed = useAppSelector(selectCollapsed);
56+
57+
const onToggleEnabled = useCallback(() => {
58+
dispatch(rasterLayerAdjustmentsEnabledToggled({ entityIdentifier }));
59+
}, [dispatch, entityIdentifier]);
4860

4961
const onReset = useCallback(() => {
5062
// Reset values to defaults but keep adjustments present; preserve enabled/collapsed/mode
@@ -57,34 +69,18 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
5769
}, [dispatch, entityIdentifier]);
5870

5971
const onToggleCollapsed = useCallback(() => {
60-
const current = adjustments ?? makeDefaultRasterLayerAdjustments(mode);
61-
dispatch(
62-
rasterLayerAdjustmentsSet({
63-
entityIdentifier,
64-
adjustments: { ...current, collapsed: !collapsed },
65-
})
66-
);
67-
}, [dispatch, entityIdentifier, collapsed, adjustments, mode]);
68-
69-
const onSetMode = useCallback(
70-
(nextMode: 'simple' | 'curves') => {
71-
if (nextMode === mode) {
72-
return;
73-
}
74-
const current = adjustments ?? makeDefaultRasterLayerAdjustments(nextMode);
75-
dispatch(
76-
rasterLayerAdjustmentsSet({
77-
entityIdentifier,
78-
adjustments: { ...current, mode: nextMode },
79-
})
80-
);
81-
},
82-
[dispatch, entityIdentifier, adjustments, mode]
72+
dispatch(rasterLayerAdjustmentsCollapsedToggled({ entityIdentifier }));
73+
}, [dispatch, entityIdentifier]);
74+
75+
const onClickModeSimple = useCallback(
76+
() => dispatch(rasterLayerAdjustmentsModeChanged({ entityIdentifier, mode: 'simple' })),
77+
[dispatch, entityIdentifier]
8378
);
8479

85-
// Memoized click handlers to avoid inline arrow functions in JSX
86-
const onClickModeSimple = useCallback(() => onSetMode('simple'), [onSetMode]);
87-
const onClickModeCurves = useCallback(() => onSetMode('curves'), [onSetMode]);
80+
const onClickModeCurves = useCallback(
81+
() => dispatch(rasterLayerAdjustmentsModeChanged({ entityIdentifier, mode: 'curves' })),
82+
[dispatch, entityIdentifier]
83+
);
8884

8985
const onFinish = useCallback(async () => {
9086
// Bake current visual into layer pixels, then clear adjustments
@@ -137,7 +133,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
137133
aria-label={t('controlLayers.adjustments.cancel')}
138134
size="md"
139135
onClick={onCancel}
140-
isDisabled={!adjustments}
136+
isDisabled={!hasAdjustments}
141137
colorScheme="red"
142138
icon={<PiTrashBold />}
143139
variant="ghost"
@@ -146,15 +142,15 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
146142
aria-label={t('controlLayers.adjustments.reset')}
147143
size="md"
148144
onClick={onReset}
149-
isDisabled={!adjustments}
145+
isDisabled={!hasAdjustments}
150146
icon={<PiArrowCounterClockwiseBold />}
151147
variant="ghost"
152148
/>
153149
<IconButton
154150
aria-label={t('controlLayers.adjustments.finish')}
155151
size="md"
156152
onClick={onFinish}
157-
isDisabled={!adjustments}
153+
isDisabled={!hasAdjustments}
158154
colorScheme="green"
159155
icon={<PiCheckBold />}
160156
variant="ghost"

invokeai/frontend/web/src/features/controlLayers/components/RasterLayer/RasterLayerSimpleAdjustmentsEditor.tsx

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,40 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
44
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
55
import { rasterLayerAdjustmentsSimpleUpdated } from 'features/controlLayers/store/canvasSlice';
66
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
7+
import type { SimpleAdjustmentsConfig } from 'features/controlLayers/store/types';
78
import React, { memo, useCallback, useMemo } from 'react';
89
import { useTranslation } from 'react-i18next';
910

1011
type AdjustmentSliderRowProps = {
1112
label: string;
12-
value: number;
13+
name: keyof SimpleAdjustmentsConfig;
1314
onChange: (v: number) => void;
1415
min?: number;
1516
max?: number;
1617
step?: number;
1718
};
1819

19-
const AdjustmentSliderRow = ({ label, value, onChange, min = -1, max = 1, step = 0.01 }: AdjustmentSliderRowProps) => (
20-
<FormControl orientation="horizontal" mb={1} w="full">
21-
<FormLabel m={0} minW="90px">
22-
{label}
23-
</FormLabel>
24-
<CompositeSlider value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} marks />
25-
<CompositeNumberInput value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} />
26-
</FormControl>
27-
);
20+
const AdjustmentSliderRow = ({ label, name, onChange, min = -1, max = 1, step = 0.01 }: AdjustmentSliderRowProps) => {
21+
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
22+
const selectValue = useMemo(() => {
23+
return createSelector(
24+
selectCanvasSlice,
25+
(canvas) =>
26+
selectEntity(canvas, entityIdentifier)?.adjustments?.simple?.[name] ?? DEFAULT_SIMPLE_ADJUSTMENTS[name]
27+
);
28+
}, [entityIdentifier, name]);
29+
const value = useAppSelector(selectValue);
30+
31+
return (
32+
<FormControl orientation="horizontal" mb={1} w="full">
33+
<FormLabel m={0} minW="90px">
34+
{label}
35+
</FormLabel>
36+
<CompositeSlider value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} marks />
37+
<CompositeNumberInput value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} />
38+
</FormControl>
39+
);
40+
};
2841

2942
const DEFAULT_SIMPLE_ADJUSTMENTS = {
3043
brightness: 0,
@@ -39,13 +52,6 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => {
3952
const dispatch = useAppDispatch();
4053
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
4154
const { t } = useTranslation();
42-
const selectSimpleAdjustments = useMemo(() => {
43-
return createSelector(
44-
selectCanvasSlice,
45-
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.simple ?? DEFAULT_SIMPLE_ADJUSTMENTS
46-
);
47-
}, [entityIdentifier]);
48-
const simple = useAppSelector(selectSimpleAdjustments);
4955
const selectIsDisabled = useMemo(() => {
5056
return createSelector(
5157
selectCanvasSlice,
@@ -83,28 +89,24 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => {
8389
<Flex px={3} pb={2} direction="column" opacity={isDisabled ? 0.3 : 1} pointerEvents={isDisabled ? 'none' : 'auto'}>
8490
<AdjustmentSliderRow
8591
label={t('controlLayers.adjustments.brightness')}
86-
value={simple.brightness}
92+
name="brightness"
8793
onChange={onBrightness}
8894
/>
89-
<AdjustmentSliderRow
90-
label={t('controlLayers.adjustments.contrast')}
91-
value={simple.contrast}
92-
onChange={onContrast}
93-
/>
95+
<AdjustmentSliderRow label={t('controlLayers.adjustments.contrast')} name="contrast" onChange={onContrast} />
9496
<AdjustmentSliderRow
9597
label={t('controlLayers.adjustments.saturation')}
96-
value={simple.saturation}
98+
name="saturation"
9799
onChange={onSaturation}
98100
/>
99101
<AdjustmentSliderRow
100102
label={t('controlLayers.adjustments.temperature')}
101-
value={simple.temperature}
103+
name="temperature"
102104
onChange={onTemperature}
103105
/>
104-
<AdjustmentSliderRow label={t('controlLayers.adjustments.tint')} value={simple.tint} onChange={onTint} />
106+
<AdjustmentSliderRow label={t('controlLayers.adjustments.tint')} name="tint" onChange={onTint} />
105107
<AdjustmentSliderRow
106108
label={t('controlLayers.adjustments.sharpness')}
107-
value={simple.sharpness}
109+
name="sharpness"
108110
onChange={onSharpness}
109111
min={0}
110112
max={1}

invokeai/frontend/web/src/features/controlLayers/store/canvasSlice.ts

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,11 @@ const slice = createSlice({
130130
rasterLayerAdjustmentsReset: (state, action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>) => {
131131
const { entityIdentifier } = action.payload;
132132
const layer = selectEntity(state, entityIdentifier);
133-
if (!layer) {
133+
if (!layer?.adjustments) {
134134
return;
135135
}
136-
if (layer.adjustments) {
137-
layer.adjustments.simple = makeDefaultRasterLayerAdjustments('simple').simple;
138-
layer.adjustments.curves = makeDefaultRasterLayerAdjustments('curves').curves;
139-
}
136+
layer.adjustments.simple = makeDefaultRasterLayerAdjustments('simple').simple;
137+
layer.adjustments.curves = makeDefaultRasterLayerAdjustments('curves').curves;
140138
},
141139
rasterLayerAdjustmentsCancel: (state, action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>) => {
142140
const { entityIdentifier } = action.payload;
@@ -146,18 +144,26 @@ const slice = createSlice({
146144
}
147145
delete layer.adjustments;
148146
},
147+
rasterLayerAdjustmentsModeChanged: (
148+
state,
149+
action: PayloadAction<EntityIdentifierPayload<{ mode: 'simple' | 'curves' }, 'raster_layer'>>
150+
) => {
151+
const { entityIdentifier, mode } = action.payload;
152+
const layer = selectEntity(state, entityIdentifier);
153+
if (!layer?.adjustments) {
154+
return;
155+
}
156+
layer.adjustments.mode = mode;
157+
},
149158
rasterLayerAdjustmentsSimpleUpdated: (
150159
state,
151160
action: PayloadAction<EntityIdentifierPayload<{ simple: Partial<SimpleAdjustmentsConfig> }, 'raster_layer'>>
152161
) => {
153162
const { entityIdentifier, simple } = action.payload;
154163
const layer = selectEntity(state, entityIdentifier);
155-
if (!layer) {
164+
if (!layer?.adjustments) {
156165
return;
157166
}
158-
if (!layer.adjustments) {
159-
layer.adjustments = makeDefaultRasterLayerAdjustments('simple');
160-
}
161167
layer.adjustments.simple = merge(layer.adjustments.simple, simple);
162168
},
163169
rasterLayerAdjustmentsCurvesUpdated: (
@@ -166,14 +172,33 @@ const slice = createSlice({
166172
) => {
167173
const { entityIdentifier, channel, points } = action.payload;
168174
const layer = selectEntity(state, entityIdentifier);
169-
if (!layer) {
175+
if (!layer?.adjustments) {
170176
return;
171177
}
172-
if (!layer.adjustments) {
173-
layer.adjustments = makeDefaultRasterLayerAdjustments('curves');
174-
}
175178
layer.adjustments.curves[channel] = points;
176179
},
180+
rasterLayerAdjustmentsEnabledToggled: (
181+
state,
182+
action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>
183+
) => {
184+
const { entityIdentifier } = action.payload;
185+
const layer = selectEntity(state, entityIdentifier);
186+
if (!layer?.adjustments) {
187+
return;
188+
}
189+
layer.adjustments.enabled = !layer.adjustments.enabled;
190+
},
191+
rasterLayerAdjustmentsCollapsedToggled: (
192+
state,
193+
action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>
194+
) => {
195+
const { entityIdentifier } = action.payload;
196+
const layer = selectEntity(state, entityIdentifier);
197+
if (!layer?.adjustments) {
198+
return;
199+
}
200+
layer.adjustments.collapsed = !layer.adjustments.collapsed;
201+
},
177202
rasterLayerAdded: {
178203
reducer: (
179204
state,
@@ -1732,6 +1757,9 @@ export const {
17321757
rasterLayerAdjustmentsSet,
17331758
rasterLayerAdjustmentsCancel,
17341759
rasterLayerAdjustmentsReset,
1760+
rasterLayerAdjustmentsModeChanged,
1761+
rasterLayerAdjustmentsEnabledToggled,
1762+
rasterLayerAdjustmentsCollapsedToggled,
17351763
rasterLayerAdjustmentsSimpleUpdated,
17361764
rasterLayerAdjustmentsCurvesUpdated,
17371765
entityDeleted,

0 commit comments

Comments
 (0)