Skip to content

Commit 6756a8b

Browse files
authored
refactor(ui): Optimize rendering performance (#8239)
1 parent d54cdd8 commit 6756a8b

File tree

13 files changed

+501
-87
lines changed

13 files changed

+501
-87
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.7.0-nightly.20250917.0b10ba2c"
1818
},
1919
"scripts": {
20-
"start": "node scripts/start.js",
20+
"start": "cross-env node scripts/start.js",
2121
"start:a2a-server": "CODER_AGENT_PORT=41242 npm run start --workspace @google/gemini-cli-a2a-server",
2222
"debug": "cross-env DEBUG=1 node --inspect-brk scripts/start.js",
2323
"auth:npm": "npx google-artifactregistry-auth",

packages/cli/src/gemini.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import React from 'react';
78
import { render } from 'ink';
89
import { AppContainer } from './ui/AppContainer.js';
910
import { loadCliConfig, parseArguments } from './config/config.js';
@@ -212,10 +213,19 @@ export async function startInteractiveUI(
212213
);
213214
};
214215

215-
const instance = render(<AppWrapper />, {
216-
exitOnCtrlC: false,
217-
isScreenReaderEnabled: config.getScreenReader(),
218-
});
216+
const instance = render(
217+
process.env['DEBUG'] ? (
218+
<React.StrictMode>
219+
<AppWrapper />
220+
</React.StrictMode>
221+
) : (
222+
<AppWrapper />
223+
),
224+
{
225+
exitOnCtrlC: false,
226+
isScreenReaderEnabled: config.getScreenReader(),
227+
},
228+
);
219229

220230
checkForUpdates()
221231
.then((info) => {

packages/cli/src/ui/AppContainer.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
8585
import { useWorkspaceMigration } from './hooks/useWorkspaceMigration.js';
8686
import { useSessionStats } from './contexts/SessionContext.js';
8787
import { useGitBranchName } from './hooks/useGitBranchName.js';
88+
import { FocusContext } from './contexts/FocusContext.js';
8889
import type { ExtensionUpdateState } from './state/extensions.js';
8990
import { checkForAllExtensionUpdates } from '../config/extension.js';
9091

@@ -1210,7 +1211,9 @@ Logging in with Google... Please restart Gemini CLI to continue.
12101211
startupWarnings: props.startupWarnings || [],
12111212
}}
12121213
>
1213-
<App />
1214+
<FocusContext.Provider value={isFocused}>
1215+
<App />
1216+
</FocusContext.Provider>
12141217
</AppContext.Provider>
12151218
</ConfigContext.Provider>
12161219
</UIActionsContext.Provider>

packages/cli/src/ui/components/Composer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { OverflowProvider } from '../contexts/OverflowContext.js';
1818
import { theme } from '../semantic-colors.js';
1919
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
2020
import { useUIState } from '../contexts/UIStateContext.js';
21+
import { useFocusState } from '../contexts/FocusContext.js';
2122
import { useUIActions } from '../contexts/UIActionsContext.js';
2223
import { useVimMode } from '../contexts/VimModeContext.js';
2324
import { useConfig } from '../contexts/ConfigContext.js';
@@ -32,6 +33,7 @@ export const Composer = () => {
3233
const config = useConfig();
3334
const settings = useSettings();
3435
const uiState = useUIState();
36+
const isFocused = useFocusState();
3537
const uiActions = useUIActions();
3638
const { vimEnabled, vimMode } = useVimMode();
3739
const terminalWidth = process.stdout.columns;
@@ -192,7 +194,7 @@ export const Composer = () => {
192194
setShellModeActive={uiActions.setShellModeActive}
193195
approvalMode={showAutoAcceptIndicator}
194196
onEscapePromptChange={uiActions.onEscapePromptChange}
195-
focus={uiState.isFocused}
197+
focus={isFocused}
196198
vimHandleInput={uiActions.vimHandleInput}
197199
isShellFocused={uiState.shellFocused}
198200
placeholder={

packages/cli/src/ui/components/shared/text-buffer.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,53 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
14951495
expect(stripAnsi('')).toBe('');
14961496
});
14971497
});
1498+
1499+
describe('Memoization', () => {
1500+
it('should keep action references stable across re-renders', () => {
1501+
// We pass a stable `isValidPath` so that callbacks that depend on it
1502+
// are not recreated on every render.
1503+
const isValidPath = () => false;
1504+
const { result, rerender } = renderHook(() =>
1505+
useTextBuffer({ viewport, isValidPath }),
1506+
);
1507+
1508+
const initialInsert = result.current.insert;
1509+
const initialBackspace = result.current.backspace;
1510+
const initialMove = result.current.move;
1511+
const initialHandleInput = result.current.handleInput;
1512+
1513+
rerender();
1514+
1515+
expect(result.current.insert).toBe(initialInsert);
1516+
expect(result.current.backspace).toBe(initialBackspace);
1517+
expect(result.current.move).toBe(initialMove);
1518+
expect(result.current.handleInput).toBe(initialHandleInput);
1519+
});
1520+
1521+
it('should have memoized actions that operate on the latest state', () => {
1522+
const isValidPath = () => false;
1523+
const { result } = renderHook(() =>
1524+
useTextBuffer({ viewport, isValidPath }),
1525+
);
1526+
1527+
// Store a reference to the memoized insert function.
1528+
const memoizedInsert = result.current.insert;
1529+
1530+
// Update the buffer state.
1531+
act(() => {
1532+
result.current.insert('hello');
1533+
});
1534+
expect(getBufferState(result).text).toBe('hello');
1535+
1536+
// Now, call the original memoized function reference.
1537+
act(() => {
1538+
memoizedInsert(' world');
1539+
});
1540+
1541+
// It should have operated on the updated state.
1542+
expect(getBufferState(result).text).toBe('hello world');
1543+
});
1544+
});
14981545
});
14991546

15001547
describe('offsetToLogicalPos', () => {

packages/cli/src/ui/components/shared/text-buffer.ts

Lines changed: 128 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,71 +1984,135 @@ export function useTextBuffer({
19841984
dispatch({ type: 'move_to_offset', payload: { offset } });
19851985
}, []);
19861986

1987-
const returnValue: TextBuffer = {
1988-
lines,
1989-
text,
1990-
cursor: [cursorRow, cursorCol],
1991-
preferredCol,
1992-
selectionAnchor,
1993-
1994-
allVisualLines: visualLines,
1995-
viewportVisualLines: renderedVisualLines,
1996-
visualCursor,
1997-
visualScrollRow,
1998-
visualToLogicalMap,
1987+
const returnValue: TextBuffer = useMemo(
1988+
() => ({
1989+
lines,
1990+
text,
1991+
cursor: [cursorRow, cursorCol],
1992+
preferredCol,
1993+
selectionAnchor,
1994+
1995+
allVisualLines: visualLines,
1996+
viewportVisualLines: renderedVisualLines,
1997+
visualCursor,
1998+
visualScrollRow,
1999+
visualToLogicalMap,
2000+
2001+
setText,
2002+
insert,
2003+
newline,
2004+
backspace,
2005+
del,
2006+
move,
2007+
undo,
2008+
redo,
2009+
replaceRange,
2010+
replaceRangeByOffset,
2011+
moveToOffset,
2012+
deleteWordLeft,
2013+
deleteWordRight,
19992014

2000-
setText,
2001-
insert,
2002-
newline,
2003-
backspace,
2004-
del,
2005-
move,
2006-
undo,
2007-
redo,
2008-
replaceRange,
2009-
replaceRangeByOffset,
2010-
moveToOffset,
2011-
deleteWordLeft,
2012-
deleteWordRight,
2013-
2014-
killLineRight,
2015-
killLineLeft,
2016-
handleInput,
2017-
openInExternalEditor,
2018-
// Vim-specific operations
2019-
vimDeleteWordForward,
2020-
vimDeleteWordBackward,
2021-
vimDeleteWordEnd,
2022-
vimChangeWordForward,
2023-
vimChangeWordBackward,
2024-
vimChangeWordEnd,
2025-
vimDeleteLine,
2026-
vimChangeLine,
2027-
vimDeleteToEndOfLine,
2028-
vimChangeToEndOfLine,
2029-
vimChangeMovement,
2030-
vimMoveLeft,
2031-
vimMoveRight,
2032-
vimMoveUp,
2033-
vimMoveDown,
2034-
vimMoveWordForward,
2035-
vimMoveWordBackward,
2036-
vimMoveWordEnd,
2037-
vimDeleteChar,
2038-
vimInsertAtCursor,
2039-
vimAppendAtCursor,
2040-
vimOpenLineBelow,
2041-
vimOpenLineAbove,
2042-
vimAppendAtLineEnd,
2043-
vimInsertAtLineStart,
2044-
vimMoveToLineStart,
2045-
vimMoveToLineEnd,
2046-
vimMoveToFirstNonWhitespace,
2047-
vimMoveToFirstLine,
2048-
vimMoveToLastLine,
2049-
vimMoveToLine,
2050-
vimEscapeInsertMode,
2051-
};
2015+
killLineRight,
2016+
killLineLeft,
2017+
handleInput,
2018+
openInExternalEditor,
2019+
// Vim-specific operations
2020+
vimDeleteWordForward,
2021+
vimDeleteWordBackward,
2022+
vimDeleteWordEnd,
2023+
vimChangeWordForward,
2024+
vimChangeWordBackward,
2025+
vimChangeWordEnd,
2026+
vimDeleteLine,
2027+
vimChangeLine,
2028+
vimDeleteToEndOfLine,
2029+
vimChangeToEndOfLine,
2030+
vimChangeMovement,
2031+
vimMoveLeft,
2032+
vimMoveRight,
2033+
vimMoveUp,
2034+
vimMoveDown,
2035+
vimMoveWordForward,
2036+
vimMoveWordBackward,
2037+
vimMoveWordEnd,
2038+
vimDeleteChar,
2039+
vimInsertAtCursor,
2040+
vimAppendAtCursor,
2041+
vimOpenLineBelow,
2042+
vimOpenLineAbove,
2043+
vimAppendAtLineEnd,
2044+
vimInsertAtLineStart,
2045+
vimMoveToLineStart,
2046+
vimMoveToLineEnd,
2047+
vimMoveToFirstNonWhitespace,
2048+
vimMoveToFirstLine,
2049+
vimMoveToLastLine,
2050+
vimMoveToLine,
2051+
vimEscapeInsertMode,
2052+
}),
2053+
[
2054+
lines,
2055+
text,
2056+
cursorRow,
2057+
cursorCol,
2058+
preferredCol,
2059+
selectionAnchor,
2060+
visualLines,
2061+
renderedVisualLines,
2062+
visualCursor,
2063+
visualScrollRow,
2064+
setText,
2065+
insert,
2066+
newline,
2067+
backspace,
2068+
del,
2069+
move,
2070+
undo,
2071+
redo,
2072+
replaceRange,
2073+
replaceRangeByOffset,
2074+
moveToOffset,
2075+
deleteWordLeft,
2076+
deleteWordRight,
2077+
killLineRight,
2078+
killLineLeft,
2079+
handleInput,
2080+
openInExternalEditor,
2081+
vimDeleteWordForward,
2082+
vimDeleteWordBackward,
2083+
vimDeleteWordEnd,
2084+
vimChangeWordForward,
2085+
vimChangeWordBackward,
2086+
vimChangeWordEnd,
2087+
vimDeleteLine,
2088+
vimChangeLine,
2089+
vimDeleteToEndOfLine,
2090+
vimChangeToEndOfLine,
2091+
vimChangeMovement,
2092+
vimMoveLeft,
2093+
vimMoveRight,
2094+
vimMoveUp,
2095+
vimMoveDown,
2096+
vimMoveWordForward,
2097+
vimMoveWordBackward,
2098+
vimMoveWordEnd,
2099+
vimDeleteChar,
2100+
vimInsertAtCursor,
2101+
vimAppendAtCursor,
2102+
vimOpenLineBelow,
2103+
vimOpenLineAbove,
2104+
vimAppendAtLineEnd,
2105+
vimInsertAtLineStart,
2106+
vimMoveToLineStart,
2107+
vimMoveToLineEnd,
2108+
vimMoveToFirstNonWhitespace,
2109+
vimMoveToFirstLine,
2110+
vimMoveToLastLine,
2111+
vimMoveToLine,
2112+
vimEscapeInsertMode,
2113+
visualToLogicalMap,
2114+
],
2115+
);
20522116
return returnValue;
20532117
}
20542118

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { createContext, useContext } from 'react';
8+
9+
export const FocusContext = createContext<boolean>(true);
10+
11+
export const useFocusState = () => useContext(FocusContext);

0 commit comments

Comments
 (0)