如何防止Javascript执行因大量HTML导致页面无响应?
我有一个为用户生成 HTML 报告的程序。这些报告有一些用 jQuery 和 Bootstrap 制作的组件。它主要在某些标签上创建下拉组件,如下所示:
$(document).ready(function () {
$('.my-dropdown').each(function () {
// ...
$(this).collapse("hide");
// ...
}
}
问题是其中一些 HTML 报告有大量下拉列表。有时,一份报告可能有超过 5,000 个下拉菜单,并导致整个页面在一分钟内没有响应。我已经验证删除上面的代码块修复了页面无响应(但失去了下拉切换功能)。
对于页面中的下拉列表数量,无能为力。用户更喜欢等待几分钟让页面加载,因为在单个页面上找到他们需要的东西比在几个不同的页面上查看他们需要的东西更容易。
既然报表不能拆分成不同的页面,那么有没有办法编写 JavaScript,使浏览器在 JavaScript 创建组件时不会挂起?使用deferorawait不起作用,因为它本质上不是正在执行的大量 Javascript,而是大量 HTML 导致大量 JavaScript 执行。
在诸如 React 之类的框架或诸如 Web Components 之类的更新标准中是否有任何东西可以帮助解决这个问题?我不指望浏览器能够在一秒钟内处理几千个组件,但如果有办法将无响应时间缩短到 15 秒左右,那对用户来说会好得多。
回答
只是作为答案:
首先,这里的主要问题是默认情况下您的所有元素都是可见的,而不是您通过 JavaScript 在数千个元素上调用已准备好的 DOM 上的“折叠”。浏览器会逐项关闭并重新计算元素流、重新绘制和渲染。
没有这样的框架可以帮助你——如果你做错了。
相反,默认情况下通过 CSS 隐藏所有元素,而不是通过 JS - 并且仅在单击(或其他)时打开目标元素。
其次,通过 AJAX延迟加载 DOM 元素- 取决于滚动,或者建议的分页也是一个了不起的选择。
现代 Web 编译器、框架,如 Svelte、Vue、React 可以帮助您跟踪当前数据的状态,但原理是一样的,框架或没有框架,在滚动时 AJAX 会触发更多的 HTTP 请求数据,该框架只会在数据更新时帮助毫不费力地更新视图。而PS,15秒甚至对于一个商业定制的应用方式来说也是太多时间了。
因此,回顾 5000 个元素对于现代浏览器来说应该不是主要问题。只是颠倒你的逻辑。默认使用 CSS 隐藏,仅在注册用户操作时使用折叠展开。
示例 1
显示如果操作正确,即使5000 个可折叠元素或超过 80000 个 DOM 元素对浏览器也没有问题:
const tpl_collapse = (item) => `
<div>
<div>${item.i}. Character: &#x${item.hex};</div>
<div>
Hex: <code>&#x${item.hex};</code><br>
Num: <code>&#${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></div>
<div></div>
示例 2
动态加载N 个可折叠元素:
如果您有一双慧眼,您可能已经注意到上述操作“仅”花费了几秒钟(在相对较好的台式机上)。
这也是不能接受的。因此,让我们尝试使用获取和创建元素的动态方法!
一种方法是将Intersection Observer API分配给最底部的元素。一旦页面滚动和/或该元素进入视口,只需加载下一页(在本例中为 100 个项目集)。
您可以在这里使用 AJAX,但我将为此使用纯代码生成。无论如何,这是一个概念证明。
注意立即渲染(没有更多的等待时间):
示例 3
分页是解决此问题的最常用方法
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>${item.index}. Character: &#x${item.hex};</div>
<div>
Hex: <code>&#x${item.hex};</code><br>
Num: <code>&#${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();
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>${item.index}. Character: &#x${item.hex};</div>
<div>
Hex: <code>&#x${item.hex};</code><br>
Num: <code>&#${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 type="button">PREV</button>
<input type="number" value="1" min="1">
<button type="button">NEXT</button>
<div></div>