From 1cab24fea1418ceb8fd4b6b6a9fb68e4aec500e0 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 7 Nov 2026 20:35:17 +0800 Subject: [PATCH] perf: keep typing, scrolling and list navigation within a frame (#1902) * perf(project-tree): measure row widths once per frame The horizontal scroll range was measured in a layout effect on every render: each pass forced layout, its setState re-rendered synchronously and measured again, and the ResizeObserver was recreated each time, firing once more. Expanding a 10k-entry folder went from ~26 ms to ~63 ms. Keep one observer per tree and coalesce renders and resizes into a single measurement on the next animation frame. Expanding is back to ~30 ms and arrow-key p50 drops from 3.3 ms to 1.7 ms; the widths behave as before. * perf(github): load the pull requests integration after start-up Actions, the tool window and the timeline editor stay registered at start-up; their implementations, the connection and review-mode tracking load on idle or on first use. Takes ~60 KB gzip off the initial bundle. * perf(templates): load Emmet when an HTML or stylesheet editor opens The Emmet engine (~70 KB minified) was part of the initial bundle; it now loads with the first editor that can expand abbreviations, or with Update Tag with Emmet. * perf(search, run): load popups and Edit Configurations when first shown Search Everywhere, Find in Files, the recent files/locations popups, File Structure and the Run/Debug Configurations dialog were part of the initial bundle although each only renders while open. `lazyWhen` mounts a lazy view only while its condition holds; the idle prefetch still loads them right after start-up, so opening one stays as fast. * perf: load dialogs, the SQL console and diff views on first use - Database dialogs, Assign Shortcut and the Logcat tool window load when shown. - The SQL console extensions load with the first SQL editor (`lazyEditorExtension` reconfigures the editor once they arrive). - The changes tree loads the diff viewer when a file is selected, and "Open Repository Log" loads the revision browser when run. * perf: load plugin, debugger and project popups when first shown The IntelliJ theme import, the debugger's popovers and dialogs and the title bar's project popup now load when first opened. `lazyWhen` keeps a view mounted once shown, so its own open/closed handling is unchanged. * perf(angular): parse only text with Angular syntax as Angular Every text node of a template got its own nested Angular parse, including the whitespace between tags, which is most of them. Text without a `{`, `}` or `@` holds no interpolation or block, so it stays HTML text. A full parse of a 12k-line template takes about a fifth less work. * perf(angular): reuse nested trees of unchanged text and attributes An edit makes the mixed parser recreate many text and attribute nodes it does not keep: the document's own text and the nodes up to the viewport. It reuses nested trees only of nodes it kept, so each of those was parsed again as Angular on every keystroke (~170 nested parses per key in a 12k-line template). Their parse depends only on their text, so the trees are now remembered by text, up to about 2M characters. Typing in the middle of a 12k-line template: main thread per key 57 -> 22 ms, p50 latency 30 -> 19.5 ms, no long tasks. * perf: budget typing in a large Angular template * perf: measure code completion over a 2,000-item list The harness's probe can answer completion with generated items through the command route; language server requests go through a route when one is installed, not only under Tauri. The scenario opens the popup by typing, narrows it letter by letter and moves the selection, with budgets for each. * perf(run): show a flood of console output at most every 50 ms While a process printed thousands of lines a second, the Run console appended, re-rendered and laid out on every frame, so typing in an editor meanwhile missed the next frame 35% of the time. A line after a quiet spell still shows on the next frame; a flood now updates the console at ~20 Hz, and typing during it misses 2-5% (p50 16 -> 9.7 ms) with the output done as fast as before. The performance harness gets a scenario for it: 50,000 log lines at 200 a frame (a dev and perf build action, run.sampleOutput) while typing. * perf(vcs): keep Git Log rows from re-rendering on every selection change Commit rows were memoized but received a new Set and new closures on every render, so moving the selection re-rendered every visible row, and each row built a new Intl.DateTimeFormat for its date. Rows now get stable handlers and shared remote-name sets, and date formatting reuses cached formatters. * perf: measure the Git Log and splitter dragging gitLog shows 20k generated commits served page by page through a fake vcs_log, walks the selection with the arrow keys and wheel-scrolls the list. resizePanels drags the side and bottom splitters with an editor, the Project tree and the Log open. The perf probe's fake backends can now be combined. * perf: translate the sample output action and keep fake completion details out of the text catalog * fix(run): keep flushing console output when the window hides mid-wait * test(e2e): ignore the TODO index's scan when checking the Find in Files query --- e2e/search.spec.ts | 6 +++++- src/features/run/store.ts | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/e2e/search.spec.ts b/e2e/search.spec.ts index e27aedc92..a08a281dd 100644 --- a/e2e/search.spec.ts +++ b/e2e/search.spec.ts @@ -92,8 +92,12 @@ test("Find in Files streams matches by file and opens the chosen one at its line await dialog.getByRole("textbox", { name: "Text to find" }).fill("OrderService"); const matches = dialog.getByRole("listbox", { name: "Matches" }); await expect(matches.getByRole("option")).toHaveCount(3); + // The TODO index scans the project through `search_start` too, on its own schedule. await expect - .poll(async () => (await ipcCalls(page, "search_start")).at(-1)?.query) + .poll( + async () => + (await ipcCalls(page, "search_start")).filter((c) => !/\bTODO\b/.test(c.query?.pattern ?? "")).at(-1)?.query, + ) .toMatchObject({ pattern: "OrderService", }); diff --git a/src/features/run/store.ts b/src/features/run/store.ts index 9c37216d5..168c45158 100644 --- a/src/features/run/store.ts +++ b/src/features/run/store.ts @@ -530,10 +530,10 @@ function enqueue(sessionId: string, chunk: OutputChunk) { ? { timeout: window.setTimeout(() => flush(sessionId), 100) } : wait > 0 ? { - timeout: window.setTimeout( - () => (rt.flushHandle = { raf: requestAnimationFrame(() => flush(sessionId)) }), - wait, - ), + timeout: window.setTimeout(() => { + if (document.hidden) flush(sessionId); + else rt.flushHandle = { raf: requestAnimationFrame(() => flush(sessionId)) }; + }, wait), } : { raf: requestAnimationFrame(() => flush(sessionId)) }; }