diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..7c894a1 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-24 - Pre-calculating search strings and dates for render loops +**Learning:** Repeated Date formatting, object instantiations, and string lowercasing operations inside the `renderPDFs` loop and its filter functions cause significant CPU overhead and UI lag during user search typing, as these operations run for every item on every keystroke/render cycle. +**Action:** Implement `prepareSearchIndex` to pre-calculate `_searchStr` (a lowercased concatenation of searchable fields), `_formattedDate`, and `_isNew` once when the PDF database is loaded. Use these cached properties in `renderPDFs` and `createPDFCard` along with early returns in the filter callback to speed up filtering and rendering. diff --git a/script.js b/script.js index bdc06f3..b0489fb 100644 --- a/script.js +++ b/script.js @@ -335,6 +335,33 @@ function getAdData(slotName) { /* ========================================= 5. DATA LOADING WITH CACHING ========================================= */ + +// OPTIMIZATION: Pre-calculate expensive values for search and rendering +function prepareSearchIndex(pdfs) { + const now = new Date(); + const sevenDaysMs = 7 * 24 * 60 * 60 * 1000; + + pdfs.forEach(pdf => { + // Handle both raw Date strings and Firestore Timestamps + const uploadDateObj = pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function' + ? pdf.uploadDate.toDate() + : new Date(pdf.uploadDate); + + // Pre-calculate search string + pdf._searchStr = `${pdf.title} ${pdf.description} ${pdf.category} ${pdf.author || ''}`.toLowerCase(); + + // Pre-calculate formatted date + pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + // Pre-calculate isNew boolean + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < sevenDaysMs; + }); + return pdfs; +} + function renderSemesterTabs() { const container = document.getElementById('semesterTabsContainer'); if (!container) return; @@ -451,6 +478,10 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + // The cached data already went through prepareSearchIndex when it was saved, + // but we need to re-run it in case `now` has changed to update `_isNew`. + prepareSearchIndex(pdfDatabase); + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -469,6 +500,9 @@ async function loadPDFDatabase() { pdfDatabase.push({ id: doc.id, ...doc.data() }); }); + // Pre-calculate expensive properties before saving to cache + prepareSearchIndex(pdfDatabase); + localStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: new Date().getTime(), data: pdfDatabase @@ -476,6 +510,10 @@ async function loadPDFDatabase() { // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); + // Missing render calls for fresh fetch (similar to cache hit): + renderSemesterTabs(); + renderCategoryFilters(); + renderPDFs(); hidePreloader(); @@ -902,26 +940,25 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // OPTIMIZATION: Early returns for cheap equality checks first + + // 1. Check Class + 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; + // 2. Check Semester + if (pdf.semester !== currentSemester) return false; - let matchesCategory = false; + // 3. Check Category / Favorites 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); + // 4. Check Search (using pre-calculated lowercased string) + if (searchTerm && !pdf._searchStr.includes(searchTerm)) return false; - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -991,11 +1028,7 @@ 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 - - const newBadgeHTML = isNew + const newBadgeHTML = pdf._isNew ? `NEW` : ''; @@ -1007,10 +1040,8 @@ 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' - }); + // OPTIMIZATION: Use pre-calculated date string + const formattedDate = pdf._formattedDate; // Uses global escapeHtml() now