前端最佳实践(一)——DOM操做

前言

本文是博主最近处理一些网站卡顿问题的学习记录。
原文地址:前端最佳实践(一)——DOM操做,以为有用的话能够给个star,谢谢啦。
做者:wengjqcss

一、浏览器渲染原理

在讲DOM操做的最佳性能实践以前,先介绍下浏览器的基本渲染原理。浏览器渲染展现网页的主流程大体能够用下图表示:html

图:WebKit 主流程
图:WebKit 主流程

分为如下四个步骤:前端

  • 解析HTML(HTML Parser)git

  • 构建DOM树(DOM Tree)github

  • 渲染树构建(Render Tree)后端

  • 绘制渲染树(Painting)浏览器

浏览器请求解析(Parser) HTML 文档,并将各标记逐个转化成 DOM 节点(DOM Tree)。同时也会解析外部 CSS 文件以及样式元素中的样式数据。HTML 中这些带有视觉指令的样式信息将用于建立另外一个树结构:呈现树(Render Tree)。呈现树(Render Tree)包含多个带有视觉属性(如颜色和尺寸)的矩形。这些矩形的排列顺序就是它们将在屏幕上显示的顺序。呈现树(Render Tree)构建完毕以后,进入“布局”处理阶段,也就是为每一个节点分配一个应出如今屏幕上的确切坐标。下一个阶段是绘制(Painting) - 浏览器会遍历呈现树(Render Tree),由用户界面后端层将每一个节点绘制出来。缓存

须要着重指出的是,这是一个渐进的过程。为达到更好的用户体验,浏览器会力求尽快将内容显示在屏幕上。它没必要等到整个 HTML 文档解析完毕以后,就会开始构建呈现树和设置布局。在不断接收和处理来自网络的其他内容的同时,浏览器会将部份内容解析并显示出来。bash

二、Repaints and reflows

Repaint:能够理解为重绘或重画,当render tree中的一些元素须要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,例如改变背景颜色 。则就叫称为重绘。
Reflows:能够理解为回流、布局或者重排,当渲染树(render Tree)中的一部分(或所有)由于元素的规模尺寸,布局,隐藏等改变而须要从新构建。这就称为回流(reflow),也就是从新布局(relayout)。网络

回流或者重绘什么时候触发?

改变用于构建渲染树的任何内容均可能致使重绘或回流,例如:
一、添加,删除,更新DOM节点
二、用display: none(回流和重绘)或者visibility: hidden隐藏节点(只有重绘,由于没有几何更改)
三、添加样式表,调整样式属性
四、调整窗口大小,更改字体大小
五、页面初始化的渲染
六、移动DOM元素
。。。

咱们来看几个例子:

var bstyle = document.body.style; // cache

bstyle.padding = "20px"; // reflow, repaint
bstyle.border = "10px solid red"; // another reflow and a repaint

bstyle.color = "blue"; // repaint only, no dimensions changed
bstyle.backgroundColor = "#fad"; // repaint

bstyle.fontSize = "2em"; // reflow, repaint

// new DOM element - reflow, repaint
document.body.appendChild(document.createTextNode('dude!'));复制代码

咱们能够想象一下,若是直接在渲染树(render Tree)最后面增长或者删除一个节点,这对于浏览器渲染页面来讲无伤大雅,由于只须要在渲染树(render Tree)的末端重绘那一部分变更的节点。可是,若是是在页面的顶部变更一个节点,浏览器须要从新计算渲染树(render Tree),致使渲染树(render Tree)的一部分或所有发生变化。渲染树(render Tree)从新创建后,浏览器会从新绘制页面上受影响的元素。重排的代价比重绘的代价高不少,重绘会影响部分的元素,而重排则有可能影响所有的元素。

三、DOM操做最佳实践

DOM操做带来的页面 Repaints 和 Reflows 是不可避免的,但能够遵循一些最佳实践来最大限度地减小Repaints 和 Reflows。以下是一些具体的实践方法:

3.一、合并屡次的DOM操做

// bad
var left = 10,
  top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// better 
el.className += " theclassname";
// better
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";复制代码

因为与渲染树更改相关的 Repaints and Reflows 是代价很是高,所以现代浏览器针对频繁的 Repaints and Reflows 有性能的优化。 一个策略是浏览器将设置脚本所需更改的队列,并分批执行。 这样,每一个须要 Reflows 的几个变化将被组合,而且将仅计算一个 Reflows 。 浏览器能够添加排队的更改,而后在必定时间过去或达到必定数量的更改后刷新队列(并非全部的浏览器都存在这样的优化。推荐的方式是把DOM操做尽可能合并)。但有时脚本可能会阻止浏览器优化 Reflows ,并使其刷新队列并执行全部批量更改。 当您请求以下样式信息时(并不是包含所有),会发生这种状况。见下图:

以上全部这些基本上都是请求有关节点的样式信息,浏览器必须提供最新的值。 为了作到这一点,它须要应用全部计划的更改,刷新队列,强行回流。因此在有大批量DOM操做时,应避免获取DOM元素的布局信息,使得浏览器针对大批量DOM操做的优化不被破坏。若是须要这些布局信息,最好是在DOM操做以前就去获取。

//bad
var bstyle = document.body.style;

bodystyle.color = 'red';
tmp = computed.backgroundColor;

bodystyle.color = 'white';
tmp = computed.backgroundImage;

bodystyle.color = 'green';
tmp = computed.backgroundAttachment;

//better
tmp = computed.backgroundColor;
tmp = computed.backgroundImage;
tmp = computed.backgroundAttachment;

bodystyle.color = 'yellow';
bodystyle.color = 'pink';
bodystyle.color = 'blue';复制代码

3.二、让DOM元素脱离渲染树(render Tree)后修改

(1)使用文档片断
DocumentFragments 是DOM节点。它们不是主DOM树的一部分。一般的用例是建立文档片断,将元素附加到文档片断,而后将文档片断附加到DOM树。在DOM树中,文档片断被其全部的孩子所代替。由于文档片断存在于内存中,并不在DOM树中,因此将子元素插入到文档片断时不会引发页面回流(Reflow)。固然,最后一步把文档片断附加到页面的这一步操做仍是会形成回流(Reflow)。

var fragment = document.createDocumentFragment();
// 一些基于fragment的大量DOM操做
...
document.getElementById('myElement').appendChild(fragment);复制代码

(2)经过设置DOM元素的display样式为none来隐藏元素
原理是先隐藏元素,而后基于元素作DOM操做,通过大量的DOM操做后才把元素显示出来。

var myElement = document.getElementById('myElement');
myElement.style.display = 'none';
// 一些基于myElement的大量DOM操做
...
myElement.style.display = 'block';复制代码

(3)克隆DOM元素到内存中
这种方式是把页面上的DOM元素克隆一份到内存中,而后再在内存中操做克隆的元素,操做完成后使用此克隆元素替换页面中原来的DOM元素。

var old = document.getElementById('myElement');
var clone = old.cloneNode(true);
// 一些基于clone的大量DOM操做
...
old.parentNode.replaceChild(clone, old);复制代码

3.三、使用局部变量缓存样式信息

获取DOM的样式信息会有性能的损耗,因此若是存在循环调用,最佳的作法是尽可能把这些值缓存在局部变量中。

// bad
function resizeAllParagraphsToMatchBlockWidth() {
    for (var i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = box.offsetWidth + 'px';
    }
}

// better
var width = box.offsetWidth;
function resizeAllParagraphsToMatchBlockWidth() {
    for (var i = 0; i < paragraphs.length; i++) {
        paragraphs[i].style.width = width + 'px';
    }
}复制代码

3.四、 设置具备动画效果的DOM元素为固定定位

使用绝对定位使得该元素在渲染树中成为 body 下的一个直接子节点,所以当它进行动画时,它不会影响太多其余节点。

四、具体例子

4.一、浏览器的批处理及回流

如下会经过一个具体例子来讲明,连接地址以下:reflow

第一次点击的代码以下:

function touch() {
    bodystyle.color = 'red';
    bodystyle.padding = '1px';
    tmp = computed.backgroundColor;
    bodystyle.color = 'white';
    bodystyle.padding = '2px';
    tmp = computed.backgroundImage;
    bodystyle.color = 'green';
    bodystyle.padding = '3px';
    tmp = computed.backgroundAttachment;
}复制代码

第二次点击的代码以下:

function touchlast() {
    tmp = computed.backgroundColor;
    tmp = computed.backgroundImage;
    tmp = computed.backgroundAttachment;
    bodystyle.color = 'yellow';
    bodystyle.padding = '4px';
    bodystyle.color = 'pink';
    bodystyle.padding = '5px';
    bodystyle.color = 'blue';
    bodystyle.padding = '6px';
}复制代码

如下咱们将经过谷歌工具来查看这两次操做有什么异同。

4.1.一、首先用谷歌浏览器打开如上的连接。按下F12,切换到Performance选项

结果以下图:

4.1.二、按下ctrl + E(或者点击小圆点)开始录制,点击 body 区域,待文字变成绿色后点击“stop”中止录制

结果以下图:

4.1.三、选中上图中蓝色(js堆)忽然升高的部分,表示刚才点击body的过程,滚动鼠标放大主线程

结果以下图,注意箭头指的地方:

从上图咱们能够很容易看到在点击body的过程当中,浏览器计算了3次样式。

4.1.四、点击圆点旁边的clear按钮清空,重复上述的操做,直到文字变蓝色中止:

结果以下图,注意箭头指的地方:

从上图咱们能够很容易看到在再次点击body的过程当中,浏览器只计算了1次样式。从而能够证实咱们上述的浏览器批处理的结论。未优化的Rendering(渲染)时间为0.4ms,而优化后的Rendering(渲染)时间为0.3ms,在这么小的js执行都有这么大的差异,在一些操做DOM频繁的动画中,浪费的性能可想而知,后面将会有一个动画的例子能够直观的看出来。

4.二、频繁回流形成的影响

谷歌文档给的例子,连接地址以下:animation

优化前的代码:

var pos = m.classList.contains('down') ?
    m.offsetTop + distance : m.offsetTop - distance;
    if (pos < 0) pos = 0;
    if (pos > maxHeight) pos = maxHeight;
        m.style.top = pos + 'px';
    if (m.offsetTop === 0) {
        m.classList.remove('up');
        m.classList.add('down');
    }
    if (m.offsetTop === maxHeight) {
        m.classList.remove('down');
        m.classList.add('up');
    }复制代码

优化后的代码:

var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
    m.classList.contains('down') ? pos += distance : pos -= distance;
    if (pos < 0) pos = 0;
    if (pos > maxHeight) pos = maxHeight;
    m.style.top = pos + 'px';
    if (pos === 0) {
        m.classList.remove('up');
        m.classList.add('down');
    }
    if (pos === maxHeight) {
        m.classList.remove('down');
        m.classList.add('up');
    }复制代码

先节流cpu,而后加多小“谷歌”图标,直到图标速度明显减慢,再点击“Optimize”优化按钮,能够明显感觉出差距。至于如何节流cpu及定位问题能够参考个人另一篇文章 什么?页面卡顿?操做慢?

相关文章
相关标签/搜索