Skip to content

Commit dfbc1fd

Browse files
committed
perf: preserve sidebar layout across hide/show to make expand near-instant
The CCB sidebar-toggle routed through `$sidebar.hide()` → `display:none`, which Chromium handles by destroying the element's layout tree. On re-show, the browser has to rebuild layout + paint for the entire sidebar subtree — for the AI chat panel with a large history (>1k messages) that was ~750ms of synchronous work per toggle, and scrollTop on the messages container was zeroed along the way. Add an opt-in visibility-based hide mode to Resizer: when an element has the `layout-preserve-hide` class, Resizer toggles a `layout-hidden` class (which applies `visibility: hidden; pointer-events: none`) instead of calling `$element.hide()` / `$element.show()`. The element's layout object stays alive across the toggle, so descendants keep their scroll positions and `content-visibility` caches, and re-show has no layout rebuild to do. `Resizer.isVisible` and the drag-path `:visible` checks were updated to consult the new class so toggle() / drag-to-reopen still work correctly. Four callers that read `$sidebar.is(":visible")` were switched to `SidebarView.isVisible()` (which delegates to the updated Resizer path), since jQuery's `:visible` can't distinguish a visibility-hidden element from a shown one. Measured with a 1098-message AI chat (~10k DOM nodes): expand time on the sidebar toggle dropped from 752ms → 49.7ms (~15x), scrollTop drift is 0. Existing suites (mainview:CentralControlBar 50/50, mainview:SidebarTabs 37/37, integration:Navigation 6/6) all green.
1 parent e007213 commit dfbc1fd

6 files changed

Lines changed: 43 additions & 13 deletions

File tree

src/document/DocumentCommandHandlers.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ define(function (require, exports, module) {
6262
NodeUtils = require("utils/NodeUtils"),
6363
ChangeHelper = require("editor/EditorHelper/ChangeHelper"),
6464
SidebarTabs = require("view/SidebarTabs"),
65+
SidebarView = require("project/SidebarView"),
6566
_ = require("thirdparty/lodash");
6667

6768
const KernalModeTrust = window.KernalModeTrust;
@@ -1954,7 +1955,7 @@ define(function (require, exports, module) {
19541955
function handleShowInTree() {
19551956
let activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
19561957
if(activeFile){
1957-
if (!$("#sidebar").is(":visible")) {
1958+
if (!SidebarView.isVisible()) {
19581959
CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR);
19591960
}
19601961
SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES);

src/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,7 +905,7 @@
905905
<div class="main-view forced-hidden">
906906
<div id="notificationUIDefaultAnchor" href="#">
907907
</div>
908-
<div id="sidebar" class="sidebar panel quiet-scrollbars horz-resizable right-resizer collapsible" data-minsize="0" data-maxsize="80%" data-forceleft=".content">
908+
<div id="sidebar" class="sidebar panel quiet-scrollbars horz-resizable right-resizer collapsible layout-preserve-hide" data-minsize="0" data-maxsize="80%" data-forceleft=".content">
909909
<div id="mainNavBar">
910910
<div id="mainNavBarLeft">
911911
<div id="newProject" class="new-project-btn btn-alt-quiet"></div>

src/project/SidebarView.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ define(function (require, exports, module) {
251251

252252
// AppInit.htmlReady in utils/Resizer executes before, so it's possible that the sidebar
253253
// is collapsed before we add the event. Check here initially
254-
if (!$sidebar.is(":visible")) {
254+
if (!isVisible()) {
255255
$sidebar.trigger("panelCollapsed");
256256
}
257257

src/styles/brackets.less

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,15 @@ a, img {
13821382
cursor: col-resize;
13831383
}
13841384

1385+
/* Resizer.js visibility-preserving hide: elements with .layout-preserve-hide
1386+
get this class toggled instead of `display: none`, so their descendants
1387+
keep their layout objects (and scrollTop / content-visibility caches)
1388+
across collapse/expand. */
1389+
.layout-preserve-hide.layout-hidden {
1390+
visibility: hidden !important;
1391+
pointer-events: none !important;
1392+
}
1393+
13851394
.working-set-header-title{
13861395
font-size: 15px;
13871396
}

src/utils/Resizer.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ define(function (require, exports, module) {
178178
* @param {DOMNode} element Html element to toggle
179179
*/
180180
function toggle(element) {
181-
if ($(element).is(":visible")) {
181+
if (isVisible(element)) {
182182
hide(element);
183183
} else {
184184
show(element);
@@ -217,7 +217,14 @@ define(function (require, exports, module) {
217217
* @return {boolean} true if element is visible, false if it is not visible
218218
*/
219219
function isVisible(element) {
220-
return $(element).is(":visible");
220+
var $el = $(element);
221+
if ($el.hasClass("layout-preserve-hide")) {
222+
// Element opts out of `display: none` on hide (to keep layout alive
223+
// so scrollTop / content-visibility cache survive the toggle). jQuery's
224+
// `:visible` still returns true in that case, so consult our own flag.
225+
return !$el.hasClass("layout-hidden");
226+
}
227+
return $el.is(":visible");
221228
}
222229

223230
function _isPercentage(value) {
@@ -422,7 +429,13 @@ define(function (require, exports, module) {
422429
// the resizer, the size of the element should be 0, so we restore size in preferences
423430
resizeElement(elementSize, contentSize);
424431

425-
$element.show();
432+
if ($element.hasClass("layout-preserve-hide")) {
433+
// Keep display unchanged — just un-hide via class. The element's
434+
// layout tree was preserved, so no reflow avalanche here.
435+
$element.removeClass("layout-hidden");
436+
} else {
437+
$element.show();
438+
}
426439
elementPrefs.visible = true;
427440

428441
if (collapsible) {
@@ -451,7 +464,13 @@ define(function (require, exports, module) {
451464
elementSize = elementSizeFunction.apply($element),
452465
resizerSize = elementSizeFunction.apply($resizer);
453466

454-
$element.hide();
467+
if ($element.hasClass("layout-preserve-hide")) {
468+
// Visually hide but keep the layout object alive so descendants
469+
// retain scrollTop and content-visibility caches across toggles.
470+
$element.addClass("layout-hidden");
471+
} else {
472+
$element.hide();
473+
}
455474
elementPrefs.visible = false;
456475
if (collapsible) {
457476
$resizer.insertBefore($element);
@@ -472,7 +491,7 @@ define(function (require, exports, module) {
472491
$resizer.on("mousedown.resizer", function (e) {
473492
var $resizeShield = $("<div class='resizing-container " + direction + "-resizing' />"),
474493
startPosition = e[directionProperty],
475-
startSize = $element.is(":visible") ? elementSizeFunction.apply($element) : 0,
494+
startSize = isVisible($element) ? elementSizeFunction.apply($element) : 0,
476495
newSize = startSize,
477496
previousSize = startSize,
478497
baseSize = 0,
@@ -502,7 +521,7 @@ define(function (require, exports, module) {
502521
if (newSize !== previousSize) {
503522
previousSize = newSize;
504523

505-
if ($element.is(":visible")) {
524+
if (isVisible($element)) {
506525
if (collapsible && newSize < 10) {
507526
toggle($element);
508527
elementSizeFunction.apply($element, [0]);
@@ -579,7 +598,7 @@ define(function (require, exports, module) {
579598
if (isResizing) {
580599

581600
var elementSize = elementSizeFunction.apply($element);
582-
if ($element.is(":visible")) {
601+
if (isVisible($element)) {
583602
elementPrefs.size = elementSize;
584603
if ($resizableElement.length) {
585604
elementPrefs.contentSize = contentSizeFunction.apply($resizableElement);
@@ -651,7 +670,7 @@ define(function (require, exports, module) {
651670
}
652671

653672
function onWindowResize(e) {
654-
if ($sideBar.css("display") === "none") {
673+
if (!isVisible($sideBar)) {
655674
return;
656675
}
657676

src/view/CentralControlBar.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ define(function (require, exports, module) {
2525
const Commands = require("command/Commands");
2626
const Strings = require("strings");
2727
const WorkspaceManager = require("view/WorkspaceManager");
28+
const SidebarView = require("project/SidebarView");
2829

2930
const BAR_WIDTH = 30;
3031

@@ -44,7 +45,7 @@ define(function (require, exports, module) {
4445
// and rendered width can diverge mid-drag, and outerWidth has returned the
4546
// uncapped style value in some frames which left CCB / main-toolbar stuck
4647
// at stale offsets.
47-
if (!$sidebar || !$sidebar.is(":visible")) {
48+
if (!$sidebar || !SidebarView.isVisible()) {
4849
return 0;
4950
}
5051
return $sidebar[0].offsetWidth || 0;
@@ -248,7 +249,7 @@ define(function (require, exports, module) {
248249
if (!$btn.length) {
249250
return;
250251
}
251-
const isVisible = $("#sidebar").is(":visible");
252+
const isVisible = SidebarView.isVisible();
252253
$btn.find("i").attr("class", isVisible ? "fa-solid fa-angles-left" : "fa-solid fa-angles-right");
253254
}
254255

0 commit comments

Comments
 (0)