包阅导读总结
1. 关键词:DOM 操作、JavaScript、性能优化、前端、内存管理
2. 总结:本文介绍了使用现代 JavaScript 进行高效 DOM 操作以提升 Web 应用性能的方法,包括避免内存过度使用的实践、常用 DOM API、手动操作 DOM 的原因及技巧,还提及了代码剖析与调试的方法和优化的关键要点。
3. 主要内容:
– 前言
– 介绍使用现代纯 JavaScript 技术进行高效 DOM 操作以提高性能
– DOM 概述
– 解释 DOM 概念及常用 API
– 指出框架底层使用相关 API
– 手动操作 DOM 的原因
– 性能问题,框架可能导致卡顿
– 高效 DOM 操作技巧
– 倾向隐藏/显示而非创建新元素
– 选择 textContent 而非 innerText
– 使用 insertAdjacentHTML 等方法
– 使用模板、DocumentFragment 等
– 管理删除节点时的引用
– 清理事件监听器
– 代码剖析与调试
– 内存分析
– JavaScript 执行时间分析
– 优化关键要点
– 总结高效 DOM 操作的要点及决策原则
思维导图:
文章地址:https://mp.weixin.qq.com/s/R4YC0wp3oOWVJ77ioOa1ig
文章来源:mp.weixin.qq.com
作者:飘飘
发布时间:2024/8/1 0:02
语言:中文
总字数:3758字
预计阅读时间:16分钟
评分:87分
标签:前端开发,JavaScript,DOM操作,性能优化,内存管理
以下为原文内容
本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com
前言
介绍了如何使用现代的纯 JavaScript 技术进行高效的 DOM 操作,以提高 Web 应用程序的性能。今日前端早读课文章由 @飘飘翻译分享。
正文从这开始~~
我将讨论在管理 DOM 更新时避免内存使用过度的最佳实践,以使您的应用程序运行得更快。
DOM:文档对象模型 – 简要概述
当您渲染 HTML 时,在浏览器中显示的已渲染元素的实时视图称为 DOM(文档对象模型)。这就是你在 “元素” 调试器中看到的内容:
它本质上是一棵树,其中每个元素都是一片叶子。有一套完整的 API 专门用于修改这些元素组成的树。
【第3295期】DOM 的层级深度影响性能
以下是一些常用的 DOM API:
-
querySelector()
-
querySelectorAll()
-
createElement()
-
getAttribute()
-
setAttribute()
-
addEventListener()
-
appendChild()
这些方法是与document
相关的,因此您可以像使用const el = document.querySelector("#el");
一样使用它们。它们也适用于所有其他元素,因此如果您有一个元素引用,就可以使用这些方法,它们的功能仅限于该元素。
const nav = document.querySelector("#site-nav");
const navLinks = nav.querySelectorAll("a");
这些方法在浏览器中可用来修改 DOM,但在服务器端 JavaScript(如 Node.js)中无法直接使用这些方法,除非您使用 DOM 模拟器如 js-dom。
作为一种行业,我们已经将大部分的直接渲染工作移交给了框架。所有 JavaScript 框架(如 React、Angular、Vue、Svelte 等)都在底层使用这些 API。虽然我承认框架的生产力优势往往大于手动 DOM 操作可能带来的性能提升,但我还是想在本文中揭开框架背后的运作机制。
为什么要亲自操作 DOM 呢?
主要原因是性能问题。框架可能会添加不必要的数据结构和重新渲染操作,导致在许多现代 Web 应用中常见的卡顿 / 冻结行为。这是因为垃圾回收器不得不处理所有这些代码,导致其超负荷运行。
【早阅】 如何掌握JavaScript性能优化
缺点是需要编写更多的代码来手动处理 DOM 操作。这可能会变得很复杂,因此使用 DOM 框架和抽象层来处理 DOM 操作,而不是手动操作 DOM,可以提供更好的开发人员体验。无论如何,有些情况下可能需要额外的性能,这就是本指南的目的所在。
VS Code 是基于手动 DOM 操作构建的。
Visual Studio Code 就是其中之一。VS Code 是用 vanilla JavaScript 编写的,”目的是尽可能接近 DOM”。像 VS Code 这样的大型项目需要对性能有严格的控制。由于插件生态系统提供了大部分功能,因此核心必须尽可能地轻量级,这也是它得到广泛采用的原因。
微软 Edge 也出于同样的原因放弃了 React。
如果你发现自己需要进行直接的 DOM 操作(比使用框架的低级编程),那么希望本文能对你有所帮助!
更高效 DOM 操作技巧
更倾向于隐藏 / 显示而不是创建新的元素。
通过隐藏和显示元素来保持 DOM 不变,而不是用 JavaScript 来销毁和创建元素,这始终是性能更高的选择。
服务器会渲染你的元素,并使用el.classList.add('show')
或el.style.display = 'block'
这样的类(和适当的 CSS 规则集)来隐藏 / 显示它,而不是使用 JavaScript 动态创建和插入元素。由于缺少垃圾回收调用和复杂的客户端逻辑,这种主要由静态 DOM 构成的页面性能会更好。
如果可以避免,不要在客户端动态创建 DOM 节点。
但请记住辅助技术。如果你想要一个既在视觉上不可见又对辅助技术不可见的元素,可以使用display: none;
。但如果你想隐藏一个元素并让它对辅助技术不可见,请考虑其他隐藏内容的方法。
选择使用textContent
而不是innerText
来显示元素的内容
innerText 方法很酷,因为它知道某个元素当前的样式。它知道某个元素是否隐藏,只有在确实有内容显示时才获取文本。它的问题在于,检查样式的过程会触发布局重排,而且速度较慢。
使用element.textContent
来读取内容的速度比使用element.innerText
要快得多,因此在可能的情况下,尽量使用 textContent 标记来读取元素的内容。
使用 insertAdjacentHTML 而不是 innerHTML
insertAdjacentHTML
方法比innerHTML
方法快得多,因为它不必在插入之前先销毁 DOM。该方法在放置新 HTML 的位置上具有灵活性,例如:
el.insertAdjacentHTML("afterbegin", html);
el.insertAdjacentHTML("beforeend", html);
最快的方法是使用 insertAdjacentElement 或 appendChild
方法 1:使用<template>
标签创建 HTML 模板,并使用 appendChild 标签插入新的 HTML 内容。
这些是最快的添加完全构建的 DOM 元素的方法。一种常用的方法是使用<template>
标签创建 HTML 模板来创建元素,然后使用 insertAdjacentElement 或 appendChild 方法将它们插入 DOM 中。
<template id="card_template">
<article class="card">
<h3></h3>
<div class="card__body">
<div class='card__body__image'></div>
<section class='card__body__content'>
</section>
</div>
</article>
</template>
function createCardElement(title, body) {
const template = document.getElementById('card_template');
const element = template.content.cloneNode(true).firstElementChild;
const [cardTitle] = element.getElementsByTagName("h3");
const [cardBody] = element.getElementsByTagName("section");
[cardTitle.textContent, cardBody.textContent] = [title, body];
return element;
}
container.appendChild(createCardElement(
"Frontend System Design: Fundamentals",
"This is a random content"
))
第二种方法:使用 createDocumentFragment 与 appendChild 进行批量插入
DocumentFragment 是一个轻量级的 “空” 文档对象,可以容纳 DOM 节点。它不属于活跃的 DOM 树,因此非常适合准备多个元素以进行插入。
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
fragment.appendChild(li);
}
document.getElementById('myList').appendChild(fragment);
这种方法一次性插入所有元素,而不是逐个插入,从而最大限度地减少了回流和重绘。
当节点被删除时,管理引用
在删除 DOM 节点时,你不希望有引用残留下来,阻止垃圾回收器清理相关的数据。我们可以使用 WeakMap 和 WeakRef 来避免泄漏引用。
使用WeakMap
元素为 DOM 节点关联数据。
你可以使用WeakMap
将数据与 DOM 节点关联起来。这样一来,如果以后删除了该 DOM 节点,对数据的引用也将永久消失。
let DOMdata = { 'logo': 'Frontend Masters' };
let DOMmap = new WeakMap();
let el = document.querySelector(".FmLogo");
DOMmap.set(el, DOMdata);
console.log(DOMmap.get(el)); // { 'logo': 'Frontend Masters' }
el.remove(); // DOMdata is able to be garbage collected
使用弱引用可以确保如果从 DOM 树中删除一个元素,对数据的引用也会随之消失。
使用WeakRef
进行垃圾回收后进行清理
在下面的例子中,我们创建了一个指向 DOM 节点的 WeakRef :
class Counter {
constructor(element) {
// Remember a weak reference to the DOM element
this.ref = new WeakRef(element);
this.start();
}
start() {
if (this.timer) {
return;
}
this.count = 0;
const tick = () => {
// get the element from the weak reference, if it still exists
const element = this.ref.deref();
if (element) {
console.log("Element is still in memory, updating count.")
element.textContent = `Counter: ${++this.count}`;
} else {
// The element doesn't exist anymore
console.log("Garabage Collector ran and element is GONE – clean up interval");
this.stop();
this.ref = null;
}
};
tick();
this.timer = setInterval(tick, 1000);
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = 0;
}
}
}
const counter = new Counter(document.getElementById("counter"));
setTimeout(() => {
document.getElementById("counter").remove();
}, 5000);
在删除节点之后,您可以查看控制台以查看实际的垃圾回收何时发生,或者您可以使用开发工具中的 “性能” 选项卡手动触发垃圾回收:
然后你可以确保所有引用都被删除,计时器也被清理了。
注意:尽量不要过度使用 WeakRef-this 的魔法,因为它是有代价的。如果您可以明确管理引用,那么性能会更好。
清理事件监听器
手动删除带有 “removeEventListener” 的事件。
function handleClick() {
console.log("Button was clicked!");
el.removeEventListener("click", handleClick);
}
// Add an event listener to the button
const el = document.querySelector("#button");
el.addEventListener("click", handleClick);
使用 once 参数来标记一次性事件。
使用 “once” 参数也可以实现与上述相同的功能:
el.addEventListener('click', handleClick, {
once: true
});
在addEventListener
中添加一个布尔类型的参数,指示在添加该监听器之后,最多只应执行一次该监听器。在调用该监听器时,它将自动被移除。
使用事件委托来绑定更少的事件
如果您在高度动态的组件中频繁地创建和替换节点,那么在构建节点时为每个节点设置相应的事件监听器的成本会更高。
相反,你可以将事件绑定到根元素更靠近的位置。由于事件会沿着 DOM 层级向上冒泡,因此你可以检查event.target
(事件的原始目标)来捕获并响应该事件。
使用matches(selector)
只能匹配当前元素,因此它必须是叶子节点。
const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
// if the element is clicked has class "target-element"
if (event.target.matches('.target-element')) doSomething();
});
在这种情况下,你可能会遇到类似于<div class="target-element"><p>...</p></div>
的元素,这时你需要使用.closest(element)
方法。
const rootEl = document.querySelector("#root");
// Listen for clicks on the entire window
rootEl.addEventListener('click', function (event) {
// if the element is clicked has a parent with "target-element"
if (event.target.closest('.target-element')) doSomething();
});
这种方法可以让您在动态注入元素后不必担心添加和移除监听器的问题。
使用 AbortController 来解除一组事件的绑定
const button = document.getElementById('button');
const controller = new AbortController();
const { signal } = controller;
button.addEventListener(
'click',
() => console.log('clicked!'),
{ signal }
);
// Remove the listener!
controller.abort();
你可以使用AbortController
来删除事件集合。
let controller = new AbortController();
const { signal } = controller;
button.addEventListener('click', () => console.log('clicked!'), { signal });
window.addEventListener('resize', () => console.log('resized!'), { signal });
document.addEventListener('keyup', () => console.log('pressed!'), { signal });
// Remove all listeners at once:
controller.abort();
代码剖析与调试
测量你的 DOM,确保它不要太大。
以下是使用 Chrome DevTools 进行内存分析的简要指南:
-
打开 Chrome 开发者工具
-
前往 “内存” 选项卡
-
选择 “堆快照” 并点击 “获取快照”
-
执行您的 DOM 操作
-
再拍一张 snapshot
-
比较快照来识别内存增长情况
需要关注的关键点:
-
意外保留的 DOM 元素
-
大量未清理的数组或对象
-
内存使用量随时间不断增加(可能存在内存泄露问题)
你还可以使用 “性能” 选项卡来记录内存使用情况随时间的变化:
-
前往 “性能” 选项卡
-
请检查 “内存” 选项
-
点击 “记录”
-
执行您的 DOM 操作
-
停止记录并分析内存图表
这将帮助您可视化内存分配,并在 DOM 操作期间识别潜在的泄漏或不必要的分配。
JavaScript 执行时间分析
除了内存分析外,Chrome DevTools 中的 “性能” 标签对于分析 JavaScript 执行时间也非常有用,这对于优化 DOM 操作代码至关重要。
以下是如何使用它的方法:
最终的进度表将向您展示:
-
JavaScript 执行(黄色)
-
渲染活动(紫色)
-
绘画 (绿色)
寻找:
深潜:
这种分析可以帮助您准确找出 DOM 操作代码可能导致的性能问题所在,从而进行有针对性的优化。
记住,高效的 DOM 操作不仅仅是使用正确的方法,还包括了解何时以及如何与 DOM 进行交互。即使使用了高效的方法,过度的操作仍然可能导致性能问题。
DOM 优化的关键要点
在创建对性能敏感的 Web 应用时,高效的 DOM 操作知识非常重要。虽然现代框架提供了方便和抽象,但理解并应用这些低级技术可以显著提升应用程序的性能,尤其是在要求苛刻的情况下。
回顾一下:
-
在可能的情况下,尽量修改现有元素而不是创建新的元素。
-
使用高效的方法,如
textContent
、insertAdjacentHTML
和appendChild
。 -
谨慎管理引用,利用 WeakMap 和 WeakRef 避免内存泄漏。
-
正确地清理事件监听器,以避免不必要的开销。
-
可以考虑采用事件委托等技术来实现更高效的事件处理。
-
使用工具如
AbortController
来更方便地管理多个事件监听器。 -
使用批量插入功能,并了解虚拟 DOM 等概念,以便制定更广泛的优化策略。
记住,并不是每个项目都需要放弃框架,手动操作 DOM。而是要理解这些原则,以便在何时使用框架和何时在较低层次上优化之间做出明智的决策。内存剖析和性能基准测试等工具可以指导这些决策。
关于本文
译者:@飘飘
作者:@MARC GRABANSKI
原文:https://frontendmasters.com/blog/patterns-for-memory-efficient-dom-manipulation/
这期前端早读课
对你有帮助,帮”赞“一下,
期待下一期,帮”在看” 一下 。