JavaScript性能优化实战(三):DOM操作性能优化

        想象一下,你正在精心布置一个豪华蛋糕(你的网页),每次添加一颗草莓(DOM元素)都要把整个蛋糕从冰箱拿出来、放回去(重排重绘),来来回回几十次,不仅效率低下,蛋糕也可能被弄坏。DOM操作就像布置这个蛋糕,每一次操作都可能触发浏览器的重排(Reflow)和重绘(Repaint),这可是前端性能的"隐形杀手"。

        今天我们就来揭秘DOM操作的5大优化技巧,用生动的案例告诉你如何让页面操作如丝般顺滑,告别卡顿!

1. 批量操作:DocumentFragment的"快递箱"哲学

        频繁的DOM操作就像每次买一件商品都收一次快递——每次都要开门、签收、处理包装,效率极低。DocumentFragment就像一个"虚拟快递箱",可以把所有要添加的DOM元素先放进去,最后一次送达,大大减少操作次数。

问题代码:频繁DOM操作的噩梦

// 糟糕的做法:每次循环都操作DOM
function renderList(items) {const list = document.getElementById('myList');items.forEach(item => {const li = document.createElement('li');li.textContent = item.name;// 每次都触发DOM更新,引发重排list.appendChild(li); });
}// 测试:渲染1000条数据
const largeDataset = Array.from({length: 1000}, (_, i) => ({name: `项目${i}`}));
renderList(largeDataset); // 触发1000次DOM更新!

优化方案:用DocumentFragment批量处理

// 优化做法:批量处理后一次性更新
function renderListOptimized(items) {const list = document.getElementById('myList');// 创建文档片段(虚拟容器)const fragment = document.createDocumentFragment();items.forEach(item => {const li = document.createElement('li');li.textContent = item.name;// 先添加到虚拟容器,不触发DOM更新fragment.appendChild(li);});// 一次性更新DOM,只触发1次重排list.appendChild(fragment);
}// 同样渲染1000条数据,性能提升80%+
renderListOptimized(largeDataset); // 仅触发1次DOM更新!

为什么这么快?
每次DOM操作都会触发浏览器的重排计算(计算元素位置和大小)和重绘(像素渲染)。1000次单独操作会产生1000次重排,而使用DocumentFragment只会产生1次,性能差异呈指数级增长。

2. 缓存DOM查询:别反复"找东西"

        DOM查询就像在杂乱的房间找东西——每次找都要翻箱倒柜(遍历DOM树),如果频繁找同一个东西,最好的办法是找到后放在固定位置(缓存)。

问题代码:重复查询DOM的陷阱

// 糟糕的做法:反复查询同一个DOM元素
function updateUserInfo(user) {// 每次都查询DOM,性能浪费document.getElementById('username').textContent = user.name;document.getElementById('email').textContent = user.email;document.getElementById('age').textContent = user.age;// 循环中重复查询,性能杀手!for (let i = 0; i < 100; i++) {const item = document.querySelector(`.list-item-${i}`);item.classList.add('highlight');}
}

优化方案:缓存查询结果

// 优化做法:缓存DOM查询结果
// 1. 一次性查询并缓存常用元素
const userElements = {name: document.getElementById('username'),email: document.getElementById('email'),age: document.getElementById('age')
};function updateUserInfoOptimized(user) {// 直接使用缓存的DOM引用userElements.name.textContent = user.name;userElements.email.textContent = user.email;userElements.age.textContent = user.age;
}// 2. 循环中优化查询
function highlightItems() {// 先查询父元素(1次查询)const list = document.getElementById('itemList');// 从缓存的父元素中查询子元素(更快)const items = list.querySelectorAll('[class^="list-item-"]');// 直接遍历缓存的集合items.forEach(item => {item.classList.add('highlight');});
}

性能对比

  • 重复查询相同DOM元素:每次查询耗时约10-50ms(视DOM复杂度)
  • 缓存查询结果:后续访问耗时≈0ms,性能提升100倍以上

3. 平滑动画:requestAnimationFrame的"舞蹈节奏"

        想象你在跳舞时,没有音乐节奏(setTimeout),动作会僵硬卡顿;而跟着音乐节拍(requestAnimationFrame)跳舞,动作会流畅自然。浏览器渲染也有自己的"节拍"(通常60fps),跟着这个节奏更新视觉效果才能流畅。

问题代码:定时器动画的卡顿

// 糟糕的做法:用setTimeout做动画
function animateBoxBad() {const box = document.getElementById('animatedBox');let position = 0;function move() {position += 1;box.style.left = `${position}px`;if (position < 500) {// 不匹配浏览器渲染节奏,可能导致卡顿setTimeout(move, 16); // 尝试模拟60fps,但不精准}}move();
}

优化方案:用requestAnimationFrame同步渲染

// 优化做法:使用requestAnimationFrame
function animateBoxOptimized() {const box = document.getElementById('animatedBox');let position = 0;function move(timestamp) {position += 1;box.style.left = `${position}px`;if (position < 500) {// 告诉浏览器:下一帧渲染前调用moverequestAnimationFrame(move);}}// 启动动画requestAnimationFrame(move);
}// 高级用法:控制动画帧率
function animateWithFpsControl() {const box = document.getElementById('animatedBox');let position = 0;const fps = 30; // 目标帧率const interval = 1000 / fps;let lastTime = 0;function move(timestamp) {// 控制帧率if (!lastTime || timestamp - lastTime > interval) {lastTime = timestamp;position += 2; // 每帧移动距离加倍,保持相同速度感box.style.left = `${position}px`;}if (position < 500) {requestAnimationFrame(move);}}requestAnimationFrame(move);
}

为什么更流畅?

  • setTimeout/setInterval:不管浏览器是否准备好渲染,到时就执行,可能导致掉帧
  • requestAnimationFrame:由浏览器调度,在每次重绘前执行,与浏览器渲染节奏完全同步
  • 节能优势:页面隐藏时(如切换标签),动画会自动暂停,节省CPU资源

4. 避免强制同步布局:别让浏览器"手忙脚乱"

        浏览器渲染有自己的流水线:布局(计算几何属性)→ 绘制(填充像素)→ 合成(组合图层)。正常情况下这个流程是异步的,但如果你先读取布局属性(如offsetHeight),再立即修改样式,会强制浏览器同步执行布局计算,造成性能阻塞。

问题代码:强制同步布局的陷阱

// 糟糕的做法:读取布局属性后立即修改
function updateHeightsBad() {const boxes = document.querySelectorAll('.box');boxes.forEach(box => {// 1. 读取布局属性(触发布局计算)const height = box.offsetHeight;// 2. 立即修改样式(强制浏览器同步重新计算布局)box.style.height = `${height + 10}px`;});
}

优化方案:分离读写操作

// 优化做法:先批量读取,再批量修改
function updateHeightsOptimized() {const boxes = document.querySelectorAll('.box');// 1. 第一阶段:批量读取所有必要的布局属性const heights = Array.from(boxes).map(box => box.offsetHeight);// 2. 第二阶段:批量修改样式(此时不会触发布局计算)boxes.forEach((box, index) => {box.style.height = `${heights[index] + 10}px`;});
}// 更复杂场景的优化:使用FastDOM库思想
const fastDOM = {read: (callback) => {// 收集所有读操作const results = [];// 批量执行读操作results.push(callback());return results;},write: (callback) => {// 批量执行写操作callback();}
};// 使用示例
function optimizedUpdate() {const boxes = document.querySelectorAll('.box');const heights = [];// 批量读取fastDOM.read(() => {boxes.forEach(box => {heights.push(box.offsetHeight);});});// 批量写入fastDOM.write(() => {boxes.forEach((box, index) => {box.style.height = `${heights[index] + 10}px`;});});
}

性能差异
在包含100个元素的页面中,强制同步布局可能导致操作耗时增加10-100倍,在低端设备上甚至会造成明显卡顿。

5. 虚拟DOM:用"蓝图"代替直接施工

        直接操作DOM就像直接在装修好的房子里频繁拆改——成本高、效率低。虚拟DOM则像先在电脑上用3D模型设计(虚拟DOM树),规划好所有改动后,再一次性施工(更新真实DOM),大大减少实际操作。

传统DOM操作的痛点

// 直接操作DOM的繁琐与低效
function updateTodoList(todos) {const list = document.getElementById('todoList');list.innerHTML = ''; // 清空列表(整个替换,效率低)todos.forEach(todo => {const li = document.createElement('li');li.className = todo.completed ? 'completed' : '';li.innerHTML = `<span>${todo.text}</span><button class="delete">删除</button>`;list.appendChild(li);});
}// 问题:即使只有一个todo变化,也会重新创建所有DOM元素

虚拟DOM的工作原理(简化版)

// 1. 定义虚拟DOM节点结构
class VNode {constructor(tag, props, children) {this.tag = tag;this.props = props;this.children = children;}// 2. 渲染为真实DOMrender() {const el = document.createElement(this.tag);// 设置属性Object.keys(this.props).forEach(key => {el.setAttribute(key, this.props[key]);});// 渲染子节点this.children.forEach(child => {const childEl = child instanceof VNode ? child.render() : document.createTextNode(child);el.appendChild(childEl);});return el;}
}// 3. 实现简单的diff算法(找出最小差异)
function diff(oldVNode, newVNode) {// 标签不同,直接替换if (oldVNode.tag !== newVNode.tag) {return { type: 'REPLACE', newVNode };}// 文本节点比较if (typeof oldVNode === 'string' && typeof newVNode === 'string') {if (oldVNode !== newVNode) {return { type: 'TEXT', content: newVNode };}return null;}// 属性比较const propsDiff = {};const oldProps = oldVNode.props || {};const newProps = newVNode.props || {};// 查找属性变化Object.keys(newProps).forEach(key => {if (oldProps[key] !== newProps[key]) {propsDiff[key] = newProps[key];}});// 查找被移除的属性Object.keys(oldProps).forEach(key => {if (!newProps.hasOwnProperty(key)) {propsDiff[key] = undefined;}});// 子节点比较(简化版)const childrenDiff = [];for (let i = 0; i < Math.max(oldVNode.children.length, newVNode.children.length); i++) {const childDiff = diff(oldVNode.children[i], newVNode.children[i]);if (childDiff) childrenDiff.push(childDiff);}return {type: 'UPDATE',props: propsDiff,children: childrenDiff};
}// 4. 使用虚拟DOM更新列表
function createTodoVNode(todo) {return new VNode('li', { class: todo.completed ? 'completed' : '' }, [new VNode('span', {}, [todo.text]),new VNode('button', { class: 'delete' }, ['删除'])]);
}function updateTodoListOptimized(todos) {// 创建新的虚拟DOM树const newVList = new VNode('ul', { id: 'todoList' }, todos.map(todo => createTodoVNode(todo)));// 与旧的虚拟DOM树比较(实际应用中会保存上一次的vNode)const oldVList = window.lastVList; // 假设我们保存了上一次的虚拟DOMconst changes = diff(oldVList, newVList);// 只更新有变化的部分(实际应用中会有patch函数执行这些变化)applyChanges(document.getElementById('todoList'), changes);// 保存当前虚拟DOM供下次比较window.lastVList = newVList;
}

实战建议

  • 小型项目:手动优化DOM操作可能比引入虚拟DOM更高效
  • 中大型项目:使用React、Vue等框架的虚拟DOM和diff算法,大幅减少DOM操作
  • 极端性能场景:结合Web Components或原生API做针对性优化

总结:DOM优化的"黄金法则"

  1. 减少操作次数:批量处理DOM变更,避免频繁的增删改
  2. 缓存查询结果:DOM查询代价高,复用查询结果
  3. 遵循渲染节奏:用requestAnimationFrame同步视觉更新
  4. 避免布局抖动:分离读写操作,不强制同步布局
  5. 智能更新:使用虚拟DOM或手动计算最小变更集

        记住:每次DOM操作都是"昂贵"的,优化的核心思想是减少实际DOM操作的数量和复杂度。在实际开发中,建议使用Chrome DevTools的Performance面板录制操作过程,找到真正的性能瓶颈后再针对性优化。

        最后送大家一句话:不是所有DOM操作都需要优化,但所有优化都应该基于测量。让我们的页面在性能与开发效率之间找到最佳平衡!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/93699.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/93699.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【力扣】面试经典150题总结02-双指针、滑动窗口

1.验证回文串&#xff08;简单&#xff09;用toLowerCase()转为小写字母&#xff0c;然后前后指针向中间进行比对。2.判断子序列&#xff08;简单&#xff09;两个指针一个指向长字符串&#xff0c;另一个指向短字符串。匹配就都1&#xff0c;不匹配就将长字符串指针1。长字符串…

MQ迁移方案

以下是完整的MQ迁移方案设计&#xff0c;涵盖同构/异构迁移、零丢失保障、灰度切换等关键环节&#xff0c;适用于Kafka、RabbitMQ、RocketMQ等主流消息队列&#xff1a;​一、迁移方案选型矩阵​​场景​​适用方案​​技术实现​​优缺点​​同集群版本升级​滚动重启 协议兼…

RAG 分块中表格填补简明示例:Markdown、HTML、Excel、Doc

表格填补是RAG分块中常见的需求&#xff0c;但不同格式的表格处理方式有所不同。本文将对 Markdown、HTML、Excel 的合并单元格进行说明&#xff0c;并给出 Python 示例&#xff0c;演示如何解析和填补。1. Markdown 表格Markdown 只能用空值表示合并单元格。&#xff08;只有列…

IDEA创建一个VUE项目

由于新手学习VUE&#xff0c;所以使用手动初始化项目 步骤&#xff1a; 创建项目文件夹&#xff1a;在 IDEA 中点击 File > New > Project&#xff0c;选择 Empty Project&#xff0c;指定项目路径。初始化 npm&#xff1a;在终端中&#xff1a;npm init -y安装vue&#…

Chrome插件开发实战:todoList 插件

以下是一个适合小团队自用的 Chrome TodoList 插件开发示例&#xff0c;包含基础功能&#xff08;增删改查、本地存储、统计&#xff09;和简洁的交互设计。代码结构清晰&#xff0c;适合新手学习或快速上手。 一、项目准备 创建插件项目目录 todo-list-extension&#xff0c;…

【Redis数据库开启SSL加密】【小白指南】【生产环境可用】附带Docker服务器配置和python连接Redis数据库代码(加密通讯版)

【Redis数据库开启SSL加密】【填坑指南】附带服务器配置和python连接测试代码 本教程转为小白提供设置Redis安全访问&#xff0c;自签名证书进行安全访问你的Redis数据库&#xff0c;轻松实现安全访问和保护数据库不被非法入侵。 本文原创&#xff0c;转载请注明出处&#xff0…

笔记本电脑键盘失灵【已解决】

配置环境硬件详情笔记本电脑联想拯救者y7000 2019 PG0&#xff08;已更新为win11&#xff09;外接键盘colorful ckb-p100问题今天笔记本开机后&#xff0c;进入登录页面输入密码&#xff0c;突然发现笔记本自带键盘&#xff08;我通常不用外接键盘&#xff09;的键失灵了&#…

postgresql运维问题解决:PG集群备节点状态异常告警处理

小亦平台会持续给大家科普一些运维过程中常见的问题解决案例&#xff0c;运维朋友们可以在常见问题及解决方案专栏查看更多案例 问题概述&#xff1a; 故障&#xff1a; pg数据库备节点状态异常现象&#xff1a; 一般为集群间心跳超时导致,现象为集群有fail-count失败数告警&…

Maven 开发实践

文章目录1. 搭建私服&#xff08;windows)2.上传依赖3.多个远程仓库配置4.其它1. 搭建私服&#xff08;windows) 软件下载 https://help.sonatype.com/en/download.html修改端口 etc/nexus-default.properties启动程序 管理员身份进入进入bin目录下执行.\nexus.exe /run创建Ma…

设计心得——如何架构选型

一、架构的作用 可能对于很多的公司&#xff0c;其实架构本身的重要性并不大。大家一定明白这回事&#xff0c;架构在实际的开发&#xff0c;在大多数的场景下其实用处并没有书籍和资料中讲的那样重要&#xff0c;甚至是可有可无。这样讲是不有些可笑&#xff1f;是不是觉得挺意…

vba学习系列(12)--反射率通过率计算复杂度优化25/8/17

系列文章目录 文章目录系列文章目录前言一、反射率通过率1.整体通过率2.整体通过率3.客户工艺匹配4.机台通过率分析5.镜片通过率罩次分析分析1.1分析1.26.镜片通过率圈数分析分析1.1分析1.28.镜筒通过率圈数分析分析1.1分析1.29.镜筒通过率罩次分析分析1.2总结前言 一、反射率通…

Microsoft WebView2

运行效果 代码如下 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Form…

GitCode 疑难问题诊疗:全方位指南

一、问题诊断与解决框架1.1 通用问题排查流程在面对 GitCode 问题时&#xff0c;遵循一套科学的排查流程至关重要。首先&#xff0c;详细记录问题出现时的具体操作步骤与相关报错信息&#xff0c;这有助于精准定位问题根源。例如&#xff0c;若在执行git push命令时出现错误&am…

AMD Ryzen AI Max+ 395四机并联:大语言模型集群推理深度测试

本文介绍使用四块Framework主板构建AI推理集群的完整过程&#xff0c;并对其在大语言模型推理任务中的性能表现进行了系统性评估。该集群基于AMD Ryzen AI Max 395处理器&#xff0c;采用mini ITX规格设计&#xff0c;可部署在10英寸标准机架中。 Jeff Geerling大佬还开发了名…

深度学习·GFSS

GFSS General Few-Shot Segmentation 任务实现方式与zero-shot有所不同本篇论文只涉及同一个模态(图像)&#xff0c;训练过程中&#xff0c;novel class有几个图像提供&#xff0c;提供k个就称之为k-shot。先从图像中提取class prototype&#xff0c;然后这个原型向量作为查询&…

Transformer架构的数学本质:从注意力机制到大模型时代的技术内核

系列专栏推荐&#xff1a;零基础学Python&#xff1a;Python从0到100最新最全教程 深入浅出讲解神经网络原理与实现&#xff0c;从基础的多层感知机到前沿的Transformer架构。包含完整的数学推导、代码实现和工程优化技巧。 写在前面&#xff1a;为什么理解Transformer如此重要…

最新微信小程序一键获取真实微信头像和昵称方法

使用公开免费插件&#xff0c;快速实现获取用户头像和昵称&#xff0c;已附uniapp、微信开发工具开发详细教程。前言为了保护用户隐私&#xff0c;wx.getUserInfo、wx.getUserProfile都没法获取到用户头像和昵称了&#xff0c;只能通过设计用户主动选择/输入形式&#xff0c;操…

路由器配置之模式

文章目录配置路由器时&#xff0c;有一个模式选择最佳实践各个选项的区别11b only11g only11n only11bg mixed11bgn mixed配置路由器时&#xff0c;有一个模式选择 最佳实践 • 追求速度&#xff1a;选 11n only&#xff08;需所有设备支持&#xff09;。 • 兼容性优先&…

评测系统构建

合成数据更“科研驱动”&#xff0c;强调 controllability 和 generalization evaluation&#xff1a; 之前往往直接采用经典数据集如OGB和OGB-large提供的经典数据集和数据划分思路 该思想从现有真实数据中学习参数&#xff0c;再构造类似但分布略异的数据集&#xff0c;验证模…

【计算机网络面试】TCP/IP网络模型有哪几层

参考&#xff1a; 2.1 TCP/IP 网络模型有哪几层&#xff1f; | 小林coding | Java面试学习 以下为自己做的笔记 应用层 专注于为用户提供应用功能&#xff0c;如HTTP、FTP、Telnet、DNS、SMTP等。应用层不关心用户是怎么传输的&#xff0c;当两个设备间的应用需要通信时&…