Skip to content

Commit 6dd09c2

Browse files
drfarrellclaude
andcommitted
perf: optimize element selection responsiveness for instant red outline
Prioritize visual feedback by rendering the selection outline immediately, before triggering heavy operations like chat context updates and UI animations. Key optimizations: - Split click handling to render outline first, then update state - Defer chat context calculations with requestAnimationFrame - Reduce animation durations across UI components (400ms → 150ms) - Optimize overlay refresh debounce timing (100ms → 50ms) - Batch MobX updates with runInAction to prevent re-renders This significantly improves responsiveness, especially for large projects with many branches where context calculations were blocking the UI. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent afd9a39 commit 6dd09c2

File tree

7 files changed

+50
-37
lines changed

7 files changed

+50
-37
lines changed

apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const OverlayChatInput = observer(({
4040
return (
4141
<div
4242
className={cn(
43-
'rounded-xl backdrop-blur-lg transition-all duration-300',
43+
'rounded-xl backdrop-blur-lg transition-all duration-150',
4444
'shadow-xl shadow-background-secondary/50',
4545
inputState.isInputting
4646
? 'bg-background/80 border shadow-xl shadow-background-secondary/50 p-1'

apps/web/client/src/app/project/[id]/_components/canvas/overlay/elements/buttons/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ export const OverlayButtons = observer(() => {
4343
}, [chatPosition.x, chatPosition.y]);
4444

4545
const animationClass =
46-
'origin-center opacity-0 -translate-y-2 transition-all duration-200';
46+
'origin-center opacity-0 -translate-y-1 transition-all duration-100';
4747

4848
useEffect(() => {
4949
if (domId) {
5050
requestAnimationFrame(() => {
5151
const element = document.querySelector(`[data-element-id="${domId}"]`);
5252
if (element) {
53-
element.classList.remove('scale-[0.2]', 'opacity-0', '-translate-y-2');
53+
element.classList.remove('scale-[0.2]', 'opacity-0', '-translate-y-1');
5454
element.classList.add('scale-100', 'opacity-100', 'translate-y-0');
5555
}
5656
});
@@ -72,7 +72,7 @@ export const OverlayButtons = observer(() => {
7272
transform: 'translate(-50%, 0)',
7373
transformOrigin: 'center center',
7474
pointerEvents: 'auto',
75-
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
75+
transition: 'all 0.15s ease-out',
7676
};
7777

7878
return (

apps/web/client/src/app/project/[id]/_components/canvas/overlay/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const Overlay = observer(() => {
4141
id={EditorAttributes.OVERLAY_CONTAINER_ID}
4242
className={cn(
4343
'absolute top-0 left-0 h-0 w-0 pointer-events-none',
44-
editorEngine.state.shouldHideOverlay ? 'opacity-0' : 'opacity-100 transition-opacity duration-150',
44+
editorEngine.state.shouldHideOverlay ? 'opacity-0' : 'opacity-100 transition-opacity duration-75',
4545
editorEngine.state.editorMode === EditorMode.PREVIEW && 'hidden',
4646
)}
4747
>

apps/web/client/src/app/project/[id]/_components/editor-bar/index.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,19 +93,16 @@ export const EditorBar = observer(({ availableWidth }: { availableWidth?: number
9393
return (
9494
<DropdownManagerProvider>
9595
<motion.div
96-
initial={{ opacity: 0, y: 20 }}
96+
initial={{ opacity: 0, y: 8 }}
9797
animate={{ opacity: 1, y: 0 }}
98-
exit={{ opacity: 0, y: 20 }}
98+
exit={{ opacity: 0, y: 8 }}
9999
className={cn(
100100
'flex flex-col border-[0.5px] border-border p-1 px-1 bg-background rounded-xl backdrop-blur drop-shadow-xl z-50 overflow-hidden',
101101
editorEngine.state.editorMode === EditorMode.PREVIEW && !windowSelected && 'hidden',
102102
)}
103103
transition={{
104-
type: 'spring',
105-
bounce: 0.1,
106-
duration: 0.4,
107-
stiffness: 200,
108-
damping: 25,
104+
duration: 0.15,
105+
ease: 'easeOut',
109106
}}
110107
>
111108
{getTopBar()}

apps/web/client/src/components/store/editor/chat/context.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ export class ChatContext {
2525
init() {
2626
this.selectedReactionDisposer = reaction(
2727
() => this.editorEngine.elements.selected,
28-
() => this.getChatContext().then((context) => (this.context = context)),
28+
() => {
29+
// Defer context update to prevent blocking UI
30+
requestAnimationFrame(() => {
31+
this.getChatContext().then((context) => (this.context = context));
32+
});
33+
},
2934
);
3035
}
3136

apps/web/client/src/components/store/editor/element/index.ts

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { CoreElementType, DomElement, DynamicType } from '@onlook/models';
22
import type { RemoveElementAction } from '@onlook/models/actions';
33
import { toast } from '@onlook/ui/sonner';
4-
import { makeAutoObservable } from 'mobx';
4+
import { makeAutoObservable, runInAction } from 'mobx';
55
import type { EditorEngine } from '../engine';
66
import type { FrameData } from '../frames';
77
import { adaptRectToCanvas } from '../overlay/utils';
@@ -60,30 +60,41 @@ export class ElementsManager {
6060
}
6161

6262
click(domEls: DomElement[]) {
63-
this.editorEngine.overlay.state.removeClickRects();
64-
this.clearSelectedElements();
65-
66-
for (const domEl of domEls) {
67-
const frameData = this.editorEngine.frames.get(domEl.frameId);
68-
if (!frameData) {
69-
console.error('Frame data not found');
70-
continue;
63+
// Batch all synchronous updates for the outline rendering
64+
runInAction(() => {
65+
// Immediately update the visual outline for instant feedback
66+
this.editorEngine.overlay.state.removeClickRects();
67+
68+
// Process outline rendering first for immediate visual feedback
69+
for (const domEl of domEls) {
70+
const frameData = this.editorEngine.frames.get(domEl.frameId);
71+
if (!frameData) {
72+
console.error('Frame data not found');
73+
continue;
74+
}
75+
const { view } = frameData;
76+
if (!view) {
77+
console.error('No frame view found');
78+
continue;
79+
}
80+
const adjustedRect = adaptRectToCanvas(domEl.rect, view);
81+
const isComponent = !!domEl.instanceId;
82+
this.editorEngine.overlay.state.addClickRect(
83+
adjustedRect,
84+
domEl.styles,
85+
isComponent,
86+
domEl.domId,
87+
);
7188
}
72-
const { view } = frameData;
73-
if (!view) {
74-
console.error('No frame view found');
75-
continue;
89+
});
90+
91+
// Update selected elements in a separate action to control reaction timing
92+
runInAction(() => {
93+
this.clearSelectedElements();
94+
for (const domEl of domEls) {
95+
this._selected.push(domEl);
7696
}
77-
const adjustedRect = adaptRectToCanvas(domEl.rect, view);
78-
const isComponent = !!domEl.instanceId;
79-
this.editorEngine.overlay.state.addClickRect(
80-
adjustedRect,
81-
domEl.styles,
82-
isComponent,
83-
domEl.domId,
84-
);
85-
this._selected.push(domEl);
86-
}
97+
});
8798
}
8899

89100
setHoveredElement(element: DomElement) {

apps/web/client/src/components/store/editor/overlay/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class OverlayManager {
7979
}
8080
};
8181

82-
refresh = debounce(this.undebouncedRefresh, 100, { leading: true });
82+
refresh = debounce(this.undebouncedRefresh, 50, { leading: true });
8383

8484
showMeasurement() {
8585
this.editorEngine.overlay.removeMeasurement();

0 commit comments

Comments
 (0)