Skip to content

Website listings add another update handler on every URL hash change #15206

Closed

Description

@cderv

Each listing on a website page adds a list.on("updated", ...) handler every time the URL hash changes, and pages with several listings get extra handlers from the start. The handlers pile up, so a single listing update reruns the same work more and more times.

window["quarto-listing-loaded"] does two things: it applies the URL hash (category filter, page of each listing), then it attaches the updated handler to every listing registered so far.

window["quarto-listing-loaded"] = () => {
// Process any existing hash
const hash = getHash();
if (hash) {
// If there is a category, switch to that
if (hash.category) {
// category hash are URI encoded so we need to decode it before processing
// so that we can match it with the category element processed in JS
activateCategory(decodeURIComponent(hash.category));
}
// Paginate a specific listing
const listingIds = Object.keys(window["quarto-listings"]);
for (const listingId of listingIds) {
const page = hash[getListingPageKey(listingId)];
if (page) {
showPage(listingId, page);
}
}
}
const listingIds = Object.keys(window["quarto-listings"]);
for (const listingId of listingIds) {
// The actual list
const list = window["quarto-listings"][listingId];
// Update the handlers for pagination events
refreshPaginationHandlers(listingId);
// Render any visible items that need it
renderVisibleProgressiveImages(list);
// Whenever the list is updated, we also need to
// attach handlers to the new pagination elements
// and refresh any newly visible items.
list.on("updated", function () {
renderVisibleProgressiveImages(list);
window.quartoDecorateExternalLinks?.(list.list);
setTimeout(() => refreshPaginationHandlers(listingId));
// Show or hide the no matching message
toggleNoMatchingMessage(list);
});
}
};

The script emitted for each listing calls this function from DOMContentLoaded, and also from a hashchange listener on window:

const jsScript = `
window.document.addEventListener("DOMContentLoaded", function (_event) {
const listingTargetEl = window.document.querySelector('#listing-${id} .list');
if (!listingTargetEl) {
// No listing discovered, do not attach.
return;
}
const options = {
valueNames: ${rowJs},
${pageOptions}
${filterOptions}
};
window['quarto-listings'] = window['quarto-listings'] || {};
window['quarto-listings']['listing-${id}'] = new List('listing-${id}', options);
if (window['quarto-listing-loaded']) {
window['quarto-listing-loaded']();
}
});
window.addEventListener('hashchange',() => {
if (window['quarto-listing-loaded']) {
window['quarto-listing-loaded']();
}
})
`;

So the first listing of a page with two listings gets two handlers on load, and each hash change adds one handler per listing on the page to every listing. A hash change happens with back/forward after clicking a category or a page, or with an in-page # link.

With two paginated listings, counting how many times the handler runs for one list.update(), in Chromium:

first listing second listing
After load 2 1
After 3 hash changes 8 7

Nothing is duplicated on the page today, as every step of the handler can run several times without side effect (the external link icon step was made safe for this in #9469). It is only repeated work, but any non idempotent step added to this handler would show it.

The hashchange listener was added in 962d83d for #1139, and only the hash part is needed there. We could attach the handler once per listing, and use a single hashchange listener in quarto-listing.js that only applies the hash. Back/forward between categories also relies on this listener, so this behavior needs to stay.

Activity

  1. added this to the v1.11 milestone on Oct 27, 2026
  2. added a commit that references this issue on Oct 27, 2026
    4906587
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinglistings

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions