Skip to content

perf(layout): skip the body-level OverlayScrollbars on the workspace / 工作区不再挂 body 级滚动条,减轻窗口缩放卡顿 - #898

Merged
xintaofei merged 3 commits into
spacering-net:mainfrom
sumanx:perf/skip-body-overlayscrollbars-on-workspace
Oct 9, 2026
Merged

xintaofei merged 3 commits into
spacering-net:mainfrom
sumanx:perf/skip-body-overlayscrollbars-on-workspace

Conversation

@sumanx

@sumanx sumanx commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

What / 改动内容

OverlayScrollbarsInit attaches an OverlayScrollbars instance to document.body. The library measures its host from a window resize listener and a ResizeObserver, each forcing a synchronous layout of the host — here the whole document — so every window resize step paid two extra full-page layouts on top of the real one. On /workspace the shell is fixed and viewport-filling and every pane scrolls inside itself, so the body never scrolls and that instance has nothing to do.

  • The body instance is no longer created on /workspace.
  • It follows client-side navigation: / routes into the workspace with router.replace while this component stays mounted, so an instance created on the way in is destroyed on arrival. Other pages keep the body scrollbar.

Only src/components/overlay-scrollbars-init.tsx changes.

Measurements / 测量

Chrome, web build, a workspace with a long transcript open, a series of resize_page calls, long-animation-frame entries:

slowest resize frame
before ~66 ms — script attribution: two ~10 ms entries, one from DOMWindow.onresize and one from a ResizeObserver callback, both inside OverlayScrollbars and almost entirely forced style/layout
after ~33 ms, no long frame (> 50 ms)

The desktop (Tauri) build is the one this matters most for: a slow webview frame also holds back the native window while it is being resized. Single resizes feel noticeably better there; repeated large resizes can still stutter, which looks like the upstream Tauri/webview issue (tauri-apps/tauri#13807) and is not addressed here. The numbers above are from the browser build, not the desktop one.

Testing / 测试

  • pnpm lint ., pnpm test (560 files / 8390 tests) and pnpm build pass.
  • In the browser: / → lands on /workspace with no body instance; /settings still has one.
  • Tested on Linux only (GNOME / Wayland, WebKitGTK desktop build and Chrome); not tried on macOS or Windows.

中文说明

OverlayScrollbarsInit 会给 document.body 挂一个 OverlayScrollbars 实例。该库通过窗口 resize 监听和 ResizeObserver 来测量宿主元素,每次测量都会强制对宿主做一次同步布局,这里的宿主是整个文档,于是每一步窗口缩放除了真正的布局之外,还要额外多做两次全页布局。而 /workspace 的外壳是固定铺满视口的,各面板都在内部滚动,body 根本不会滚动,这个实例没有任何作用。

  • /workspace 上不再创建 body 级别的实例。
  • 跟随前端路由变化:/ 通过 router.replace 跳转到工作区时该组件保持挂载,所以在跳转途中创建的实例会在到达后被销毁;其他页面保留 body 滚动条。

只改动 src/components/overlay-scrollbars-init.tsx。

测量:Chrome、网页版、打开一个带长对话的工作区,连续调用 resize_page,采集 long-animation-frame。改动前最慢的缩放帧约 66 ms,脚本归因是两段各约 10 ms 的代码(一个来自 DOMWindow.onresize,一个来自 ResizeObserver 回调),都在 OverlayScrollbars 内部,几乎全是强制样式/布局;改动后最慢约 33 ms,没有超过 50 ms 的长帧。

这个改动对桌面端(Tauri)最有意义:webview 的某一帧慢了,也会拖住缩放中的原生窗口。桌面端单次缩放明显更跟手;反复大幅缩放时仍可能卡顿,这看起来是 Tauri / webview 本身的问题(tauri-apps/tauri#13807),这里没有处理。上面的数据来自网页版,不是桌面版。

测试:pnpm lint .、pnpm test(560 个文件 / 8390 个用例)、pnpm build 均通过。浏览器中验证:打开 / 会进入 /workspace 且 body 上没有实例;/settings 上仍有。仅在 Linux(GNOME / Wayland、WebKitGTK 桌面版和 Chrome)测试,未在 macOS、Windows 上试过。

🤖 Generated with Claude Code

sumanx and others added 3 commits October 8, 2026 23:53
OverlayScrollbarsInit attaches an instance to `document.body`. The
library measures its host from a window `resize` listener and a
ResizeObserver, each forcing a synchronous layout of the host — here the
whole document — so every window resize step paid two extra full-page
layouts on top of the real one. On /workspace the shell is fixed and
viewport-filling and every pane scrolls inside itself, so the body never
scrolls and the instance has nothing to do.

Profiled in Chrome against a long transcript with long-animation-frame
entries: ~66ms resize frames, with both extra layouts attributed to
OverlayScrollbars, down to ~33ms and no frame over 50ms without it. In
the desktop app that lag also holds back the window itself, which waits
on the webview's next frame.

The instance is created per route and follows client-side navigation:
`/` routes into the workspace with `router.replace` while this component
stays mounted, so one created on the way in is destroyed on arrival.
Other pages keep the body scrollbar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…kspace

The body-level OverlayScrollbars init is deferred to an idle callback and
an animation frame. Destroying the instance on arrival at /workspace
missed an init that had not run yet, since there was no instance to
destroy, so it still landed on the workspace afterwards: for example when
`/` redirects in a background tab, where frames do not run.

The hook now lives in a child that only renders off fixed routes, so its
own unmount cleanup cancels a pending init as well as destroying a live
one. Its options are a module constant, so the re-render on each
navigation no longer hands the instance a new options object.

Tests drive the real hook and library, with idle and frame callbacks
queued by hand.
…utes

Adds the hidden-tab case, where the deferred init has passed its idle
stage and only waits for a frame when the workspace is reached, and pins
that navigating between pages that scroll keeps the same body instance
instead of remounting it.
@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 295 is done — #898 (2 files, +166/-19).

@xintaofei
xintaofei merged commit 3b4ed46 into spacering-net:main Oct 9, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants