From b6f8ec6f0dc2adf7e477acad612c35f79db9cbe8 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:23:03 +0000 Subject: [PATCH] perf(script.js): precalculate search string and dates to avoid render overhead Moved expensive `toLowerCase()` conversions, string concatenations, and `Date` instantiations out of the frequent `renderPDFs` and `createPDFCard` loops. These are now pre-calculated once in `prepareSearchIndex` during the initial data load. Also refactored `renderPDFs` filtering to use explicit early returns for further performance gains. Co-authored-by: MrAlokTech <107493955+MrAlokTech@users.noreply.github.com> --- .jules/bolt.md | 3 ++ script.js | 73 +++++++++++++++++-------- test_frontend.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 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..5aadb79 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-05-17 - [Optimizing Render Loop] +**Learning:** Moving string concatenation and object creations (like `Date`) out of the loop and precalculating derived fields into object properties avoids overhead per render. +**Action:** Use precalculated properties during data fetches. diff --git a/script.js b/script.js index 22f399d..7888cab 100644 --- a/script.js +++ b/script.js @@ -416,6 +416,39 @@ async function syncClassSwitcher() { renderSemesterTabs(); } +function prepareSearchIndex() { + const now = new Date(); + const sevenDaysMs = 7 * 24 * 60 * 60 * 1000; + + for (let i = 0; i < pdfDatabase.length; i++) { + const pdf = pdfDatabase[i]; + + // 1. Pre-calculate search string + const title = pdf.title || ''; + const desc = pdf.description || ''; + const cat = pdf.category || ''; + const author = pdf.author || ''; + pdf._searchStr = `${title} ${desc} ${cat} ${author}`.toLowerCase(); + + // 2. Pre-calculate Date and New status + let uploadDateObj; + if (pdf.uploadDate && typeof pdf.uploadDate.toDate === 'function') { + uploadDateObj = pdf.uploadDate.toDate(); + } else if (pdf.uploadDate) { + uploadDateObj = new Date(pdf.uploadDate); + } else { + uploadDateObj = new Date(); // Fallback + } + + pdf._formattedDate = uploadDateObj.toLocaleDateString('en-US', { + year: 'numeric', month: 'short', day: 'numeric' + }); + + const timeDiff = now - uploadDateObj; + pdf._isNew = timeDiff < sevenDaysMs; + } +} + async function loadPDFDatabase() { if (isMaintenanceActive) return; @@ -454,6 +487,7 @@ async function loadPDFDatabase() { if (shouldUseCache) { pdfDatabase = cachedData; + prepareSearchIndex(); // ⚡ BOLT: Pre-calculate derived fields to avoid per-render overhead // --- FIX: CALL THIS TO POPULATE UI --- syncClassSwitcher(); renderSemesterTabs(); @@ -472,6 +506,8 @@ async function loadPDFDatabase() { pdfDatabase.push({ id: doc.id, ...doc.data() }); }); + prepareSearchIndex(); // ⚡ BOLT: Pre-calculate derived fields to avoid per-render overhead + localStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: new Date().getTime(), data: pdfDatabase @@ -904,27 +940,23 @@ function renderPDFs() { } // Locate renderPDFs() in script.js and update the filter section + // ⚡ BOLT: Explicit early returns and pre-calculated _searchStr for huge perf win const filteredPdfs = pdfDatabase.filter(pdf => { - const matchesSemester = pdf.semester === currentSemester; + // 1. Cheap checks first (equality) + 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; + // 2. Category / Favorites check 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); + // 3. Expensive check last (string search using pre-calculated _searchStr) + if (searchTerm && !pdf._searchStr.includes(searchTerm)) return false; - // Update return statement to include matchesClass - return matchesSemester && matchesClass && matchesCategory && matchesSearch; + return true; }); updatePDFCount(filteredPdfs.length); @@ -994,11 +1026,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 - - const newBadgeHTML = isNew + // ⚡ BOLT: Using pre-calculated _isNew and _formattedDate to avoid per-render Date overhead + const newBadgeHTML = pdf._isNew ? `NEW` : ''; @@ -1011,9 +1040,7 @@ 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' - }); + const formattedDate = pdf._formattedDate || 'Unknown Date'; // Uses global escapeHtml() now diff --git a/test_frontend.py b/test_frontend.py new file mode 100644 index 0000000..8e3c1aa --- /dev/null +++ b/test_frontend.py @@ -0,0 +1,136 @@ +from playwright.sync_api import sync_playwright + +def test_frontend(): + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + # Block external dependencies + page.route("**/*", lambda route: route.continue_() if "127.0.0.1" in route.request.url or "localhost" in route.request.url or route.request.url.startswith("file://") else route.abort()) + + # Seed local storage and block requests + page.goto("file:///app/index.html", wait_until="domcontentloaded") + + page.evaluate(""" + localStorage.setItem('currentClass', 'MSc Chemistry'); + localStorage.setItem('currentSemester', '1'); + + // Provide a mock cached database + const mockDb = { + timestamp: new Date().getTime(), + data: [ + { + id: 'pdf1', + title: 'Organic Synthesis Note', + description: 'Detailed reactions', + category: 'Organic', + author: 'Dr. John', + class: 'MSc Chemistry', + semester: 1, + uploadDate: new Date().toISOString() + }, + { + id: 'pdf2', + title: 'Quantum Mechanics', + description: 'Schrodinger equation', + category: 'Physical', + author: 'Dr. Smith', + class: 'MSc Chemistry', + semester: 1, + uploadDate: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString() // 10 days old + } + ] + }; + localStorage.setItem('classnotes_db_cache', JSON.stringify(mockDb)); + """) + + # Reload to apply mock + page.goto("file:///app/index.html", wait_until="networkidle") + + # Console output for debugging + page.on("console", lambda msg: print(f"Browser console: {msg.text}")) + + # Mock Firestore and other variables missing since external JS is blocked + page.evaluate(""" + window.firebase = { + apps: [{ name: 'mock' }], + initializeApp: () => {}, + auth: () => ({ + signInAnonymously: () => Promise.resolve({ user: { uid: 'mock_uid' } }), + onAuthStateChanged: (cb) => cb(null) + }), + firestore: () => { + const mockFirestore = () => ({ + collection: (colName) => ({ + doc: () => ({ + set: () => Promise.resolve(), + onSnapshot: () => {} + }), + orderBy: () => ({ + limit: () => ({ + get: () => Promise.resolve({ + empty: false, + docs: [{ id: 'pdf1' }] // Match cache ID to bypass "empty" logic and use cache + }) + }), + get: () => Promise.resolve({ + forEach: () => {} + }) + }) + }) + }); + mockFirestore.FieldValue = { + serverTimestamp: () => 'mock_timestamp', + increment: () => 'mock_increment' + }; + return mockFirestore(); + } + }; + window.db = window.firebase.firestore(); + + // Remove preloader + document.getElementById('preloader').classList.add('hidden'); + document.getElementById('contentWrapper').classList.add('active'); + + // Programmatically call init manually as we mocked after domcontentloaded + if (typeof loadPDFDatabase === 'function') { + loadPDFDatabase().then(() => console.log('loadPDFDatabase finished')); + } else { + console.log('loadPDFDatabase not found!'); + } + """) + + page.wait_for_timeout(2000) # wait for render + + # Check if items are rendered + cards_count = page.locator('.pdf-card').count() + print(f"Number of PDF cards rendered: {cards_count}") + assert cards_count == 2, f"Expected 2 cards, got {cards_count}" + + # Check text in cards to verify _isNew and _formattedDate logic didn't break them + card1_text = page.locator('.pdf-card').nth(0).inner_text() + print("Card 1 text:", card1_text) + assert "Organic Synthesis Note" in card1_text + assert "NEW" in card1_text # 1st one is new + + card2_text = page.locator('.pdf-card').nth(1).inner_text() + print("Card 2 text:", card2_text) + assert "Quantum Mechanics" in card2_text + assert "NEW" not in card2_text # 2nd one is 10 days old + + # Test search logic refactoring + page.fill('#searchInput', 'Quantum') + + # We need to trigger input event as Playwright's fill doesn't always trigger standard input listeners that we hooked on + page.evaluate("document.getElementById('searchInput').dispatchEvent(new Event('input', { bubbles: true }))") + + page.wait_for_timeout(500) + + cards_count = page.locator('.pdf-card').count() + print(f"Number of PDF cards after search 'Quantum': {cards_count}") + assert cards_count == 1, f"Expected 1 card after search, got {cards_count}" + + browser.close() + +if __name__ == "__main__": + test_frontend()