From 24508b5c39f92409024d3be6a76e92b51562d878 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:46:42 +0000 Subject: [PATCH] feat: Implement DOM pruning for table rendering This commit introduces a DOM pruning mechanism to the infinite scroll feature for displaying vocabulary lists. Previously, as the user scrolled, new table rows were continuously added to the DOM, but old rows that scrolled out of view were never removed. This led to an ever-growing number of DOM elements, causing significant performance degradation and crashes on memory-constrained devices like older iPads, especially when displaying more than 80-100 rows. This change addresses the issue by: 1. Introducing a `MAX_ROWS_IN_DOM` constant to limit the total number of `` elements that can exist in the DOM at any given time. 2. Adding a `pruneOffscreenRows` function that removes rows from the top or bottom of the table when the total number of rows exceeds the defined limit. 3. Updating the `scrollHandler` to call this pruning function after new rows are added, ensuring the DOM size remains small and constant, thus improving performance and preventing crashes. 4. Adding a global tracker `g_currentlyRendered` to keep track of the rendered data slice. --- main.js | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/main.js b/main.js index dff99c0..ffb61eb 100644 --- a/main.js +++ b/main.js @@ -45,6 +45,9 @@ let firstLoadedIndex = 0; let lastLoadedIndex = 0; let isLoadingMoreItems = false; const ITEMS_PER_LOAD = 20; +const VIEWPORT_MULTIPLIER = 4; // Buffer for rows outside the viewport +const MAX_ROWS_IN_DOM = ITEMS_PER_LOAD * (1 + 2 * VIEWPORT_MULTIPLIER); // e.g., 20 * (1 + 2*4) = 180 +let g_currentlyRendered = { start: 0, end: 0 }; // Tracks the slice of activeCategoryData in the DOM let g_audioElementsList = []; let g_bookmarkButtonsList = []; @@ -2466,6 +2469,7 @@ function buildTableAndSetupPlayback(category, vocabularyArray, dialectInfo, auto currentAudioIndex = 0; isPlaying = false; isPaused = false; + g_currentlyRendered = { start: 0, end: 0 }; // Reset DOM tracking window.removeEventListener('scroll', scrollHandler); // Remove old listener // 2. Filter data and handle empty category @@ -2490,8 +2494,10 @@ function buildTableAndSetupPlayback(category, vocabularyArray, dialectInfo, auto } firstLoadedIndex = start; lastLoadedIndex = Math.min(start + ITEMS_PER_LOAD, totalResults); + g_currentlyRendered = { start: firstLoadedIndex, end: lastLoadedIndex }; // Set initial range const initialItems = activeCategoryData.slice(firstLoadedIndex, lastLoadedIndex); + // 4. Render the initial chunk of items (no return value handled) renderCategoryItems(initialItems, dialectInfo, category, true, totalResults, autoPlayTargetRowId); @@ -2692,6 +2698,40 @@ function renderCategoryItems(itemsToRender, dialectInfo, category, isInitialLoad setTimeout(() => handleResizeActions(), 50); } +function pruneOffscreenRows(direction) { + const tableBody = document.querySelector('#category-table tbody'); + if (!tableBody) return; + + const totalRowsInDom = tableBody.rows.length; + if (totalRowsInDom <= MAX_ROWS_IN_DOM) { + return; // No need to prune + } + + const rowsToRemove = totalRowsInDom - MAX_ROWS_IN_DOM; + if (rowsToRemove <= 0) return; + + if (direction === 'down') { + // User is scrolling down, remove rows from the top + for (let i = 0; i < rowsToRemove; i++) { + if (tableBody.rows[0]) { + tableBody.rows[0].remove(); + } + } + g_currentlyRendered.start += rowsToRemove; + console.log(`Pruned ${rowsToRemove} rows from top. New range: ${g_currentlyRendered.start}-${g_currentlyRendered.end}`); + } else if (direction === 'up') { + // User is scrolling up, remove rows from the bottom + for (let i = 0; i < rowsToRemove; i++) { + const lastRow = tableBody.rows[tableBody.rows.length - 1]; + if (lastRow) { + lastRow.remove(); + } + } + g_currentlyRendered.end -= rowsToRemove; + console.log(`Pruned ${rowsToRemove} rows from bottom. New range: ${g_currentlyRendered.start}-${g_currentlyRendered.end}`); + } +} + function scrollHandler() { if (isLoadingMoreItems || !g_currentDialectInfo) { return; @@ -2710,6 +2750,8 @@ function scrollHandler() { const itemsToRender = activeCategoryData.slice(start, end); renderCategoryItems(itemsToRender, g_currentDialectInfo, g_currentCategory, false, activeCategoryData.length, null, false); lastLoadedIndex = end; + g_currentlyRendered.end = lastLoadedIndex; + pruneOffscreenRows('down'); } isLoadingMoreItems = false; } @@ -2725,6 +2767,8 @@ function scrollHandler() { const itemsToRender = activeCategoryData.slice(start, end); renderCategoryItems(itemsToRender, g_currentDialectInfo, g_currentCategory, false, activeCategoryData.length, null, true); firstLoadedIndex = start; + g_currentlyRendered.start = firstLoadedIndex; + pruneOffscreenRows('up'); const newHeight = table.offsetHeight; window.scrollTo({ top: scrollTop + (newHeight - currentHeight), behavior: 'instant' });