From c0d1b94b715606bcde9e733673b95f47c4a1f5ff Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:13:10 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Optimize=20search=20filter?= =?UTF-8?q?=20by=20pre-calculating=20index=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved expensive regex processing and date parsing out of `renderPDFs` - Pre-calculate `_searchStr` and `_formattedDate` once during db load via `prepareSearchIndex` - Implemented fast early-returns inside `renderPDFs` filter loop using boolean checks and basic string `.includes()` - Added unit tests for inferLabel utility Co-authored-by: MrAlokTech <107493955+MrAlokTech@users.noreply.github.com> --- .jules/bolt.md | 3 + script.js | 74 ++++++++++++------ test_perf.js | 160 +++++++++++++++++++++++++++++++++++++++ tests/test_inferLabel.js | 16 ++++ 4 files changed, 231 insertions(+), 22 deletions(-) create mode 100644 .jules/bolt.md create mode 100644 test_perf.js create mode 100644 tests/test_inferLabel.js diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..9b82f27 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-03-26 - [Search and Render Performance Optimization] +**Learning:** In a vanilla JS SPA that lacks a virtual DOM, running heavy string concatenations (e.g. for search index preparation) and new `Date` object initializations for every item during every keystroke causes significant rendering stutter. +**Action:** Move derived data calculations to an initialization phase (`prepareSearchIndex`) but ensure they are attached to the data *after* `localStorage` persistence. Bloating `localStorage` with runtime derivatives can cause QuotaExceeded errors, so in-memory only is best. diff --git a/script.js b/script.js index 22f399d..6591eaa 100644 --- a/script.js +++ b/script.js @@ -338,6 +338,34 @@ function getAdData(slotName) { /* ========================================= 5. DATA LOADING WITH CACHING ========================================= */ + +// Bolt: Optimizing search and rendering by pre-calculating derived fields +// This prevents expensive string concatenations and Date parsing during each keystroke in renderPDFs() +function prepareSearchIndex(data) { + const now = new Date(); + data.forEach(pdf => { + if (!pdf._searchStr) { + pdf._searchStr = `${pdf.title || ''} ${pdf.description || ''} ${pdf.category || ''} ${pdf.author || ''}`.toLowerCase(); + } + if (!pdf._formattedDate) { + let dateVal = pdf.uploadDate; + if (dateVal && typeof dateVal.toDate === 'function') { + dateVal = dateVal.toDate(); // Handle Firestore Timestamp if fresh + } else if (dateVal) { + dateVal = new Date(dateVal); // Handle ISO string from cache + } else { + dateVal = new Date(); // Fallback + } + + pdf._formattedDate = dateVal.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const timeDiff = now - dateVal; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + } + }); +} + function renderSemesterTabs() { const container = document.getElementById('semesterTabsContainer'); if (!container) return; @@ -454,6 +482,10 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + + // Bolt: Pre-calculate expensive derived properties for fast search + prepareSearchIndex(pdfDatabase); + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -472,11 +504,15 @@ async function loadPDFDatabase() { pdfDatabase.push({ id: doc.id, ...doc.data() }); }); + // Bolt: Save to cache FIRST before index preparation to prevent bloating localStorage localStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: new Date().getTime(), data: pdfDatabase })); + // Bolt: Pre-calculate search strings and formats for fast render iteration + prepareSearchIndex(pdfDatabase); + // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderPDFs(); @@ -905,26 +941,23 @@ function renderPDFs() { // Locate renderPDFs() in script.js and update the filter section const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; - - // 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; + // Bolt: Optimize filtering with early returns on cheap boolean checks + // Avoids expensive regex/string evaluations if the item isn't in the correct class/semester + if (pdf.semester !== currentSemester) return false; + if (pdf.class !== currentClass) return false; - 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: Use pre-calculated search string and early exit for truthiness guard + if (searchTerm) { + if (!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 +1027,8 @@ 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: Use pre-calculated values to skip expensive object creation per card + const isNew = pdf._isNew; const newBadgeHTML = isNew ? `NEW` @@ -1010,10 +1042,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' - }); + // Bolt: Use pre-calculated formatted date + const formattedDate = pdf._formattedDate; // Uses global escapeHtml() now diff --git a/test_perf.js b/test_perf.js new file mode 100644 index 0000000..a2f9e30 --- /dev/null +++ b/test_perf.js @@ -0,0 +1,160 @@ +const fs = require('fs'); + +const scriptContent = fs.readFileSync('script.js', 'utf8'); + +// We'll mock the DOM and some globals to test performance. +const domMock = ` +const document = { + getElementById: () => ({ classList: { add: () => {}, remove: () => {} }, style: {}, dataset: {}, value: 'test' }), + querySelectorAll: () => [], + querySelector: () => ({ classList: { add: () => {}, remove: () => {} }, style: {} }), + createElement: () => ({ classList: { add: () => {} }, style: {} }), +}; +const window = { + location: { search: '' }, + addEventListener: () => {}, + matchMedia: () => ({ matches: false }), + scrollY: 0, +}; +const localStorage = { getItem: () => null, setItem: () => {} }; +const sessionStorage = { getItem: () => null, setItem: () => {} }; +const navigator = { userAgent: 'test', platform: 'test' }; +const searchInput = { value: 'test', trim: () => 'test' }; +const getFavorites = () => ['1', '2']; +const escapeHtml = (t) => t; +let pdfDatabase = []; +let currentSemester = 1; +let currentClass = 'MSc Chemistry'; +let currentCategory = 'all'; + +function createPDFCard(pdf, favoritesList, index = 0, highlightRegex = null) { + const favorites = favoritesList || getFavorites(); + const isFav = favorites.includes(pdf.id); + const uploadDateObj = new Date(pdf.uploadDate); + const timeDiff = new Date() - uploadDateObj; + const isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); // 7 days + const formattedDate = new Date(pdf.uploadDate).toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + return 'card'; +} + +function renderPDFsBefore() { + const searchTerm = searchInput.value.toLowerCase(); + const favorites = getFavorites(); + + const filteredPdfs = pdfDatabase.filter(pdf => { + const matchesSemester = pdf.semester === currentSemester; + const matchesClass = pdf.class === currentClass; + + let matchesCategory = false; + if (currentCategory === 'favorites') { + matchesCategory = favorites.includes(pdf.id); + } else { + matchesCategory = currentCategory === 'all' || pdf.category === currentCategory; + } + + const matchesSearch = pdf.title.toLowerCase().includes(searchTerm) || + pdf.description.toLowerCase().includes(searchTerm) || + pdf.category.toLowerCase().includes(searchTerm) || + pdf.author.toLowerCase().includes(searchTerm); + + return matchesSemester && matchesClass && matchesCategory && matchesSearch; + }); + + let gridHTML = ""; + filteredPdfs.forEach((pdf, index) => { + gridHTML += createPDFCard(pdf, favorites, index, null); + }); +} + +function prepareSearchIndex(data) { + const now = new Date(); + data.forEach(pdf => { + if (!pdf._searchStr) { + pdf._searchStr = \`\${pdf.title || ''} \${pdf.description || ''} \${pdf.category || ''} \${pdf.author || ''}\`.toLowerCase(); + } + if (!pdf._formattedDate) { + let dateVal = pdf.uploadDate; + if (dateVal && typeof dateVal.toDate === 'function') { + dateVal = dateVal.toDate(); + } else { + dateVal = new Date(dateVal); + } + pdf._formattedDate = dateVal.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + const timeDiff = now - dateVal; + pdf._isNew = timeDiff < (7 * 24 * 60 * 60 * 1000); + } + }); +} + +function createPDFCardAfter(pdf, favoritesList, index = 0, highlightRegex = null) { + const favorites = favoritesList || getFavorites(); + const isFav = favorites.includes(pdf.id); + const isNew = pdf._isNew; + const formattedDate = pdf._formattedDate; + return 'card'; +} + +function renderPDFsAfter() { + const searchTerm = searchInput.value.toLowerCase(); + const favorites = getFavorites(); + + const filteredPdfs = pdfDatabase.filter(pdf => { + if (pdf.semester !== currentSemester) return false; + if (pdf.class !== currentClass) return false; + + if (currentCategory === 'favorites') { + if (!favorites.includes(pdf.id)) return false; + } else if (currentCategory !== 'all') { + if (pdf.category !== currentCategory) return false; + } + + if (searchTerm) { + if (!pdf._searchStr || !pdf._searchStr.includes(searchTerm)) return false; + } + + return true; + }); + + let gridHTML = ""; + filteredPdfs.forEach((pdf, index) => { + gridHTML += createPDFCardAfter(pdf, favorites, index, null); + }); +} + +// Create 10000 mock PDFs +for (let i = 0; i < 10000; i++) { + pdfDatabase.push({ + id: i.toString(), + title: 'Test Title ' + i, + description: 'Test description goes here ' + i, + category: 'Organic', + author: 'John Doe', + semester: 1, + class: 'MSc Chemistry', + uploadDate: new Date().toISOString() + }); +} + +console.log("Benchmarking before..."); +const startBefore = performance.now(); +for (let i = 0; i < 100; i++) renderPDFsBefore(); +const endBefore = performance.now(); +console.log("Before:", endBefore - startBefore, "ms"); + +console.log("Benchmarking after..."); +const startPrepare = performance.now(); +prepareSearchIndex(pdfDatabase); +const endPrepare = performance.now(); +console.log("Prepare time:", endPrepare - startPrepare, "ms"); + +const startAfter = performance.now(); +for (let i = 0; i < 100; i++) renderPDFsAfter(); +const endAfter = performance.now(); +console.log("After:", endAfter - startAfter, "ms"); +`; + +eval(domMock); diff --git a/tests/test_inferLabel.js b/tests/test_inferLabel.js new file mode 100644 index 0000000..9097529 --- /dev/null +++ b/tests/test_inferLabel.js @@ -0,0 +1,16 @@ +const assert = require('assert'); +const fs = require('fs'); + +const scriptContent = fs.readFileSync('script.js', 'utf8'); + +const inferLabelMatch = scriptContent.match(/function inferLabelFromTitle\(title\) \{[\s\S]*?return "notes";\n\}/); +if (inferLabelMatch) { + eval(inferLabelMatch[0]); + assert.strictEqual(inferLabelFromTitle('Chemistry Notes'), 'notes'); + assert.strictEqual(inferLabelFromTitle('Math Syllabus 2024'), 'syllabus'); + assert.strictEqual(inferLabelFromTitle('Physics Exam Question Paper'), 'question paper'); + assert.strictEqual(inferLabelFromTitle('Assignment 1'), 'assignment'); + console.log('test_inferLabel.js passed'); +} else { + console.log('inferLabelFromTitle not found'); +}