Problem/Motivation
When the number of modules in composer is high, the filtering functionnality by a search string could be really slow and stuck for something like 20 seconds !
Steps to reproduce
Proposed resolution
Response generated by ChatGPT(sorry...) but working great, and make the search instant !
To optimize this Drupal code for filtering a large list of modules and avoid JavaScript freezes (especially when the module list is long), we can improve performance in a few key areas:
✅ Key Optimizations
- Avoid redundant DOM queries inside loops.
- Use textContent concatenation smarter.
- Minimize DOM manipulation (toggle, show, etc.)—batch operations or defer them.
- Avoid jQuery .show() / .hide() per row; instead, use display: none classes with minimal DOM writes.
function hidePackageDetails(index, element) {
const $packDetails = $(element);
const visibleRows = $packDetails[0].querySelectorAll('tbody tr:not([data-hidden])');
$packDetails.toggle(visibleRows.length > 0);
}
function filterModuleList(e) {
const query = e.target.value.trim();
const re = query.length >= 2 ? new RegExp(`\\b${escapeRegExp(query)}`, 'i') : null;
let visibleCount = 0;
if (re) {
searching = true;
$rows.each((index, row) => {
const sources = row.querySelectorAll('.table-filter-text-source, .module-name, .module-description');
let combined = '';
for (let i = 0; i < sources.length; i++) {
combined += sources[i].textContent + ' ';
}
const match = re.test(combined);
if (match) {
row.removeAttribute('data-hidden');
row.style.display = '';
visibleCount++;
} else {
row.setAttribute('data-hidden', 'true');
row.style.display = 'none';
}
});
$details.each((_, detail) => {
detail.setAttribute('open', true);
if (!detail.hasAttribute('open')) {
detail.setAttribute('data-drupal-system-state', 'forced-open');
}
hidePackageDetails(null, detail);
});
Drupal.announce(
Drupal.formatPlural(
visibleCount,
'1 module is available in the modified list.',
'@count modules are available in the modified list.',
),
);
} else if (searching) {
searching = false;
$rows.each((_, row) => {
row.removeAttribute('data-hidden');
row.style.display = '';
});
$details.each((_, detail) => {
if (detail.getAttribute('data-drupal-system-state') === 'forced-open') {
detail.removeAttribute('data-drupal-system-state');
detail.removeAttribute('open');
}
});
}
}
function escapeRegExp(string) {
// Prevent RegExp injection
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function preventEnterKey(event) {
if (event.which === 13) {
event.preventDefault();
event.stopPropagation();
}
}
// Setup
if ($table.length) {
$rowsAndDetails = $table.find('tr, details');
$rows = $table.find('tbody tr');
$details = $rowsAndDetails.filter('.package-listing');
$(input).on({
input: debounce(filterModuleList, 200),
keydown: preventEnterKey,
});
}
⚡ What's Improved
- ✅ Avoided jQuery toggle() calls in loops; used style.display for fast updates.
- ✅ Avoided querying :visible repeatedly—uses data-hidden flags instead.
- ✅ Precompiled RegExp only once.
- ✅ Debounced input listener already present, good for long lists.