高性能 JavaScript - DOM

这几天分享一下我看《高性能 JavaScript》的学习笔记,希望能对大家有所帮助。

如果说前面两章日常工作中不太关注,那么 DOM 优化确实我们必知必会的知识点了。

天生就慢

首先,DOM 天生就是非常慢的。为什么呢?因为 DOM 和 JavaScript 运行环境是两个环境,所以两者通信只能通过接口连接。就像是 DOM 和 JavaScript 运行时是两个岛屿,中间只能通过一艘小船来往。

所以,DOM 优化的核心就是尽量将更多的处理停留在 JavaScript 运行时这座小岛上,减少使用小船来往的次数。或者说尽量在 JavaScript 端处理逻辑,只在必要时访问 DOM。

DOM 的修改

  • 由于 API 交互特性,访问 DOM 的次数越多,代码运行的速度必然越慢。
  • 修改 DOM 元素有两种方式:element.innerHTML 和 document.createElement() 。两者的性能在新版本浏览器上差不多,而在老版本浏览器中 innerHTML 会更好。(个人觉得 innerHTML 写法也更加清晰)
  • 另外一种修改 DOM 的方式是通过 element.cloneNodes() 来克隆元素节点,修改克隆的元素然后替换 DOM 中的元素。

HTML 集合

我们使用一些 API 能够获取 HTML 集合,HTML 集合是一个带有 length 属性酷似数组的对象。它有一个很重要的特性就是 HTML 集合会随着 DOM 元素的变化而变化

  • HTML 集合的 length 是会随着 DOM 元素数量变化的,所以不要直接使用 length 属性。
  • 遍历数组要比遍历 HTML 集合的速度快。
  • 如果要使用 HTML 集合的 length,可以先将 length 保存为局部变量。
  • 多使用局部变量保存和引用 HTML 集合的信息,减少访问 DOM 的次数。

以下 DOM API 能够获取 HTML 集合。

  • document.getElementByName()
  • document.getElementByTagName()
  • document.getElementByClass()
  • element.childNodes 是通过某个元素获取他的子元素的,获取的也是 HTML 集合。

更快的 API

对于获取元素节点的 DOM 方法,有只获取元素节点和获取所有节点两类。性能上显然前者的性能会更高。

只获取元素节点 获取所有节点
children childNodes
childElementCount childNodes.length
firstElementChild firstChild
lastElementChild lastChild
nextElementSibling nextSibling
previousElementSibling previousSibling

在遍历 DOM 节点上,很多 DOM 方法可以做到。

  • document.getElementByName()
  • document.getElementByTagName()
  • document.getElementByClass()
  • document.querySelectorAll()
  • ……

其中 document.querySelectorAll() 使用了 CSS 选择器来获取 NodeList 数组对象。这种写法不仅方便,而且性能上也优于其他方法。

所以,如果浏览器支持,尽量使用上述的这些方法,因为这些 API 更快!

DOM 绘制过程

对于 DOM 性能而言,重绘和重排是最重要的知识点了。下面先复习一下浏览器工作原理:

  • 浏览器解析 HTML 获取 DOM 树。
  • 浏览器解析 CSS 获取 CSSOM 规则树。
  • 将 CSS 规则树应用到 DOM 树上构成渲染树。
  • 使用渲染树上的样式计算尺寸和位置,解析排版。
  • 更具渲染树属性生成位图,即绘制。
  • 最后使用浏览器 API 呈现这些有排版的位图。
  • 如果页面元素尺寸有变化,进行重新排版和绘制。
  • 如果页面无尺寸变化,而是像背景颜色这样的变化,则会进行重新绘制。

重排何时发生?

  • 添加或删除可见的 DOM 元素
  • 元素位置改变
  • 元素尺寸改变(包括:margin、padding、border-width、width、height 等属性)
  • 内容改变,如:文本改变、图片尺寸改变。
  • 页面渲染器初始化
  • 浏览器创建尺寸改变

立即重排

其实,在浏览器中重排是有优化机制的。浏览器会队列化修改并批量执行重排行为。但是使用了一些 API 后会立即进行重排行为。主要是一些查询当前布局信息的 API 方法:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (IE 中的当前样式)

优化方案是不在布局信息改变时查询布局信息

最小化重绘和重排

最小化重绘和重排的方式是尽量减少修改 DOM 的次数。

// bad
var el = document.getElementById('mydiv')
el.style.borderLeft = '1px'
el.syle.borderRight = '1px'
el.style.padding = '5px'

// good
el.style.cssText = 'border-left: 1px; border-right: 1px; padding: 5px;'

// good
el.className = 'active'

批量修改 DOM 方案很好的减少了修改 DOM 的次数。大致方案如下:

  1. 将需要修改的元素节点脱离文档流。
  2. 修改脱离文档流的元素节点。
  3. 将元素带入文档流。

具体的方案有三种:

  1. 将需要修改的 DOM 隐藏(display: none)后对节点进行修改,最后再将隐藏的节点显示出来。
var ul = document.getElementById('list')
ul.style.display = 'none'
appendDataToElement(ul, data)
ul.style.display = 'block'
  1. 使用 document.createDocumentFragment() 方法创建文档片段,在文档片段中定义修改的 DOM,最后将文档片段应用到 DOM 中。
var fragment = document.createDocumentFragment()
appendDataToElement(fragment, data)
document.getElementById('list').appendChild(fragment)
  1. 使用 document.cloneNode() 克隆元素节点,修改后用克隆节点替换原有节点。
var old = document.getElementById('list')
var clone = old.cloneNode(true)
appendDataToElement(clone, data)
old.parentNode.replaceChild(clone, old)

在基于现有元素尺寸修改尺寸时,最好使用局部变量缓存布局信息,这样可以减少访问 DOM 的次数。

事件委托

事件监听绑定也有一定的性能开销,可以使用事件委托方法来减少嵌套组件的重复事件绑定,具体可以看下 JavaScript 事件委托一文。

小结

DOM 非常慢,非常消耗性能。所以 DOM 性能优化是前端优化非常重要的一环。下面是主要内容:

  • 最小化 DOM 的访问次数,尽量将工作交给 JavaScript 去完成。
  • 小心 HTML 集合与数组的差别 —— HTML 集合会根据 DOM 的改变而发生改变。数组的性能优于 HTML 集合。
  • 使用批量修改的方式减少重排和重绘次数。
  • 使用事件委托减少事件绑定数量。

最后

由于书中内容太多,讲的比较笼统。不过还是希望能够对大家有所帮助吧。如果有什么问题欢迎留言和我沟通。

最后我有个疑问,既然说改变布局会产生重排,那么像 transform + translate 这种变形动画改变了大小的情况,重排的频率如何,是否特别消耗性能。相比于不断的修改 top、left、width 和 height 的性能如何(想必是更高的)?

这个问题之后有空我会做个调研写篇文章分享一下~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容