perf: Refactor menu item height synchronization to prevent layout thrashing and introduce a debounced resize listener.

This commit is contained in:
Kantine Wrapper
2026-03-12 11:28:32 +01:00
parent d1d355d3c2
commit b5568c964b
10 changed files with 186 additions and 57 deletions

View File

@@ -1,3 +1,10 @@
## v1.6.25 (2026-03-12)
-**Performance**: Debounced Resize-Listener hinzugefügt. Die Höhen-Synchronisierung der Menü-Karten wird nun auch bei Viewport-Änderungen (z.B. Fenster-Skalierung oder Orientierungswechsel) automatisch und effizient ausgeführt.
- 🧹 **Tech**: `debounce` Utility-Funktion in `utils.js` ergänzt.
## v1.6.24 (2026-03-12)
-**Performance**: Layout Thrashing in `syncMenuItemHeights` behoben. Durch Batch-Verarbeitung von DOM-Lese- und Schreibvorgängen wurde die Rendering-Effizienz beim Wochenwechsel verbessert.
## v1.6.23 (2026-03-12) ## v1.6.23 (2026-03-12)
- 🎨 **UI**: Umfassende UI-Verbesserungen umgesetzt: - 🎨 **UI**: Umfassende UI-Verbesserungen umgesetzt:
- **Glassmorphism**: Header-Hintergrundtransparenz auf 72% reduziert (war 90%) der Blur-Effekt ist nun beim Scrollen sichtbar. - **Glassmorphism**: Header-Hintergrundtransparenz auf 72% reduziert (war 90%) der Blur-Effekt ist nun beim Scrollen sichtbar.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

21
dist/install.html vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1578,9 +1578,10 @@ function setLangMode(lang) {
/* harmony export */ OR: () => (/* binding */ renderVisibleWeeks), /* harmony export */ OR: () => (/* binding */ renderVisibleWeeks),
/* harmony export */ Ux: () => (/* binding */ checkForUpdates), /* harmony export */ Ux: () => (/* binding */ checkForUpdates),
/* harmony export */ gJ: () => (/* binding */ updateNextWeekBadge), /* harmony export */ gJ: () => (/* binding */ updateNextWeekBadge),
/* harmony export */ showErrorModal: () => (/* binding */ showErrorModal) /* harmony export */ showErrorModal: () => (/* binding */ showErrorModal),
/* harmony export */ wy: () => (/* binding */ syncMenuItemHeights)
/* harmony export */ }); /* harmony export */ });
/* unused harmony exports syncMenuItemHeights, createDayCard, fetchVersions, updateCountdown, removeCountdown */ /* unused harmony exports createDayCard, fetchVersions, updateCountdown, removeCountdown */
/* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(901); /* harmony import */ var _state_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(901);
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(413); /* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(413);
/* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(521); /* harmony import */ var _constants_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(521);
@@ -1738,23 +1739,39 @@ function renderVisibleWeeks() {
function syncMenuItemHeights(grid) { function syncMenuItemHeights(grid) {
const cards = grid.querySelectorAll('.menu-card'); const cards = grid.querySelectorAll('.menu-card');
if (cards.length === 0) return; if (cards.length === 0) return;
// 1. Gather all menu-item groups (rows) across cards
const itemRows = [];
let maxItems = 0; let maxItems = 0;
cards.forEach(card => {
maxItems = Math.max(maxItems, card.querySelectorAll('.menu-item').length); const cardItems = Array.from(cards).map(card => {
const items = Array.from(card.querySelectorAll('.menu-item'));
maxItems = Math.max(maxItems, items.length);
return items;
}); });
for (let i = 0; i < maxItems; i++) { for (let i = 0; i < maxItems; i++) {
let maxHeight = 0; // Collect i-th item from each card (forming a "row")
const itemsAtPos = []; itemRows[i] = cardItems.map(items => items[i]).filter(item => !!item);
cards.forEach(card => {
const items = card.querySelectorAll('.menu-item');
if (items[i]) {
items[i].style.height = 'auto';
maxHeight = Math.max(maxHeight, items[i].offsetHeight);
itemsAtPos.push(items[i]);
}
});
itemsAtPos.forEach(item => { item.style.height = `${maxHeight}px`; });
} }
// 2. Batch Reset (Write phase) - clear old heights to let them flow naturally
itemRows.flat().forEach(item => {
item.style.height = 'auto';
});
// 3. Batch Read (Read phase) - measure all heights in one pass to avoid layout thrashing
const rowMaxHeights = itemRows.map(row => {
return Math.max(...row.map(item => item.offsetHeight));
});
// 4. Batch Apply (Write phase) - set synchronized heights
itemRows.forEach((row, i) => {
const height = `${rowMaxHeights[i]}px`;
row.forEach(item => {
item.style.height = height;
});
});
} }
function createDayCard(day) { function createDayCard(day) {
@@ -2329,6 +2346,7 @@ function updateAlarmBell() {
/* harmony export */ U4: () => (/* binding */ isNewer), /* harmony export */ U4: () => (/* binding */ isNewer),
/* harmony export */ ZD: () => (/* binding */ escapeHtml), /* harmony export */ ZD: () => (/* binding */ escapeHtml),
/* harmony export */ gs: () => (/* binding */ getRelativeTime), /* harmony export */ gs: () => (/* binding */ getRelativeTime),
/* harmony export */ sg: () => (/* binding */ debounce),
/* harmony export */ sn: () => (/* binding */ getISOWeek) /* harmony export */ sn: () => (/* binding */ getISOWeek)
/* harmony export */ }); /* harmony export */ });
/* unused harmony export splitLanguage */ /* unused harmony export splitLanguage */
@@ -2582,6 +2600,18 @@ function getLocalizedText(text) {
return split.de || split.raw; return split.de || split.raw;
} }
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/***/ } /***/ }
@@ -2884,6 +2914,8 @@ var constants = __webpack_require__(521);
var api = __webpack_require__(672); var api = __webpack_require__(672);
// EXTERNAL MODULE: ./src/i18n.js // EXTERNAL MODULE: ./src/i18n.js
var i18n = __webpack_require__(646); var i18n = __webpack_require__(646);
// EXTERNAL MODULE: ./src/utils.js
var utils = __webpack_require__(413);
;// ./src/events.js ;// ./src/events.js
@@ -2892,6 +2924,7 @@ var i18n = __webpack_require__(646);
/** /**
* Updates all static UI labels/tooltips to match the current language. * Updates all static UI labels/tooltips to match the current language.
* Called when the user switches the language toggle. * Called when the user switches the language toggle.
@@ -3251,6 +3284,12 @@ function bindEvents() {
(0,actions/* updateAuthUI */.i_)(); (0,actions/* updateAuthUI */.i_)();
(0,ui_helpers/* renderVisibleWeeks */.OR)(); (0,ui_helpers/* renderVisibleWeeks */.OR)();
}); });
// Sync heights on window resize (FR-Performance)
window.addEventListener('resize', (0,utils/* debounce */.sg)(() => {
const grid = document.querySelector('.days-grid');
if (grid) (0,ui_helpers/* syncMenuItemHeights */.wy)(grid);
}, 150));
} }
;// ./src/index.js ;// ./src/index.js

View File

@@ -1,9 +1,10 @@
import { displayMode, langMode, authToken, currentUser, orderMap, userFlags, pollIntervalId, setLangMode, setDisplayMode, setAuthToken, setCurrentUser, setOrderMap } from './state.js'; import { displayMode, langMode, authToken, currentUser, orderMap, userFlags, pollIntervalId, setLangMode, setDisplayMode, setAuthToken, setCurrentUser, setOrderMap } from './state.js';
import { updateAuthUI, loadMenuDataFromAPI, fetchOrders, startPolling, stopPolling, fetchFullOrderHistory, addHighlightTag, renderTagsList, refreshFlaggedItems } from './actions.js'; import { updateAuthUI, loadMenuDataFromAPI, fetchOrders, startPolling, stopPolling, fetchFullOrderHistory, addHighlightTag, renderTagsList, refreshFlaggedItems } from './actions.js';
import { renderVisibleWeeks, openVersionMenu, updateNextWeekBadge, updateAlarmBell } from './ui_helpers.js'; import { renderVisibleWeeks, openVersionMenu, updateNextWeekBadge, updateAlarmBell, syncMenuItemHeights } from './ui_helpers.js';
import { API_BASE, GUEST_TOKEN, LS } from './constants.js'; import { API_BASE, GUEST_TOKEN, LS } from './constants.js';
import { apiHeaders } from './api.js'; import { apiHeaders } from './api.js';
import { t } from './i18n.js'; import { t } from './i18n.js';
import { debounce } from './utils.js';
/** /**
* Updates all static UI labels/tooltips to match the current language. * Updates all static UI labels/tooltips to match the current language.
@@ -364,4 +365,10 @@ export function bindEvents() {
updateAuthUI(); updateAuthUI();
renderVisibleWeeks(); renderVisibleWeeks();
}); });
// Sync heights on window resize (FR-Performance)
window.addEventListener('resize', debounce(() => {
const grid = document.querySelector('.days-grid');
if (grid) syncMenuItemHeights(grid);
}, 150));
} }

View File

@@ -149,23 +149,39 @@ export function renderVisibleWeeks() {
export function syncMenuItemHeights(grid) { export function syncMenuItemHeights(grid) {
const cards = grid.querySelectorAll('.menu-card'); const cards = grid.querySelectorAll('.menu-card');
if (cards.length === 0) return; if (cards.length === 0) return;
// 1. Gather all menu-item groups (rows) across cards
const itemRows = [];
let maxItems = 0; let maxItems = 0;
cards.forEach(card => {
maxItems = Math.max(maxItems, card.querySelectorAll('.menu-item').length); const cardItems = Array.from(cards).map(card => {
const items = Array.from(card.querySelectorAll('.menu-item'));
maxItems = Math.max(maxItems, items.length);
return items;
}); });
for (let i = 0; i < maxItems; i++) { for (let i = 0; i < maxItems; i++) {
let maxHeight = 0; // Collect i-th item from each card (forming a "row")
const itemsAtPos = []; itemRows[i] = cardItems.map(items => items[i]).filter(item => !!item);
cards.forEach(card => {
const items = card.querySelectorAll('.menu-item');
if (items[i]) {
items[i].style.height = 'auto';
maxHeight = Math.max(maxHeight, items[i].offsetHeight);
itemsAtPos.push(items[i]);
}
});
itemsAtPos.forEach(item => { item.style.height = `${maxHeight}px`; });
} }
// 2. Batch Reset (Write phase) - clear old heights to let them flow naturally
itemRows.flat().forEach(item => {
item.style.height = 'auto';
});
// 3. Batch Read (Read phase) - measure all heights in one pass to avoid layout thrashing
const rowMaxHeights = itemRows.map(row => {
return Math.max(...row.map(item => item.offsetHeight));
});
// 4. Batch Apply (Write phase) - set synchronized heights
itemRows.forEach((row, i) => {
const height = `${rowMaxHeights[i]}px`;
row.forEach(item => {
item.style.height = height;
});
});
} }
export function createDayCard(day) { export function createDayCard(day) {

View File

@@ -246,3 +246,15 @@ export function getLocalizedText(text) {
if (langMode === 'en') return split.en || split.raw; if (langMode === 'en') return split.en || split.raw;
return split.de || split.raw; return split.de || split.raw;
} }
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

View File

@@ -1 +1 @@
v1.6.23 v1.6.25