DOM 操作与优化

中等 🟡浏览器原理
4 个标签
预计阅读时间:23 分钟
浏览器原理DOMDOM 操作性能优化

DOM 操作与优化

DOM (Document Object Model) 是浏览器对 HTML 文档的结构化表示,DOM 操作是前端开发中最常见的操作之一,也是性能瓶颈的主要来源。

🌳 DOM 树结构

DOM 节点类型:

Element:元素节点,表示HTML或XML文档中的元素,如div、span、p等,是DOM树中最常见的节点类型,可以通过document.createElement创建
Text:文本节点,表示元素或属性中的文本内容,是DOM树的叶子节点,可以通过document.createTextNode创建
Comment:注释节点,表示HTML或XML文档中的注释,不会显示在页面上,但可以通过DOM API访问和操作
Document:文档节点,表示整个HTML或XML文档,是DOM树的根节点,可以通过document对象访问,提供创建其他节点的方法
DocumentFragment:文档片段节点,轻量级的文档对象,用于在内存中存储节点,可以一次性插入到文档中,减少DOM操作次数

DOM 树遍历:

遍历方法:childNodes, children, firstChild, lastChild, nextSibling, previousSibling:这些方法用于遍历DOM树,childNodes返回所有子节点,children只返回元素节点,其他方法用于获取相邻节点
遍历性能:避免深度遍历:深度遍历大型DOM树会影响性能,应尽量减少遍历深度和次数,使用更具体的选择器或缓存节点引用优化性能
遍历优化:缓存节点引用:在循环中频繁访问DOM节点时,应将节点引用缓存到变量中,避免重复查询DOM树,提高性能

DOM 操作 API:

创建节点:createElement, createTextNode, createDocumentFragment:createElement创建元素节点,createTextNode创建文本节点,createDocumentFragment创建文档片段,用于批量操作DOM
添加节点:appendChild, insertBefore:appendChild将节点添加到父节点的子节点列表末尾,insertBefore将节点插入到指定节点之前,是添加节点的两种主要方法
删除节点:removeChild:removeChild从DOM树中删除指定节点,返回被删除的节点,删除前应确保节点存在,避免报错
修改节点:innerHTML, textContent:innerHTML设置或获取元素的HTML内容,textContent设置或获取元素的文本内容,textContent性能更好且更安全

⚡ DOM 操作性能

性能瓶颈:

DOM 操作是昂贵的:DOM操作涉及浏览器的重排和重绘,每次操作都需要浏览器重新计算布局和绘制页面,是前端性能的主要瓶颈
每次操作都可能触发重排和重绘:修改元素的位置、大小、样式等都会触发浏览器的重排和重绘,频繁操作会导致性能下降
频繁操作会导致性能下降:在循环中频繁操作DOM会导致大量重排和重绘,应批量处理DOM操作,减少操作次数
影响用户体验:DOM操作性能问题会导致页面卡顿、响应缓慢,影响用户体验,应优化DOM操作提高页面性能

重排与重绘:

重排 (Reflow):计算元素位置和大小:当元素的几何属性(位置、大小)发生变化时,浏览器需要重新计算元素的位置和大小,这个过程称为重排,是性能消耗较大的操作
重绘 (Repaint):绘制元素视觉效果:当元素的视觉属性(颜色、背景、边框)发生变化但不影响布局时,浏览器需要重新绘制元素的视觉效果,这个过程称为重绘
重排会导致重绘:重排必然导致重绘,因为元素的位置和大小变化后需要重新绘制,但重绘不一定导致重排
重绘不一定导致重排:只改变元素的颜色、背景等视觉属性不会影响布局,只触发重绘不触发重排,性能消耗小于重排

操作成本:

读取布局属性:触发重排:读取offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、scrollLeft、clientTop、clientLeft、clientWidth、clientHeight等属性会强制浏览器同步计算布局,触发重排
修改样式:可能触发重排或重绘:修改元素的样式属性,如width、height、margin、padding等会触发重排,修改color、background等只会触发重绘
添加/删除元素:触发重排:添加或删除DOM元素会改变DOM树结构,导致浏览器重新计算布局,触发重排
改变元素大小/位置:触发重排:改变元素的width、height、margin、padding、position等属性会影响布局,触发重排

🚀 DOM 操作优化策略

批量操作:

使用 DocumentFragment:DocumentFragment是轻量级的文档对象,可以在内存中构建DOM片段,然后一次性插入到文档中,减少DOM操作次数,提高性能
批量修改样式:通过修改元素的class或使用cssText一次性修改多个样式属性,避免多次触发重排,减少性能消耗
批量添加/删除元素:在内存中构建完整的DOM结构,然后一次性插入到文档中,避免多次操作DOM导致的多次重排
减少操作次数:将多个DOM操作合并为一次操作,减少重排和重绘次数,提高页面性能

缓存 DOM 引用:

缓存频繁访问的节点:对于需要频繁访问的DOM节点,应将其引用存储在变量中,避免重复查询DOM树,提高性能
避免重复查询:每次使用document.getElementById、document.querySelector等方法查询DOM都会遍历DOM树,应缓存查询结果
使用变量存储节点引用:将DOM节点引用存储在变量中,后续操作直接使用变量,避免重复查询DOM,提高代码执行效率
减少 DOM 遍历:缓存节点引用可以减少DOM遍历次数,特别是在循环中操作DOM时,应先缓存节点引用再进行操作

避免频繁读取布局属性:

缓存布局属性值:将offsetTop、offsetLeft、offsetWidth、offsetHeight等布局属性值缓存到变量中,避免重复读取触发重排
避免在循环中读取:在循环中频繁读取布局属性会导致大量重排,应先读取并缓存属性值,然后在循环中使用缓存值
一次性读取多个属性:如果需要读取多个布局属性,应一次性读取所有属性,避免多次读取触发多次重排
减少重排触发:合理组织代码,避免在修改样式后立即读取布局属性,减少不必要的重排

使用事件委托:

减少事件监听器数量:事件委托将事件监听器添加到父元素,而不是每个子元素,大幅减少事件监听器数量,提高性能
提高性能:事件委托利用事件冒泡机制,只需一个事件监听器就能处理多个子元素的事件,减少内存占用和事件绑定开销
简化代码:事件委托避免了为每个子元素单独绑定事件监听器的繁琐代码,代码更简洁,更易维护
支持动态添加的元素:事件委托可以处理动态添加的子元素事件,无需为新增元素重新绑定事件监听器

💻 代码示例:DOM 操作优化

javascriptCode
// 批量DOM操作优化
function optimizedDOMOperations() {
  // ❌ 不好的做法:多次操作DOM
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    document.body.appendChild(div); // 每次都触发重排
  }

  // ✅ 好的做法:使用DocumentFragment
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    fragment.appendChild(div);
  }
  document.body.appendChild(fragment); // 只触发一次重排
}

// 缓存DOM引用
function cacheDOMReferences() {
  // ❌ 不好的做法:重复查询DOM
  function updateElement() {
    document.getElementById('myElement').style.color = 'red';
    document.getElementById('myElement').style.fontSize = '16px';
    document.getElementById('myElement').style.backgroundColor = 'blue';
  }

  // ✅ 好的做法:缓存DOM引用
  const element = document.getElementById('myElement');
  function updateElementOptimized() {
    element.style.color = 'red';
    element.style.fontSize = '16px';
    element.style.backgroundColor = 'blue';
  }
}

// 事件委托示例
function eventDelegation() {
  // ❌ 不好的做法:为每个按钮添加事件监听器
  const buttons = document.querySelectorAll('.button');
  buttons.forEach(button => {
    button.addEventListener('click', function() {
      console.log('按钮被点击:', this.textContent);
    });
  });

  // ✅ 好的做法:使用事件委托
  document.addEventListener('click', function(event) {
    if (event.target.classList.contains('button')) {
      console.log('按钮被点击:', event.target.textContent);
    }
  });
}

// 批量样式修改
function batchStyleUpdates() {
  const element = document.getElementById('myElement');
  
  // ❌ 不好的做法:多次修改样式
  element.style.width = '100px';
  element.style.height = '100px';
  element.style.backgroundColor = 'red';
  element.style.color = 'white';

  // ✅ 好的做法:批量修改样式
  element.style.cssText = 'width: 100px; height: 100px; background-color: red; color: white;';
  
  // 或者使用CSS类
  element.className = 'highlighted';
}

🔄 虚拟 DOM

虚拟 DOM 概念:

轻量级的 DOM 描述对象:虚拟DOM是用JavaScript对象描述真实DOM结构的轻量级表示,包含元素类型、属性、子节点等信息,不包含DOM的重量级特性
减少直接 DOM 操作:虚拟DOM通过比较新旧虚拟DOM树的差异,批量更新真实DOM,减少直接DOM操作次数,提高性能
提高渲染性能:虚拟DOM将多次DOM更新合并为一次,减少重排和重绘次数,提高渲染性能,特别是在频繁更新场景下
框架如 React、Vue 使用:React、Vue等现代前端框架都使用虚拟DOM技术,通过虚拟DOM实现高效的视图更新

虚拟 DOM 工作原理:

创建虚拟 DOM 树:根据组件的render方法或模板创建虚拟DOM树,虚拟DOM树是真实DOM的JavaScript对象表示
计算差异 (Diff):比较新旧虚拟DOM树的差异,找出需要更新的节点,Diff算法通过同层比较、key属性优化等方式提高效率
批量更新真实 DOM:将所有差异一次性应用到真实DOM上,减少DOM操作次数,避免频繁重排和重绘,提高性能
减少重排和重绘:通过批量更新DOM,虚拟DOM技术可以最小化重排和重绘次数,提高页面渲染性能

Diff 算法:

同层比较:Diff算法只比较同一层级的节点,不跨层级比较,大大减少了比较次数,提高了算法效率,时间复杂度从O(n^3)降低到O(n)
标签不同直接替换:如果新旧节点的标签类型不同,直接删除旧节点并创建新节点,不再比较子节点,简化了比较过程
标签相同比较属性和子节点:如果新旧节点的标签类型相同,比较节点的属性和子节点,更新变化的属性,递归比较子节点
key 属性优化:通过key属性标识节点,在列表渲染时可以准确识别哪些节点发生了变化,避免不必要的DOM操作,提高更新效率

🎯 DOM 事件

事件流:

捕获阶段:从根节点到目标节点:事件从document对象开始,沿着DOM树向下传播到目标元素,依次触发各元素的捕获事件监听器
目标阶段:在目标节点上触发:事件到达目标元素后,在目标元素上触发事件,如果事件类型没有冒泡,事件到此结束
冒泡阶段:从目标节点到根节点:事件从目标元素开始,沿着DOM树向上传播到document对象,依次触发各元素的冒泡事件监听器

事件委托:

将事件监听器添加到父元素:事件委托是一种事件处理模式,将事件监听器添加到父元素而不是每个子元素,利用事件冒泡机制处理子元素事件
利用事件冒泡:当子元素触发事件时,事件会冒泡到父元素,父元素的事件监听器可以通过event.target判断是哪个子元素触发了事件
减少事件监听器数量:事件委托可以大幅减少事件监听器数量,特别是对于大量子元素的场景,提高性能和内存效率
支持动态元素:事件委托可以处理动态添加的子元素事件,无需为新增元素重新绑定事件监听器,简化了代码

事件处理优化:

避免在事件处理函数中执行耗时操作:事件处理函数应尽快执行完毕,避免阻塞主线程,耗时操作应使用Web Workers或requestIdleCallback
使用 passive 事件监听器:对于touch、wheel等事件,使用passive: true选项声明不会调用preventDefault(),提高滚动性能
及时移除事件监听器:在组件卸载或不需要时及时移除事件监听器,避免内存泄漏和不必要的事件处理
避免内存泄漏:事件监听器会持有对元素的引用,如果不及时移除,会导致元素无法被垃圾回收,造成内存泄漏

⚡ DOM API 优化

选择器 API:

querySelector:返回第一个匹配元素:querySelector使用CSS选择器查询DOM,返回第一个匹配的元素,如果找不到则返回null,是现代DOM查询的推荐方法
querySelectorAll:返回所有匹配元素:querySelectorAll返回所有匹配元素的NodeList,不是实时的,适合一次性查询多个元素
getElementById:通过 ID 获取元素:getElementById通过元素ID获取元素,是最快的DOM查询方法,性能优于querySelector
getElementsByClassName:通过类名获取元素:getElementsByClassName返回具有指定类名的所有元素的HTMLCollection,是实时的,性能优于querySelectorAll

性能比较:

getElementById > querySelector > getElementsByClassName > querySelectorAll:这是选择器API的性能排序,getElementById是最快的,因为它直接通过ID查找元素,不需要遍历DOM树,querySelector使用CSS选择器查询,性能取决于选择器的复杂度,getElementsByClassName返回HTMLCollection是实时的,querySelectorAll返回NodeList不是实时的
选择器越具体性能越好:具体的选择器如ID选择器(#element)和类选择器(.class)比通配符选择器(*)性能好很多,因为浏览器可以快速定位元素,而不需要遍历所有元素,选择器匹配是从右向左进行的
避免复杂选择器:复杂的选择器如嵌套选择器、属性选择器会增加匹配时间,应该尽量简化选择器,使用更直接的方式选择元素,或者预先给元素添加特定的class
缓存选择结果:将选择器结果缓存到变量中,避免重复查询DOM树,特别是在循环中操作DOM时,应该先缓存节点引用再进行操作,提高代码执行效率

操作 API:

innerHTML vs createElement:innerHTML用于设置或获取元素的HTML内容,优点是简洁方便,可以一次性插入大量HTML,缺点是可能存在XSS安全风险,且性能在大量DOM操作时较差;createElement逐个创建元素,优点是更安全(不会解析恶意HTML)、性能更好(可以批量操作),缺点是代码更繁琐,需要逐个创建和追加元素
textContent vs innerText:textContent获取或设置元素的所有文本内容,包括子元素的文本,性能更好因为不会触发回流(不计算布局);innerText获取或设置元素的可见文本内容,会受到CSS样式影响(隐藏的文本不会获取),会触发回流以计算布局,因此性能较差,在不需要考虑CSS样式影响时应优先使用textContent
appendChild vs insertBefore:appendChild将节点添加到父节点的子节点列表末尾,是最常用的添加节点方法;insertBefore将节点插入到指定节点之前,可以用于在列表开头插入元素,实现类似unshift的功能,如果要在末尾插入,insertBefore配合null参数等同于appendChild
removeChild vs innerHTML:removeChild从DOM树中删除指定节点,返回被删除的节点,可以精确删除单个元素;innerHTML设置为空字符串可以删除元素的所有子节点,但会销毁所有子节点的事件监听器,可能导致内存泄漏,如果只需要删除子元素且不需要保留事件监听器,innerHTML = ''更简洁

最佳实践

DOM 操作:

减少 DOM 操作次数:DOM操作是前端性能的主要瓶颈之一,每次DOM操作都可能触发浏览器的重排和重绘,应该尽量减少DOM操作次数,可以使用DocumentFragment批量添加元素、使用虚拟DOM技术如React/Vue框架、将多次操作合并为一次等方式来优化
批量处理 DOM 更新:在需要更新多个DOM元素时,应该先将所有更新准备好,然后一次性应用到DOM上,避免多次触发重排和重绘,例如使用requestAnimationFrame在下一帧统一更新,或者使用文档片段(DocumentFragment)在内存中构建完整的DOM结构后一次性插入
缓存 DOM 引用:对于需要频繁访问的DOM节点,应该将其引用缓存到变量中,避免重复查询DOM树,特别是循环中需要多次访问同一个元素时,缓存引用可以显著提高性能,常见的缓存模式是在初始化时获取一次引用,后续直接使用变量
使用 DocumentFragment:DocumentFragment是轻量级的文档对象,相当于一个容器,可以在其中构建完整的DOM结构,然后一次性将所有子节点添加到实际的DOM树中,只会触发一次重排和重绘,比逐个添加元素性能好很多,是批量操作DOM的最佳实践

样式操作:

批量修改样式:当需要修改多个样式属性时,应该尽量一次性修改完成,可以通过修改元素的className来应用多个样式,而不是逐个设置style属性,这样可以让浏览器只触发一次重排和重绘,另外使用cssText属性也可以一次性设置多个样式,但会覆盖原有的内联样式,需要注意
使用 CSS 类代替内联样式:将需要应用的样式预先定义在CSS类中,然后通过JavaScript切换className来应用样式,这样做的好处是样式与JavaScript代码分离,代码更清晰,维护更方便,而且浏览器的CSS解析和渲染引擎对类选择器的处理更高效,内联样式每次修改都会触发重排
避免频繁修改样式:样式修改是触发重排和重绘的主要原因,应该避免在循环中频繁修改样式,或者先将元素隐藏(display:none)修改样式后再显示,或者使用CSS动画代替JavaScript动画,或者使用transform和opacity属性,因为这两个属性的变化不会触发布局变化,只会触发合成
使用 CSS transitions 代替 JavaScript 动画:CSS transitions和animations利用GPU加速,在主线程之外执行,不会阻塞JavaScript代码的执行,流畅度更好,性能更优,而JavaScript动画需要每帧计算位置并修改DOM,消耗CPU资源,还可能造成丢帧,应该优先使用CSS实现简单动画

事件处理:

使用事件委托:事件委托是一种高效的事件处理模式,将事件监听器添加到父元素而不是每个子元素,利用事件冒泡机制处理子元素的事件,这样可以大幅减少事件监听器的数量,降低内存占用,特别是在处理大量相似元素的场景(如列表)时效果显著,事件委托还可以处理动态添加的元素,无需为新元素单独绑定事件
合理使用事件监听器:在添加事件监听器时应该考虑是否真正需要,给不需要的元素添加事件监听器会浪费内存和CPU资源,另外要及时移除不再需要的事件监听器,避免内存泄漏,Vue中使用@语法、React中使用addEventListener时都要注意在组件卸载时移除监听器
及时移除事件监听器:事件监听器会持有对目标元素的引用,如果不及时移除,当元素被删除或组件卸载时,监听器仍然存在,会阻止垃圾回收导致内存泄漏,在组件的beforeDestroy或useEffect的清理函数中移除监听器,特别是在使用闭包时要注意避免内存泄漏
避免在事件处理函数中执行耗时操作:事件处理函数应该尽快执行完毕,将耗时操作放到异步队列中执行,或者使用Web Workers在后台线程处理,避免阻塞主线程导致页面卡顿,可以使用requestIdleCallback在浏览器空闲时执行非关键任务,或者将计算密集型任务拆分到多个帧中执行

性能监控:

使用 Chrome DevTools Performance 面板:Chrome DevTools的Performance面板可以录制和分析页面的性能表现,包括FPS、CPU使用率、网络请求、页面加载时间等,通过录制页面操作然后分析时间线,可以找出性能瓶颈,如长任务、频繁重排重绘、内存泄漏等,建议在开发阶段定期使用Performance面板检测性能问题
监控 DOM 操作性能:可以通过Performance API的mark和measure方法在代码中埋点,测量特定操作的执行时间,监控DOM操作的频率和耗时,找出性能热点进行优化,也可以使用User Timing API将性能数据发送到分析系统,进行持续的性能监控
识别性能瓶颈:通过Performance面板的时间线可以识别各种性能瓶颈,如长任务(超过50ms的任务)、强制同步布局(forced synchronous layout)、频繁的垃圾回收等,针对不同的瓶颈采取相应的优化措施,如拆分长任务、优化CSS选择器、减少内存分配等
持续优化:性能优化是一个持续的过程,应该建立性能指标监控体系,设置性能预算,定期检测性能变化,及时发现和解决性能退化问题,可以使用Lighthouse、WebPageTest等工具进行自动化性能测试

工具与资源

开发工具:

Chrome DevTools
Firefox Developer Tools
Edge DevTools
Safari Developer Tools

性能分析工具:

Lighthouse
WebPageTest
Chrome DevTools Performance
React DevTools

学习资源:

MDN DOM 文档
Google Web Fundamentals
JavaScript 高级程序设计
前端性能优化指南

案例分析

大型应用 DOM 优化:

Google Docs 的 DOM 优化
Facebook 的 DOM 优化
阿里巴巴的 DOM 优化
腾讯的 DOM 优化

实施效果:

页面渲染速度提升
用户交互响应时间减少
内存使用优化
整体性能改善