admin管理员组

文章数量:1023758

I have a program that generates HTML reports for users. These reports have some ponents made with jQuery and Bootstrap. It mainly creates dropdown ponents on certain tags with something like this:

$(document).ready(function () {
    $('.my-dropdown').each(function () {
        // ...
        $(this).collapse("hide");
        // ...
    }
}

The problem is some of these HTML reports have a large number of dropdowns. Sometimes a report can have over 5,000 dropdowns and causes the entire page to bee unresponsive for over a minute. I have verified that removing the above block of code fixes the page unresponsiveness (but loses the dropdown toggle capability).

There is nothing that can be done about the number of dropdowns in the page. The users prefer to just wait a couple minutes to let the page load because its easier to find what they need on a single page, than to look across a couple different pages for what they need.

Since the report cannot be broken up into different pages, is there a way to write the JavaScript in a way so the browser will not hang while the JavaScript creates the ponents? Using defer or await does not work because it's not inherently a large amount of Javascript that's being executed, it's a large amount of HTML causing a large amount of JavaScript execution.

Is there anything in frameworks such as React or newer standards such as Web Components that can help with this problem? I'm not expecting the browser to be able to process a couple thousand ponents in under a second, but if there's a way to get the unresponsiveness down to something like 15 seconds, that would make things much better for the users.

I have a program that generates HTML reports for users. These reports have some ponents made with jQuery and Bootstrap. It mainly creates dropdown ponents on certain tags with something like this:

$(document).ready(function () {
    $('.my-dropdown').each(function () {
        // ...
        $(this).collapse("hide");
        // ...
    }
}

The problem is some of these HTML reports have a large number of dropdowns. Sometimes a report can have over 5,000 dropdowns and causes the entire page to bee unresponsive for over a minute. I have verified that removing the above block of code fixes the page unresponsiveness (but loses the dropdown toggle capability).

There is nothing that can be done about the number of dropdowns in the page. The users prefer to just wait a couple minutes to let the page load because its easier to find what they need on a single page, than to look across a couple different pages for what they need.

Since the report cannot be broken up into different pages, is there a way to write the JavaScript in a way so the browser will not hang while the JavaScript creates the ponents? Using defer or await does not work because it's not inherently a large amount of Javascript that's being executed, it's a large amount of HTML causing a large amount of JavaScript execution.

Is there anything in frameworks such as React or newer standards such as Web Components that can help with this problem? I'm not expecting the browser to be able to process a couple thousand ponents in under a second, but if there's a way to get the unresponsiveness down to something like 15 seconds, that would make things much better for the users.

Share Improve this question edited Jul 15, 2021 at 20:24 Heretic Monkey 12.1k7 gold badges61 silver badges131 bronze badges asked Jul 15, 2021 at 20:17 user1428945user1428945 4212 silver badges13 bronze badges 7
  • 1 Use pagination. You do not need to render entire lists. – PM 77-1 Commented Jul 15, 2021 at 20:23
  • 1 First of, the main issue here is that all your elements are visible by default, and than you're calling collapse on thousands of elements and the browser fights to close item by item and recalculate the elements flow, repaint and render. No framework can help you here if you're doing it the wrong way. Rather Hide all elements by default - and only on click (or whatever) open the target element. Secondly, there's lazy loading of DOM elements via AJAX - depending on the scroll, or as suggested pagination is also an option. – Roko C. Buljan Commented Jul 15, 2021 at 20:23
  • Albeit unconventional, if you use document fragments, then it will only paint once. – IcyIcicle Commented Jul 15, 2021 at 20:26
  • @IcyIcicle that's not the problem. And new DocumentFragment() is of no help here. – Roko C. Buljan Commented Jul 15, 2021 at 20:27
  • In CSS: .loading .my-dropdown { visibility: hidden; }. In HTML <body class="loading">. In JavaScript: window.addEventListener('load', () => document.body.classList.remove("loading")); Your page will load with no dropdowns showing, the page will load, then the dropdowns will display. I have no idea if it will help though... – Heretic Monkey Commented Jul 15, 2021 at 20:27
 |  Show 2 more ments

1 Answer 1

Reset to default 8

Just to put as answer:

First of, the main issue here is that all your elements are visible by default, and than you're calling "collapse" on DOM ready via JavaScript on thousands of elements. The browser fights to close item by item and recalculate the elements flow, repaint and render.

There's no such framework to help you - if you're doing it the wrong way.
Rather, hide all elements by default via CSS, not via JS - and only on click (or whatever) open the target element.

Secondly, there's lazy loading of DOM elements via AJAX - depending on the scroll, or as suggested pagination is also an amazing option.

Modern web pilers, frameworks, like Svelte, Vue, React can help you track a state of your current data, but then the principle is the same, framework or no framework, on scroll AJAX is the one that will trigger a HTTP request for more data, the framework will only help in effortlessly update the view as the data updates. And PS, 15 seconds is even for a business tailored application way too much time.

So to recap 5000 elements should not be a major issue for a modern browser. Just invert your logic. Hide using CSS by default, Use collapse-expand only when a user action is registered.

Example 1

showing that even 5000 collapsible elements or more than 80000 DOM elements are no issue for the browser if done right:

const tpl_collapse = (item) => `
<div class="Collapse">
  <div class="Collapse-head">${item.i}. Character: &#x${item.hex};</div>
  <div class="Collapse-body">
    Hex: <code>&amp#x${item.hex};</code><br>
    Num: <code>&amp#${item.i};</code><br>
    CSS/JS: <code>\\${item.hex}</code><br>
  </div>
</div>`;

const data = [...Array(10001).keys()].map((_, i) => ({i,hex: i.toString(16)}));
const accordions = data.map(tpl_collapse).join("");
const EL_container = document.querySelector("#container");

EL_container.innerHTML = accordions;
EL_container.addEventListener("click", (ev) => {
  if (ev.target.closest(".Collapse-head")) {
    ev.target.closest(".Collapse").classList.toggle("expanded");
  }
})
.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}
<div id="container"></div>

Example 2

Dynamically loading N collapsible elements:

If you have a good eye you might have noticed that the above took "only" a couple of seconds (on a relatively good desktop machine).
That's also unacceptable. So let's try to use a dynamic approach of fetching and creating elements!

One way is to assign to the most bottom element an Intersection Observer API. Once the page is scrolled and/or that element enters in viewport, simply load the next page (100 items-set, in this example).

You might use AJAX here, but I'll use pure code generation for this purpose. Anyways here's a proof of concept.

Notice the immediate rendering (no more wait time):

let page = 1;             // Current page
const itemsPerPage = 100; // Items per page

const inViewport = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting && +entry.target.dataset.page === page) {
      page += 1;   // Increment page!
      fetchPage(); // Fetch next page
    }
  });
};
const observer = new IntersectionObserver(inViewport);

const EL_container = document.querySelector("#container");
const NewEL = (tag, prop) => Object.assign(document.createElement(tag), prop);

const EL_item = (item) => {
  const EL = NewEL("div", {
    className: "Collapse",
    innerHTML: `<div class="Collapse-head">${item.index}. Character: &#x${item.hex};</div>
      <div class="Collapse-body">
        Hex: <code>&amp#x${item.hex};</code><br>
        Num: <code>&amp#${item.index};</code><br>
        CSS/JS: <code>\\${item.hex}</code><br>
      </div>`
  });
  EL.dataset.page = page; // Associate page to data-page attribute
  // Observe only last element
  if (!((item.index + 1) % itemsPerPage)) observer.observe(EL);
  
  return EL;
}

// Just to programmatically emulate an AJAX call of N items in a page-range
const fetchPage = () => {
  const DF_items = [...Array(itemsPerPage).keys()].reduce((DF, index) => {
    const i = (page - 1) * itemsPerPage + index;
    DF.append(EL_item({index: i,hex: i.toString(16), page}));
    return DF;
  }, new DocumentFragment());
  EL_container.append(DF_items);
};

EL_container.addEventListener("click", (ev) => {
  if (!ev.target.closest(".Collapse-head")) return;
  ev.target.closest(".Collapse").classList.toggle("expanded");
});

// INIT!
fetchPage();
.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}
<div id="container"></div>

Example 3

Pagination as the most mon way to tackle this issue

let page = 1;            // Current page
const itemsPerPage = 50; // Items per page

const EL_container = document.querySelector("#container");
const EL_prev = document.querySelector("#prev");
const EL_next = document.querySelector("#next");
const EL_goto = document.querySelector("#goto");

const NewEL = (tag, prop) => Object.assign(document.createElement(tag), prop);

const EL_item = (item) => {
  return NewEL("div", {
    className: "Collapse",
    innerHTML: `<div class="Collapse-head">${item.index}. Character: &#x${item.hex};</div>
      <div class="Collapse-body">
        Hex: <code>&amp#x${item.hex};</code><br>
        Num: <code>&amp#${item.index};</code><br>
        CSS/JS: <code>\\${item.hex}</code><br>
      </div>`
  });
}

const fetchPage = () => {
  page = Math.abs(page) || 1; // Fix for negative or 0 page number
  
  const DF_items = [...Array(itemsPerPage).keys()].reduce((DF, index) => {
    const i = (page - 1) * itemsPerPage + index;
    DF.append(EL_item({index: i, hex: i.toString(16), page }));
    return DF;
  }, new DocumentFragment());
  EL_container.innerHTML = "";
  EL_container.append(DF_items);
  EL_goto.value = page;
};

EL_container.addEventListener("click", (ev) => {
  if (!ev.target.closest(".Collapse-head")) return;
  ev.target.closest(".Collapse").classList.toggle("expanded");
});

EL_goto.addEventListener("input", () => {
  page = parseInt(EL_goto.value, 10);
  if (page) fetchPage();
});

EL_prev.addEventListener("click", () => {
  page -= 1;
  fetchPage();
});

EL_next.addEventListener("click", () => {
  page += 1;
  fetchPage();
});

// INIT!
fetchPage();
#container {
  height: calc(100vh - 40px);
  overflow: auto;
}

.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}

#goto {
  text-align:center;
  width: 100px;
}
<button id="prev" type="button">PREV</button>
<input id="goto" type="number" value="1" min="1">
<button id="next" type="button">NEXT</button>
<div id="container"></div>

I have a program that generates HTML reports for users. These reports have some ponents made with jQuery and Bootstrap. It mainly creates dropdown ponents on certain tags with something like this:

$(document).ready(function () {
    $('.my-dropdown').each(function () {
        // ...
        $(this).collapse("hide");
        // ...
    }
}

The problem is some of these HTML reports have a large number of dropdowns. Sometimes a report can have over 5,000 dropdowns and causes the entire page to bee unresponsive for over a minute. I have verified that removing the above block of code fixes the page unresponsiveness (but loses the dropdown toggle capability).

There is nothing that can be done about the number of dropdowns in the page. The users prefer to just wait a couple minutes to let the page load because its easier to find what they need on a single page, than to look across a couple different pages for what they need.

Since the report cannot be broken up into different pages, is there a way to write the JavaScript in a way so the browser will not hang while the JavaScript creates the ponents? Using defer or await does not work because it's not inherently a large amount of Javascript that's being executed, it's a large amount of HTML causing a large amount of JavaScript execution.

Is there anything in frameworks such as React or newer standards such as Web Components that can help with this problem? I'm not expecting the browser to be able to process a couple thousand ponents in under a second, but if there's a way to get the unresponsiveness down to something like 15 seconds, that would make things much better for the users.

I have a program that generates HTML reports for users. These reports have some ponents made with jQuery and Bootstrap. It mainly creates dropdown ponents on certain tags with something like this:

$(document).ready(function () {
    $('.my-dropdown').each(function () {
        // ...
        $(this).collapse("hide");
        // ...
    }
}

The problem is some of these HTML reports have a large number of dropdowns. Sometimes a report can have over 5,000 dropdowns and causes the entire page to bee unresponsive for over a minute. I have verified that removing the above block of code fixes the page unresponsiveness (but loses the dropdown toggle capability).

There is nothing that can be done about the number of dropdowns in the page. The users prefer to just wait a couple minutes to let the page load because its easier to find what they need on a single page, than to look across a couple different pages for what they need.

Since the report cannot be broken up into different pages, is there a way to write the JavaScript in a way so the browser will not hang while the JavaScript creates the ponents? Using defer or await does not work because it's not inherently a large amount of Javascript that's being executed, it's a large amount of HTML causing a large amount of JavaScript execution.

Is there anything in frameworks such as React or newer standards such as Web Components that can help with this problem? I'm not expecting the browser to be able to process a couple thousand ponents in under a second, but if there's a way to get the unresponsiveness down to something like 15 seconds, that would make things much better for the users.

Share Improve this question edited Jul 15, 2021 at 20:24 Heretic Monkey 12.1k7 gold badges61 silver badges131 bronze badges asked Jul 15, 2021 at 20:17 user1428945user1428945 4212 silver badges13 bronze badges 7
  • 1 Use pagination. You do not need to render entire lists. – PM 77-1 Commented Jul 15, 2021 at 20:23
  • 1 First of, the main issue here is that all your elements are visible by default, and than you're calling collapse on thousands of elements and the browser fights to close item by item and recalculate the elements flow, repaint and render. No framework can help you here if you're doing it the wrong way. Rather Hide all elements by default - and only on click (or whatever) open the target element. Secondly, there's lazy loading of DOM elements via AJAX - depending on the scroll, or as suggested pagination is also an option. – Roko C. Buljan Commented Jul 15, 2021 at 20:23
  • Albeit unconventional, if you use document fragments, then it will only paint once. – IcyIcicle Commented Jul 15, 2021 at 20:26
  • @IcyIcicle that's not the problem. And new DocumentFragment() is of no help here. – Roko C. Buljan Commented Jul 15, 2021 at 20:27
  • In CSS: .loading .my-dropdown { visibility: hidden; }. In HTML <body class="loading">. In JavaScript: window.addEventListener('load', () => document.body.classList.remove("loading")); Your page will load with no dropdowns showing, the page will load, then the dropdowns will display. I have no idea if it will help though... – Heretic Monkey Commented Jul 15, 2021 at 20:27
 |  Show 2 more ments

1 Answer 1

Reset to default 8

Just to put as answer:

First of, the main issue here is that all your elements are visible by default, and than you're calling "collapse" on DOM ready via JavaScript on thousands of elements. The browser fights to close item by item and recalculate the elements flow, repaint and render.

There's no such framework to help you - if you're doing it the wrong way.
Rather, hide all elements by default via CSS, not via JS - and only on click (or whatever) open the target element.

Secondly, there's lazy loading of DOM elements via AJAX - depending on the scroll, or as suggested pagination is also an amazing option.

Modern web pilers, frameworks, like Svelte, Vue, React can help you track a state of your current data, but then the principle is the same, framework or no framework, on scroll AJAX is the one that will trigger a HTTP request for more data, the framework will only help in effortlessly update the view as the data updates. And PS, 15 seconds is even for a business tailored application way too much time.

So to recap 5000 elements should not be a major issue for a modern browser. Just invert your logic. Hide using CSS by default, Use collapse-expand only when a user action is registered.

Example 1

showing that even 5000 collapsible elements or more than 80000 DOM elements are no issue for the browser if done right:

const tpl_collapse = (item) => `
<div class="Collapse">
  <div class="Collapse-head">${item.i}. Character: &#x${item.hex};</div>
  <div class="Collapse-body">
    Hex: <code>&amp#x${item.hex};</code><br>
    Num: <code>&amp#${item.i};</code><br>
    CSS/JS: <code>\\${item.hex}</code><br>
  </div>
</div>`;

const data = [...Array(10001).keys()].map((_, i) => ({i,hex: i.toString(16)}));
const accordions = data.map(tpl_collapse).join("");
const EL_container = document.querySelector("#container");

EL_container.innerHTML = accordions;
EL_container.addEventListener("click", (ev) => {
  if (ev.target.closest(".Collapse-head")) {
    ev.target.closest(".Collapse").classList.toggle("expanded");
  }
})
.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}
<div id="container"></div>

Example 2

Dynamically loading N collapsible elements:

If you have a good eye you might have noticed that the above took "only" a couple of seconds (on a relatively good desktop machine).
That's also unacceptable. So let's try to use a dynamic approach of fetching and creating elements!

One way is to assign to the most bottom element an Intersection Observer API. Once the page is scrolled and/or that element enters in viewport, simply load the next page (100 items-set, in this example).

You might use AJAX here, but I'll use pure code generation for this purpose. Anyways here's a proof of concept.

Notice the immediate rendering (no more wait time):

let page = 1;             // Current page
const itemsPerPage = 100; // Items per page

const inViewport = (entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting && +entry.target.dataset.page === page) {
      page += 1;   // Increment page!
      fetchPage(); // Fetch next page
    }
  });
};
const observer = new IntersectionObserver(inViewport);

const EL_container = document.querySelector("#container");
const NewEL = (tag, prop) => Object.assign(document.createElement(tag), prop);

const EL_item = (item) => {
  const EL = NewEL("div", {
    className: "Collapse",
    innerHTML: `<div class="Collapse-head">${item.index}. Character: &#x${item.hex};</div>
      <div class="Collapse-body">
        Hex: <code>&amp#x${item.hex};</code><br>
        Num: <code>&amp#${item.index};</code><br>
        CSS/JS: <code>\\${item.hex}</code><br>
      </div>`
  });
  EL.dataset.page = page; // Associate page to data-page attribute
  // Observe only last element
  if (!((item.index + 1) % itemsPerPage)) observer.observe(EL);
  
  return EL;
}

// Just to programmatically emulate an AJAX call of N items in a page-range
const fetchPage = () => {
  const DF_items = [...Array(itemsPerPage).keys()].reduce((DF, index) => {
    const i = (page - 1) * itemsPerPage + index;
    DF.append(EL_item({index: i,hex: i.toString(16), page}));
    return DF;
  }, new DocumentFragment());
  EL_container.append(DF_items);
};

EL_container.addEventListener("click", (ev) => {
  if (!ev.target.closest(".Collapse-head")) return;
  ev.target.closest(".Collapse").classList.toggle("expanded");
});

// INIT!
fetchPage();
.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}
<div id="container"></div>

Example 3

Pagination as the most mon way to tackle this issue

let page = 1;            // Current page
const itemsPerPage = 50; // Items per page

const EL_container = document.querySelector("#container");
const EL_prev = document.querySelector("#prev");
const EL_next = document.querySelector("#next");
const EL_goto = document.querySelector("#goto");

const NewEL = (tag, prop) => Object.assign(document.createElement(tag), prop);

const EL_item = (item) => {
  return NewEL("div", {
    className: "Collapse",
    innerHTML: `<div class="Collapse-head">${item.index}. Character: &#x${item.hex};</div>
      <div class="Collapse-body">
        Hex: <code>&amp#x${item.hex};</code><br>
        Num: <code>&amp#${item.index};</code><br>
        CSS/JS: <code>\\${item.hex}</code><br>
      </div>`
  });
}

const fetchPage = () => {
  page = Math.abs(page) || 1; // Fix for negative or 0 page number
  
  const DF_items = [...Array(itemsPerPage).keys()].reduce((DF, index) => {
    const i = (page - 1) * itemsPerPage + index;
    DF.append(EL_item({index: i, hex: i.toString(16), page }));
    return DF;
  }, new DocumentFragment());
  EL_container.innerHTML = "";
  EL_container.append(DF_items);
  EL_goto.value = page;
};

EL_container.addEventListener("click", (ev) => {
  if (!ev.target.closest(".Collapse-head")) return;
  ev.target.closest(".Collapse").classList.toggle("expanded");
});

EL_goto.addEventListener("input", () => {
  page = parseInt(EL_goto.value, 10);
  if (page) fetchPage();
});

EL_prev.addEventListener("click", () => {
  page -= 1;
  fetchPage();
});

EL_next.addEventListener("click", () => {
  page += 1;
  fetchPage();
});

// INIT!
fetchPage();
#container {
  height: calc(100vh - 40px);
  overflow: auto;
}

.Collapse {
  border: 1px solid #eee;
}

.Collapse-head {
  cursor: pointer;
  padding: 5px 10px;
}

.Collapse-body {
  display: none;
  background: #eee;
  padding: 5px 10px;
}

.Collapse.expanded .Collapse-head {
  background: #0bf;
}

.Collapse.expanded .Collapse-body {
  display: block;
}

#goto {
  text-align:center;
  width: 100px;
}
<button id="prev" type="button">PREV</button>
<input id="goto" type="number" value="1" min="1">
<button id="next" type="button">NEXT</button>
<div id="container"></div>

本文标签: