Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端最佳实践(一)——DOM操作 #14

Open
wengjq opened this issue Aug 3, 2017 · 0 comments
Open

前端最佳实践(一)——DOM操作 #14

wengjq opened this issue Aug 3, 2017 · 0 comments

Comments

@wengjq
Copy link
Owner

wengjq commented Aug 3, 2017

1、浏览器渲染原理

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

webkitflow
(图:WebKit 主流程)

分为以下四个步骤:

  1. 解析HTML(HTML Parser)

  2. 构建DOM树(DOM Tree)

  3. 渲染树构建(Render Tree)

  4. 绘制渲染树(Painting)

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

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

2、Repaints and reflows

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

回流或者重绘何时触发?

改变用于构建渲染树的任何内容都可能导致重绘或回流,例如:
1、添加,删除,更新DOM节点
2、用display: none(回流和重绘)或者visibility: hidden隐藏节点(只有重绘,因为没有几何更改)
3、添加样式表,调整样式属性
4、调整窗口大小,更改字体大小
5、页面初始化的渲染
6、移动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)重新建立后,浏览器会重新绘制页面上受影响的元素。重排的代价比重绘的代价高很多,重绘会影响部分的元素,而重排则有可能影响全部的元素。

3、DOM操作最佳实践

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

3.1、合并多次的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 ,并使其刷新队列并执行所有批量更改。 当您请求如下样式信息时(并非包含全部),会发生这种情况。见下图:

qq 20170809203754

以上所有这些基本上都是请求有关节点的样式信息,浏览器必须提供最新的值。 为了做到这一点,它需要应用所有计划的更改,刷新队列,强行回流。所以在有大批量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.2、让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.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.4、 设置具有动画效果的DOM元素为固定定位

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

4、具体例子

4.1、浏览器的批处理及回流

以下会通过一个具体例子来说明,链接地址如下: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.1、首先用谷歌浏览器打开如上的链接。按下F12,切换到Performance选项

结果如下图:

image

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

结果如下图:

image

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

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

image

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

4.1.4、点击圆点旁边的clear按钮清空,重复上述的操作,直到文字变蓝色停止:

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

image

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

4.2、频繁回流造成的影响

谷歌文档给的例子,链接地址如下: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及定位问题可以参考我的另外一篇文章什么?页面卡顿?操作慢?

@wengjq wengjq changed the title 前端最佳实践(一) 前端最佳实践(一)——DOM操作 Aug 5, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant