WebUI: Avoid forced reflow on virtual list rerender

Avoid forced synchronous layout caused by offsetHeight/scrollTop access.

PR #22858.
This commit is contained in:
tehcneko 2025-06-22 14:27:16 +08:00 committed by GitHub
parent 86e11d344f
commit d702a02c1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -99,6 +99,7 @@ window.qBittorrent.DynamicTable ??= (() => {
return;
this.table.style.position = "relative";
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.renderedHeight = this.dynamicTableDiv.offsetHeight;
const resizeCallback = window.qBittorrent.Misc.createDebounceHandler(100, () => {
const height = this.dynamicTableDiv.offsetHeight;
@ -117,8 +118,10 @@ window.qBittorrent.DynamicTable ??= (() => {
this.dynamicTableDiv.addEventListener("scroll", (e) => {
tableElement.style.left = `${-this.dynamicTableDiv.scrollLeft}px`;
// rerender on scroll
if (this.useVirtualList)
if (this.useVirtualList) {
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.rerender();
}
});
this.dynamicTableDiv.addEventListener("click", (e) => {
@ -910,17 +913,20 @@ window.qBittorrent.DynamicTable ??= (() => {
rerender(rows = this.getFilteredAndSortedRows()) {
// set the scrollable height
this.table.style.height = `${rows.length * this.rowHeight}px`;
const tableHeight = rows.length * this.rowHeight;
if (tableHeight !== this.previousTableHeight) {
this.previousTableHeight = tableHeight;
this.table.style.height = `${tableHeight}px`;
}
if (this.dynamicTableDiv.offsetHeight === 0)
if (this.renderedHeight === 0)
return;
this.renderedHeight = this.dynamicTableDiv.offsetHeight;
// show extra rows at top/bottom to reduce flickering
const extraRowCount = 20;
// how many rows can be shown in the visible area
const visibleRowCount = Math.ceil(this.renderedHeight / this.rowHeight) + (extraRowCount * 2);
// start position of visible rows, offsetted by scrollTop
let startRow = Math.max((Math.trunc(this.dynamicTableDiv.scrollTop / this.rowHeight) - extraRowCount), 0);
// start position of visible rows, offsetted by renderedOffset
let startRow = Math.max((Math.trunc(this.renderedOffset / this.rowHeight) - extraRowCount), 0);
// ensure startRow is even
if ((startRow % 2) === 1)
startRow = Math.max(0, startRow - 1);
@ -949,24 +955,6 @@ window.qBittorrent.DynamicTable ??= (() => {
// update visible rows
for (const row of this.tableBody.children)
this.updateRow(row, true);
// refresh row height based on first row
const tr = this.tableBody.firstChild;
if (tr !== null) {
const updateRowHeight = () => {
if (tr.offsetHeight === 0)
return;
if (this.rowHeight !== tr.offsetHeight) {
this.rowHeight = tr.offsetHeight;
// rerender on row height change
this.rerender();
}
};
if (tr.offsetHeight === 0)
setTimeout(updateRowHeight);
else
updateRowHeight();
}
}
createRowElement(row, top = -1) {
@ -998,6 +986,7 @@ window.qBittorrent.DynamicTable ??= (() => {
if (this.useVirtualList) {
tr.style.position = "absolute";
tr.style.top = `${top}px`;
tr.style.height = `${this.rowHeight}px`;
}
}
@ -2599,8 +2588,10 @@ window.qBittorrent.DynamicTable ??= (() => {
this.dynamicTableDiv.addEventListener("scroll", (e) => {
headerDiv.scrollLeft = this.dynamicTableDiv.scrollLeft;
// rerender on scroll
if (this.useVirtualList)
if (this.useVirtualList) {
this.renderedOffset = this.dynamicTableDiv.scrollTop;
this.rerender();
}
});
}
}