Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ const CATEGORY_META = {
other: { className: 'bg-zinc-100 text-zinc-600', label: 'Other' },
};

// ══════════════════════════════════════════════
// UTILITY FUNCTIONS
// ══════════════════════════════════════════════
/**
* Returns a debounced version of fn that delays invoking it until after
* `delay` ms have elapsed since the last call. Prevents excessive filter
* re-renders on every keystroke in search inputs.
*/
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}

const LANGUAGE_ALIASES = {
'python': ['python'],
'javascript': ['javascript', 'js'],
Expand Down Expand Up @@ -2463,11 +2479,13 @@ document.addEventListener('DOMContentLoaded', () => {
renderGoodFirstIssues();

// Wire up filter event listeners
document.getElementById('searchInput')?.addEventListener('input', applyFilters);
const debouncedApplyFilters = debounce(applyFilters, 300);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: In a performance-focused PR aiming to prevent excessive re-renders, the debounce setup here leaves a gap: other filter controls call applyFilters() (or renderMentorFinder) directly without canceling a pending debounced search timer first. Because these functions read fresh DOM state on every call, the delayed invocation will re-process the same already-rendered state and trigger a redundant full filter/render cycle.

Consider making the debounce cancelable and clearing the pending timer before synchronous filter invocations, or having applyFilters short-circuit when no input state has actually changed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/js/app.js, line 2482:

<comment>In a performance-focused PR aiming to prevent excessive re-renders, the debounce setup here leaves a gap: other filter controls call `applyFilters()` (or `renderMentorFinder`) directly without canceling a pending debounced search timer first. Because these functions read fresh DOM state on every call, the delayed invocation will re-process the same already-rendered state and trigger a redundant full filter/render cycle.

Consider making the debounce cancelable and clearing the pending timer before synchronous filter invocations, or having `applyFilters` short-circuit when no input state has actually changed.</comment>

<file context>
@@ -2463,11 +2479,13 @@ document.addEventListener('DOMContentLoaded', () => {
 
   // Wire up filter event listeners
-  document.getElementById('searchInput')?.addEventListener('input', applyFilters);
+  const debouncedApplyFilters = debounce(applyFilters, 300);
+  document.getElementById('searchInput')?.addEventListener('input', debouncedApplyFilters);
   document.getElementById('categoryFilter')?.addEventListener('change', applyFilters);
</file context>

document.getElementById('searchInput')?.addEventListener('input', debouncedApplyFilters);
document.getElementById('categoryFilter')?.addEventListener('change', applyFilters);
document.getElementById('complexityFilter')?.addEventListener('change', applyFilters);
document.getElementById('sortSelect')?.addEventListener('change', applyFilters);
document.getElementById('mentorSearchInput')?.addEventListener('input', renderMentorFinder);
const debouncedRenderMentorFinder = debounce(renderMentorFinder, 300);
document.getElementById('mentorSearchInput')?.addEventListener('input', debouncedRenderMentorFinder);
document.getElementById('mentorChannelFilter')?.addEventListener('change', renderMentorFinder);
document.getElementById('matchAllLanguagesToggle')?.addEventListener('change', (e) => {
matchAllLanguages = e.target.checked;
Expand Down Expand Up @@ -2518,7 +2536,7 @@ document.addEventListener('DOMContentLoaded', () => {
searchInput.value = e.target.value;
const orgsSec = document.getElementById('orgs');
if (orgsSec) orgsSec.scrollIntoView({ behavior: 'smooth' });
applyFilters();
debouncedApplyFilters();
}
});
}
Expand Down
Loading