DOM操做成本到底高在哪儿?

从我接触前端到如今,一直听到的一句话:操做DOM的成本很高,不要轻易去操做DOM。尤为是React、vue等MV*框架的出现,数据驱动视图的模式愈加深刻人心,jQuery时代提供的强大便利地操做DOM的API在前端工程里用的愈来愈少。刨根问底,这里说的成本,到底高在哪儿呢?css

什么是DOM

Document Object Model 文档对象模型html

什么是DOM?可能不少人第一反应就是div、p、span等html标签(至少我是),但要知道,DOM是Model,是Object Model,对象模型,是为HTML(and XML)提供的API。HTML(Hyper Text Markup Language)是一种标记语言,HTML在DOM的模型标准中被视为对象,DOM只提供编程接口,却没法实际操做HTML里面的内容。但在浏览器端,前端们能够用脚本语言(JavaScript)经过DOM去操做HTML内容。前端

那么问题来了,只有JavaScript才能调用DOM这个API吗?vue

答案是NOnode

Python也能够访问DOM。因此DOM不是提供给Javascript的API,也不是Javascript里的API。git

PS: 实质上还存在CSSOM:CSS Object Model,浏览器将CSS代码解析成树形的数据结构,与DOM是两个独立的数据结构github

浏览器渲染过程

讨论DOM操做成本,确定要先了解该成本的来源,那么就离不开浏览器渲染。web

这里暂只讨论浏览器拿到HTML以后开始解析、渲染。(怎么拿到HTML资源的可能后续另开篇总结吧,什么握握握手啊挥挥挥挥手啊,万恶的flag...)chrome

  1. 解析HTML,构建DOM树(这里遇到外链,此时会发起请求)编程

  2. 解析CSS,生成CSS规则树

  3. 合并DOM树和CSS规则,生成render树

  4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算

  5. 绘制render树(paint),绘制页面像素信息

  6. 浏览器会将各层的信息发送给GPU,GPU将各层合成(composite),显示在屏幕上

1.构建DOM树

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
    <title>Critical Path</title>
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
  </body>
</html>
复制代码

不管是DOM仍是CSSOM,都是要通过Bytes → characters → tokens → nodes → object model这个过程。

DOM树构建过程:当前节点的全部子节点都构建好后才会去构建当前节点的下一个兄弟节点。

2.构建CSSOM树

上述也提到了CSSOM的构建过程,也是树的结构,在最终计算各个节点的样式时,浏览器都会先从该节点的广泛属性(好比body里设置的全局样式)开始,再去应用该节点的具体属性。还有要注意的是,每一个浏览器都有本身默认的样式表,所以不少时候这棵CSSOM树只是对这张默认样式表的部分替换。

3.生成render树

DOM树和CSSOM树合并生成render树

简单描述这个过程:

DOM树从根节点开始遍历可见节点,这里之因此强调了“可见”,是由于若是遇到设置了相似display: none;的不可见节点,在render过程当中是会被跳过的(但visibility: hidden; opacity: 0这种仍旧占据空间的节点不会被跳过render),保存各个节点的样式信息及其他节点的从属关系。

4.Layout 布局

有了各个节点的样式信息和属性,但不知道各个节点的确切位置和大小,因此要经过布局将样式信息和属性转换为实际可视窗口的相对大小和位置。

5.Paint 绘制

万事俱备,最后只要将肯定好位置大小的各节点,经过GPU渲染到屏幕的实际像素。

Tips

  • 在上述渲染过程当中,前3点可能要屡次执行,好比js脚本去操做dom、更改css样式时,浏览器又要从新构建DOM、CSSOM树,从新render,从新layout、paint;
  • Layout在Paint以前,所以每次Layout从新布局(reflow 回流)后都要从新出发Paint渲染,这时又要去消耗GPU;
  • Paint不必定会触发Layout,好比改个颜色改个背景;(repaint 重绘)
  • 图片下载完也会从新出发Layout和Paint;

什么时候触发reflow和repaint

reflow(回流): 根据Render Tree布局(几何属性),意味着元素的内容、结构、位置或尺寸发生了变化,须要从新计算样式和渲染树; repaint(重绘): 意味着元素发生的改变只影响了节点的一些样式(背景色,边框颜色,文字颜色等),只须要应用新样式绘制这个元素就能够了; reflow回流的成本开销要高于repaint重绘,一个节点的回流每每回致使子节点以及同级节点的回流;

GoogleChromeLabs 里面有一个csstriggers,列出了各个CSS属性对浏览器执行Layout、Paint、Composite的影响。

引发reflow回流

现代浏览器会对回流作优化,它会等到足够数量的变化发生,再作一次批处理回流。

  1. 页面第一次渲染(初始化)
  2. DOM树变化(如:增删节点)
  3. Render树变化(如:padding改变)
  4. 浏览器窗口resize
  5. 获取元素的某些属性: 浏览器为了得到正确的值也会提早触发回流,这样就使得浏览器的优化失效了,这些属性包括offsetLeft、offsetTop、offsetWidth、offsetHeight、 scrollTop/Left/Width/Height、clientTop/Left/Width/Height、调用了getComputedStyle()或者IE的currentStyle

引发repaint重绘

  1. reflow回流一定引发repaint重绘,重绘能够单独触发
  2. 背景色、颜色、字体改变(注意:字体大小发生变化时,会触发回流)

优化reflow、repaint触发次数

  • 避免逐个修改节点样式,尽可能一次性修改
  • 使用DocumentFragment将须要屡次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
  • 能够将须要屡次修改的DOM元素设置display: none,操做完再显示。(由于隐藏元素不在render树内,所以修改隐藏元素不会触发回流重绘)
  • 避免屡次读取某些属性(见上)
  • 将复杂的节点元素脱离文档流,下降回流成本

为何一再强调将css放在头部,将js文件放在尾部

DOMContentLoaded 和 load

  • DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片...
  • load 事件触发时,页面上全部的DOM,样式表,脚本,图片都已加载完成

CSS 资源阻塞渲染

构建Render树须要DOM和CSSOM,因此HTML和CSS都会阻塞渲染。因此须要让CSS尽早加载(如:放在头部),以缩短首次渲染的时间。

JS 资源

  • 阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析HTML
  • 普通的脚本会阻塞浏览器解析,加上defer或async属性,脚本就变成异步,可等到解析完毕再执行
    • async异步执行,异步下载完毕后就会执行,不确保执行顺序,必定在onload前,但不肯定在DOMContentLoaded事件的先后
    • defer延迟执行,相对于放在body最后(理论上在DOMContentLoaded事件前)

举个栗子

<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <link href="style.css" rel="stylesheet">
  </head>
  <body>
    <p>Hello <span>web performance</span> students!</p>
    <div><img src="awesome-photo.jpg"></div>
    <script src="app.js"></script>
  </body>
</html>
复制代码

  • 浏览器拿到HTML后,从上到下顺序解析文档
  • 此时遇到css、js外链,则同时发起请求
  • 开始构建DOM树
  • 这里要特别注意,因为有CSS资源,CSSOM还未构建前,会阻塞js(若是有的话)
  • 不管JavaScript是内联仍是外链,只要浏览器遇到 script 标记,唤醒JavaScript解析器,就会进行暂停 blocked 浏览器解析HTML,并等到 CSSOM 构建完毕,才执行js脚本
  • 渲染首屏(DOMContentLoaded 触发,其实不必定是首屏,可能在js脚本执行前DOM树和CSSOM已经构建完render树,已经paint)

首屏优化Tips

说了这么多,其实能够总结几点浏览器首屏渲染优化的方向

  • 减小资源请求数量(内联亦或是延迟动态加载)
  • 使CSS样式表尽早加载,减小@import的使用,由于须要解析完样式表中全部import的资源才会算CSS资源下载完
  • 异步js:阻塞解析器的 JavaScript 会强制浏览器等待 CSSOM 并暂停 DOM 的构建,致使首次渲染的时间延迟
  • so on...

知道操做DOM成本多高了吗?

其实写了这么多,感受偏题了,大量的资料参考的是chrome开发者文档。感受js脚本资源那块仍是有点乱,包括和DOMContentLoaded的关系,但愿你们能多多指点,多多批评,谢谢大佬们。

操做DOM具体的成本,说究竟是形成浏览器回流reflow和重绘reflow,从而消耗GPU资源。

参考文献:

developers.google.com/web/fundame…


已同步至我的博客-软硬皆施

Github 欢迎star :)

相关文章
相关标签/搜索