From 96f34d52acd5c4d15ac1a27bf44c1ef675aae807 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:38:49 +0000 Subject: [PATCH] Pre-calculate derived properties for render loops Pre-calculate `_searchStr`, `_isNew`, and `_formattedDate` once during initial data load in `prepareSearchIndex`. Utilize these pre-computed properties with explicit early returns in the `renderPDFs` filtering loop and inside `createPDFCard`, avoiding expensive string concatenations, `Date` instantiations, and full-iteration `Array.filter` checks on every user interaction. Co-authored-by: MrAlokTech <107493955+MrAlokTech@users.noreply.github.com> --- .jules/bolt.md | 3 +++ script.js | 62 ++++++++++++++++++++++++++++++------------------ test_frontend.py | 24 +++++++++++++++++++ 3 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 .jules/bolt.md create mode 100644 test_frontend.py diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..19b95ef --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-11-23 - [Pre-calculate derived properties for render loops] +**Learning:** Instantiating `Date` objects repeatedly inside a render or filter loop for large lists causes measurable UI slowdowns. Calculating derived search properties (e.g., lowercased text) during initial data load avoids per-render recalculations. +**Action:** Always pre-calculate and store formatted date properties and search strings on the data objects during initial load. Use explicit early returns (`if (!condition) return false;`) instead of grouped boolean expressions for optimal list filtering performance. diff --git a/script.js b/script.js index 22f399d..c30f9dd 100644 --- a/script.js +++ b/script.js @@ -454,6 +454,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -477,6 +478,7 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(); // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -488,6 +490,29 @@ async function loadPDFDatabase() { } } +// Pre-calculates derived properties on PDF objects to speed up search and render loops +function prepareSearchIndex() { + const now = Date.now(); + const sevenDaysMs = 7 * 24 * 60 * 60 * 1000; + const dateFormatter = new Intl.DateTimeFormat('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + for (let i = 0; i < pdfDatabase.length; i++) { + const pdf = pdfDatabase[i]; + + // Lowercase string for fast searching + pdf._searchStr = (pdf.title + " " + pdf.description + " " + pdf.category + " " + pdf.author).toLowerCase(); + + // Calculate if new + const uploadDateMs = new Date(pdf.uploadDate).getTime(); + pdf._isNew = (now - uploadDateMs) < sevenDaysMs; + + // Format date string once + pdf._formattedDate = dateFormatter.format(new Date(pdf.uploadDate)); + } +} + function hidePreloader() { if (preloader) { preloader.classList.add('hidden'); @@ -905,26 +930,20 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + if (pdf.semester !== currentSemester) return false; + if (pdf.class !== currentClass) return false; - // NEW: Check if the PDF class matches the UI's current class selection - // Note: If old documents don't have this field, they will be hidden. - const matchesClass = pdf.class === currentClass; - - let matchesCategory = false; if (currentCategory === 'favorites') { - matchesCategory = favorites.includes(pdf.id); - } else { - matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; + if (!favorites.includes(pdf.id)) return false; + } else if (currentCategory !== 'all') { + if (pdf.category !== currentCategory) return false; } - const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || - pdf.description.toLowerCase().includes(searchTerm) || - pdf.category.toLowerCase().includes(searchTerm) || - pdf.author.toLowerCase().includes(searchTerm); + if (searchTerm && pdf._searchStr && !pdf._searchStr.includes(searchTerm)) { + return false; + } - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -994,9 +1013,11 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { const heartIconClass = isFav ? 'fas' : 'far'; const btnActiveClass = isFav ? 'active' : ''; - const uploadDateObj = new Date(pdf.uploadDate); - const timeDiff = new Date() - uploadDateObj; - const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + // Use pre-calculated values if available, otherwise fallback + const isNew = pdf._isNew !== undefined ? pdf._isNew : (new Date() - new Date(pdf.uploadDate)) < (7 * 24 * 60 * 60 * 1000); + const formattedDate = pdf._formattedDate || new Date(pdf.uploadDate).toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); const newBadgeHTML = isNew ? `NEW` @@ -1010,11 +1031,6 @@ function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { }; const categoryIcon = categoryIcons[pdf.category] || 'fa-file-pdf'; - // Formatting Date - const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { - year: 'numeric', month: 'short', day: 'numeric' - }); - // Uses global escapeHtml() now const highlightText = (text) => { diff --git a/test_frontend.py b/test_frontend.py new file mode 100644 index 0000000..aa43fd2 --- /dev/null +++ b/test_frontend.py @@ -0,0 +1,24 @@ +from playwright.sync_api import sync_playwright +import time + +def test_frontend(): + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + # Block external resources that might timeout + page.route("**/*", lambda route: route.abort() if route.request.url.startswith("https://firestore.googleapis.com") or route.request.url.startswith("https://www.gstatic.com") else route.continue_()) + + page.goto("http://localhost:8000") + + # We expect it to load, possibly failing to fetch firebase but syntax should be fine + time.sleep(2) + + # Check if basic elements are present + assert page.locator("#searchInput").is_visible(), "Search input should be visible" + + print("Frontend test passed!") + browser.close() + +if __name__ == "__main__": + test_frontend()