commit 53600157c00452353a56c0710addf1d0f46f7563
parent 1986f706869fa42f62d8146a5f5cdec1b477760a
Author: Dimi <dlee@mozilla.com>
Date: Wed, 15 Oct 2025 12:42:39 +0000
Bug 1991651 - Use scroll position to determine visible items during scrolling r=mtigley,credential-management-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D268508
Diffstat:
3 files changed, 75 insertions(+), 0 deletions(-)
diff --git a/toolkit/components/satchel/megalist/content/MegalistAlpha.mjs b/toolkit/components/satchel/megalist/content/MegalistAlpha.mjs
@@ -420,6 +420,10 @@ export class MegalistAlpha extends MozLitElement {
};
renderList() {
+ const scroller = this.shadowRoot.querySelector(
+ ".sidebar-panel-scrollable-content"
+ );
+
return this.records.length
? html`
<virtual-passwords-list
@@ -460,6 +464,7 @@ export class MegalistAlpha extends MozLitElement {
}
}}
.items=${this.records}
+ .scroller=${scroller}
.version=${this.listVersion}
.itemHeightEstimate=${PasswordCard.DEFAULT_PASSWORD_CARD_HEIGHT}
.heightCalculator=${this.heightCalculator}
diff --git a/toolkit/components/satchel/megalist/content/components/password-card/password-card.css b/toolkit/components/satchel/megalist/content/components/password-card/password-card.css
@@ -3,6 +3,14 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
:host {
+ /**
+ * Use 1px instead of 0.5px because the virtual list relies on accurate layout
+ * measurements to pre-compute item positions. Sub-pixel borders (e.g. 0.5px)
+ * are rendered differently across devices due to DPR rounding, which leads to
+ * inconsistent height calculations and causes incorrect virtualization offsets.
+ */
+ --sidebar-box-border-width: 1px;
+
display: flex;
flex-direction: column;
background-color: var(--sidebar-box-background);
diff --git a/toolkit/components/satchel/megalist/content/components/virtual-passwords-list/virtual-passwords-list.mjs b/toolkit/components/satchel/megalist/content/components/virtual-passwords-list/virtual-passwords-list.mjs
@@ -5,6 +5,12 @@
import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
/**
* TODO: This code is duplicated from fxview-tab-list.mjs.
* The duplication is intentional to keep the variable-height changes
@@ -13,6 +19,9 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
* shared component.
*/
class VirtualPasswordsList extends MozLitElement {
+ #scroller = null;
+ #onScrollEndTimer = null;
+
static properties = {
items: { type: Array },
template: { type: Function },
@@ -54,8 +63,17 @@ class VirtualPasswordsList extends MozLitElement {
this.isVisible = false;
this.version = 0;
+ // Ignore IntersectionObserver callbacks during scrolling.
+ // Rapid scrolling causes frequent visibility changes, which can trigger
+ // excessive IO callbacks and hurt performance. While scrolling, we rely
+ // on scroll position to determine which items should be rendered instead.
+ this.ignoreIO = false;
+
this.intersectionObserver = new IntersectionObserver(
([entry]) => {
+ if (this.ignoreIO) {
+ return;
+ }
this.isVisible = entry.isIntersecting;
},
{ root: this.ownerDocument }
@@ -141,6 +159,50 @@ class VirtualPasswordsList extends MozLitElement {
}
}
+ get scroller() {
+ return this.#scroller;
+ }
+
+ set scroller(element) {
+ if (this.isSubList || this.#scroller === element) {
+ return;
+ }
+
+ if (this.#scroller) {
+ this.#scroller.removeEventListener("scroll", this.onScroll);
+ }
+
+ this.#scroller = element;
+ this.#scroller.addEventListener("scroll", () => this.onScroll());
+ }
+
+ onScroll() {
+ if (!this.children.length) {
+ return;
+ }
+
+ if (this.#onScrollEndTimer) {
+ // reset the timer
+ lazy.clearTimeout(this.#onScrollEndTimer);
+ } else {
+ Array.from(this.children).forEach(child => (child.ignoreIO = true));
+ }
+
+ this.#onScrollEndTimer = lazy.setTimeout(() => {
+ Array.from(this.children).forEach(child => (child.ignoreIO = false));
+ this.#onScrollEndTimer = null;
+ }, 1000);
+
+ const index = parseInt(
+ this.scroller.scrollTop / this.children[0].clientHeight,
+ 10
+ );
+
+ for (let i = 0; i < this.children.length; i++) {
+ this.children[i].isVisible = i <= index + 1 && i >= index - 1;
+ }
+ }
+
recalculateAfterWindowResize() {
this.maxRenderCountEstimate = Math.max(
40,