diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..8b3d054 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-17 - [Pre-Calculate Expensive Fields for Search/Filter] +**Learning:** Performing multiple Date instantiation, toLowerCase conversions, and string formatting inside high-frequency UI render loops like `renderPDFs` creates significant CPU overhead, especially as the size of `pdfDatabase` grows. +**Action:** Always map the dataset once during the initial data load (`loadPDFDatabase`) to pre-calculate and cache expensive derived strings/flags on the objects (e.g. `_searchStr`, `_formattedDate`, `_isNew`), then use simple accessors and fast equality checks in the filter array callbacks. Add these properties AFTER the raw JSON payload is cached to localStorage to prevent cache bloat. diff --git a/script.js b/script.js index 22f399d..9ffb987 100644 --- a/script.js +++ b/script.js @@ -338,6 +338,39 @@ function getAdData(slotName) { /* ========================================= 5. DATA LOADING WITH CACHING ========================================= */ + +/** + * ⚡ Bolt Optimization: Pre-calculate expensive runtime properties + * What: Calculates `_searchStr`, `_formattedDate`, and `_isNew` once during data load. + * Why: Prevents creating Date objects and concatenating/lowercasing strings on every keystroke in `renderPDFs()`. + * Impact: Significantly reduces CPU overhead during list rendering and filtering, speeding up search. + */ +function prepareSearchIndex(database) { + const now = new Date(); + database.forEach(pdf => { + // Handle Date (Firestore Timestamp vs ISO string from cache) + let uploadDateObj; + if (pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function') { + uploadDateObj = pdf.uploadDate.toDate(); + } else { + uploadDateObj = new Date(pdf.uploadDate); + } + + pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + + const title = pdf.title || ''; + const desc = pdf.description || ''; + const cat = pdf.category || ''; + const author = pdf.author || ''; + pdf._searchStr = `${title} ${desc} ${cat} ${author}`.toLowerCase(); + }); +} + function renderSemesterTabs() { const container = document.getElementById('semesterTabsContainer'); if (!container) return; @@ -454,6 +487,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(pdfDatabase); // ⚡ Bolt: Pre-calculate index // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -477,6 +511,7 @@ async function loadPDFDatabase() { data: pdfDatabase })); + prepareSearchIndex(pdfDatabase); // ⚡ Bolt: Pre-calculate index AFTER caching raw data // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -905,26 +940,22 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // ⚡ Bolt Optimization: Early Returns for cheaper checks + if (pdf.class !== currentClass) return false; + if (pdf.semester !== currentSemester) 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); + // ⚡ Bolt Optimization: Use pre-calculated search string + if (searchTerm && pdf._searchStr) { + if (!pdf._searchStr.includes(searchTerm)) return false; + } - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -994,9 +1025,9 @@ 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 + // ⚡ Bolt Optimization: Use pre-calculated flags/strings instead of Date math in loops + const isNew = pdf._isNew || false; + const formattedDate = pdf._formattedDate || ''; const newBadgeHTML = isNew ? `NEW` @@ -1010,11 +1041,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) => {