Repository navigation
perf(layout): skip the body-level OverlayScrollbars on the workspace / 工作区不再挂 body 级滚动条,减轻窗口缩放卡顿 - #898
Merged
xintaofei merged 3 commits intoOct 9, 2026
Conversation
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.
Collaborator
|
codeg work task |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / 改动内容
OverlayScrollbarsInitattaches an OverlayScrollbars instance todocument.body. The library measures its host from a windowresizelistener and aResizeObserver, 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/workspacethe shell is fixed and viewport-filling and every pane scrolls inside itself, so the body never scrolls and that instance has nothing to do./workspace./routes into the workspace withrouter.replacewhile 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.tsxchanges.Measurements / 测量
Chrome, web build, a workspace with a long transcript open, a series of
resize_pagecalls,long-animation-frameentries:DOMWindow.onresizeand one from aResizeObservercallback, both inside OverlayScrollbars and almost entirely forced style/layoutThe 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) andpnpm buildpass./→ lands on/workspacewith no body instance;/settingsstill has one.中文说明
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