面试官:请讲一下你对性能优化理解,你在项目中做过哪些事情来优化你对项目
你能说出许多方式优化页面性能,比如:减少http请求、页面渲染完后加载JS文件、资源合并使用css雪碧图、懒加载、减少DOM操作等等
面试官:你提到减少DOM操作,那你请讲一下DOM操作对性能的影响,为什么减少DOM操作可以提升页面性能
😥DOM为什么天生就慢
DOM是什么相信大家都不陌生,DOM是一个独立于语言的,用于操作XML和HTML文档的程序接口,在浏览器中,DOM和javascript是独立的,但是DOM的接口确是使用javascript实现的。
DOM和ECMAScript是相互独立的,不同的浏览器中它们处在不同位置。就好比它俩在两座不同的岛上,每次它们要发生点交流,就需要通过桥梁连接,而过桥的时候会被收取“过桥费”,并且这个桥梁是一次性的,用过了之后就会崩塌,下次交流又需要在此搭建,在此产生“过桥费”,因此javascript操控DOM是一件很耗费性能的动作。
HTML集合也是造成DOM低效的原因:HTML集合是包含DOM节点引用的类数组对象,它并不是包含DOM节点,而是包含节点的引用,因此HTML集合一直与文档保持了连接,每次需要新的信息,都会重复执行查询的过程。
😮DOM元素的访问与修改
访问DOM元素就很耗费性能了,修改DOM那所需要的消耗就更甚。用比喻来理解修改DOM元素,这里先给两者所在岛屿取名 DOM岛(DOM元素所在的岛),JS岛(javascript所在的岛)。
- 第一次建立桥梁,JS岛派遣作业员去DOM岛查探是否存在目标元素
- 第二次建立桥梁,JS岛派遣作业员再次去DOM岛对目标元素进行修改
- 修改后会触发浏览器对页面的重新计算渲染
如此只要两者进行交互,就会产生损耗,并且触发了浏览器的重新绘制,其性能消耗会更到。如果循环多次修改DOM,会迅速积累损耗,造成使用卡顿,用户体验感差。
重绘与重排
上面提到了修改DOM会触发浏览器对页面的重新计算渲染,这是面试时经常会被问到的重绘与重排。 什么是重绘?什么又是重排?它们又是何时发生的?
我们先理解一个页面从加载到完成渲染是怎么做的, 浏览器会先对html进行解析构建DOM树,DOM树与CSS解析成的树型结构合并生成render树,得到render树之后浏览器就知道了每个节点的布局以及节点CSS,根据它们将页面正确的渲染到页面上
当DOM的变化影响了元素的几何属性(宽、高),浏览器需要重新计算元素的几何属性,同时其它的元素的集合属性也会受到影响,浏览器重新构建render树,这个过程成为“重排”。重排后,浏览器将这些受影响的部分重新绘制到页面上,这过程称为“重绘”
重排:简单总结,重排就是当页面发生布局的改动,几何属性的发生变化时会触发。触发重排的情况:
- 添加或删除可见的元素
- 元素的位置发生变化
- 元素尺寸被改变(外边距、内边距、边框厚度、宽、高)
- 内容发生改变,内容的改变会导致宽高的改变
- 浏览器窗口尺寸发生变化
重绘:重绘是元素的外观发生改变触发的,如颜色、背景的改变只会触发重绘,不会触发重排。
发生重排时必定也会发生重绘,但是发生重绘却不一定会发生重排
降低DOM对性能的影响
innerHTML VS DOM方法
在一个容器中添加内容,我们可以给这个容器设置innerHTML值,也可逐步生成dom元素appendChild到这个容器,那么这两者哪一种方式最快呢? 参考《高性能javascript》中给到的例子创建一个1000行的表格,统计他们各自完成所需要的时间,测试10次取平均耗时,对比发现两者耗时并没有相差多少,用那种方式取决于个人
减少重排与重绘
重排与重绘会家中浏览器的负担,因此我们需要想办法尽量减少触发他们的次数。
-
将多个DOM操作合并成一个然后交给浏览器一次性处理好
如元素需要更改宽、高、内外边距等,我们可能会写成这样
const el = document.getElementById('div') el.style.width = '200px' el.style.height = '200px' el.style.padding = '10px' el.style.margin = '10px'这样写的结果是每一次的属性设置都会影响元素的几何结构,浏览器需要重新计算构建render树,导致发生四次重排,我们可以对其进行优化处理,使用
cssText将四次样式属性设置合并成一个const el = document.getElementById('div') el.style.cssText = 'width:200px;height:200px;padding:10px;margin:10px' -
将DOM脱离文档,对其进行处理好后再加入到文档中
将DOM元素从文档流中脱离出来后对其进行一些列的处理,然后再加入到文档流中,这种方式的操作只有第一次脱离文档流和最后加入到文档流中会发生重排,中间做任何其他的操作都不会触发
三种方式可选:
-
通过设置display来显示隐藏元素
-
使用文档片段(DocumentFragment)
-
将原始元素拷贝到一个脱离文档流的节点,修改副本,然后再替换原始元素
-
善用事件委托
要给页面中的元素绑定事件,每绑定一次都会带来对性能带来影响,事件的绑定占用了处理的时间,浏览器追踪每个事件处理器也需要占用更多的内存。对于页面中可能存在多交互的情况,我们要善用事件委托技术进行优化,通过事件的冒泡、捕获定位到目标元素,减少事件的绑定
总结
减少DOM带来的性能损耗可以通过一下几点:
- 最小化DOM访问次数,尽可能在javascript端处理
- 如需多次访问DOM节点,使用局部变量对其进行存储
- 脱离文档流,减少重排重绘的次数
- 使用事件委托,减少事件处理器的绑定