fix: virtual scroll flickering on inventory scroll

- Batch scroll renders with requestAnimationFrame to avoid multiple DOM updates per frame
- Add will-change, contain and content-visibility CSS for smoother compositing
- Add cache-bust to virtual-scroll.js
This commit is contained in:
2026-05-26 05:13:36 +00:00
parent b314a781a1
commit bfb4921ac0
3 changed files with 39 additions and 13 deletions

View File

@@ -1333,6 +1333,20 @@
/* History table inside modal */ /* History table inside modal */
.inv-modal .data-table { width: 100%; } .inv-modal .data-table { width: 100%; }
/* ─── Virtual Scroll fixes ───────────────────────────────────────────── */
.vs-container {
will-change: transform;
contain: layout paint;
-webkit-overflow-scrolling: touch;
}
.vs-container table {
will-change: transform;
}
.vs-container tbody tr {
content-visibility: auto;
contain-intrinsic-size: auto 48px;
}
/* ─── MercadoLibre Publish Modal Enhancements ────────────────────────── */ /* ─── MercadoLibre Publish Modal Enhancements ────────────────────────── */
.meli-preview-card { .meli-preview-card {
display: grid; display: grid;

View File

@@ -24,6 +24,8 @@
this._scrollHandler = this._onScroll.bind(this); this._scrollHandler = this._onScroll.bind(this);
this._resizeHandler = this._onResize.bind(this); this._resizeHandler = this._onResize.bind(this);
this._isTbody = this.container.tagName === 'TBODY'; this._isTbody = this.container.tagName === 'TBODY';
this._rafId = null;
this._pendingRender = false;
this._init(); this._init();
} }
@@ -58,11 +60,22 @@
}; };
VirtualScroll.prototype._onScroll = function() { VirtualScroll.prototype._onScroll = function() {
this._render(); this._scheduleRender();
}; };
VirtualScroll.prototype._onResize = function() { VirtualScroll.prototype._onResize = function() {
this._render(); this._scheduleRender();
};
VirtualScroll.prototype._scheduleRender = function() {
if (this._pendingRender) return;
this._pendingRender = true;
var self = this;
this._rafId = requestAnimationFrame(function() {
self._rafId = null;
self._pendingRender = false;
self._render();
});
}; };
VirtualScroll.prototype._getScrollTop = function() { VirtualScroll.prototype._getScrollTop = function() {
@@ -99,11 +112,7 @@
var buffer = this.buffer; var buffer = this.buffer;
if (!data.length) { if (!data.length) {
if (this._isTbody) {
this.container.innerHTML = this.emptyHtml; this.container.innerHTML = this.emptyHtml;
} else {
this.container.innerHTML = this.emptyHtml;
}
return; return;
} }
@@ -112,20 +121,19 @@
var startIdx = Math.max(0, Math.floor(scrollTop / rowH) - buffer); var startIdx = Math.max(0, Math.floor(scrollTop / rowH) - buffer);
var endIdx = Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowH) + buffer); var endIdx = Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowH) + buffer);
// Build new HTML
var html = ''; var html = '';
if (this._isTbody) { if (this._isTbody) {
// Top spacer row
var topSpacerHeight = startIdx * rowH; var topSpacerHeight = startIdx * rowH;
if (topSpacerHeight > 0) { if (topSpacerHeight > 0) {
html += '<tr style="height:' + topSpacerHeight + 'px;"><td colspan="99" style="padding:0;border:0;"></td></tr>'; html += '<tr style="height:' + topSpacerHeight + 'px;" aria-hidden="true"><td colspan="99" style="padding:0;border:0;"></td></tr>';
} }
for (var i = startIdx; i < endIdx; i++) { for (var i = startIdx; i < endIdx; i++) {
html += this.renderRow(data[i], i); html += this.renderRow(data[i], i);
} }
// Bottom spacer row
var bottomSpacerHeight = (data.length - endIdx) * rowH; var bottomSpacerHeight = (data.length - endIdx) * rowH;
if (bottomSpacerHeight > 0) { if (bottomSpacerHeight > 0) {
html += '<tr style="height:' + bottomSpacerHeight + 'px;"><td colspan="99" style="padding:0;border:0;"></td></tr>'; html += '<tr style="height:' + bottomSpacerHeight + 'px;" aria-hidden="true"><td colspan="99" style="padding:0;border:0;"></td></tr>';
} }
} else { } else {
for (var j = startIdx; j < endIdx; j++) { for (var j = startIdx; j < endIdx; j++) {
@@ -133,6 +141,10 @@
} }
} }
// Use a DocumentFragment approach via innerHTML to avoid flicker:
// Setting innerHTML on tbody is the fastest way, but we can reduce
// perceived flicker by ensuring the container has contain: paint
// and by batching via rAF (done in _scheduleRender).
this.container.innerHTML = html; this.container.innerHTML = html;
}; };

View File

@@ -13,7 +13,7 @@
<link rel="manifest" href="/pos/static/pwa/manifest.json" /> <link rel="manifest" href="/pos/static/pwa/manifest.json" />
<meta name="theme-color" content="#F5A623" /> <meta name="theme-color" content="#F5A623" />
<link rel="stylesheet" href="/pos/static/css/inventory.css?v=6"> <link rel="stylesheet" href="/pos/static/css/inventory.css?v=7">
</head> </head>
<body> <body>
@@ -907,7 +907,7 @@
<script src="/pos/static/js/app-init.js" defer></script> <script src="/pos/static/js/app-init.js" defer></script>
<script src="/pos/static/js/pos-utils.js" defer></script> <script src="/pos/static/js/pos-utils.js" defer></script>
<script src="/pos/static/js/sidebar.js" defer></script> <script src="/pos/static/js/sidebar.js" defer></script>
<script src="/pos/static/js/virtual-scroll.js" defer></script> <script src="/pos/static/js/virtual-scroll.js?v=2" defer></script>
<script src="/pos/static/js/inventory.js?v=12" defer></script> <script src="/pos/static/js/inventory.js?v=12" defer></script>
<script src="/pos/static/js/offline-banner.js" defer></script> <script src="/pos/static/js/offline-banner.js" defer></script>
<script src="/pos/static/js/sync-engine.js" defer></script> <script src="/pos/static/js/sync-engine.js" defer></script>