美文网首页JavaScript
前端优化(DOM篇)

前端优化(DOM篇)

作者: 祈粼 | 来源:发表于2019-12-05 14:33 被阅读0次

    界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现 的。尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,所以前端性能优化的一个主要的关注 点就是DOM操作的优化。

    DOM操作优化的总原则是尽量减少DOM操作。

    先来看看DOM操作为什么会影响性能

    在浏览器中,DOM的实现和ECMAScript的实现是分离的。比如 在IE中,ECMAScrit的实现在jscript.dll中,而DOM的实现在mshtml.dll中;在Chrome中使用WebKit中的 WebCore处理DOM和渲染,但ECMAScript是在V8引擎中实现的,其他浏览器的情况类似。所以通过JavaScript代码调用DOM接 口,相当于两个独立模块的交互。相比较在同一模块中的调用,这种跨模块的调用其性能损耗是很高的。但

    DOM操作对性能影响最大其实还是因为它导致了浏览器 的重绘(repaint)和回流(reflow)。

    浏览器的渲染原理

    从下载文档到渲染页面的过程中,浏览器会通过解析HTML文档来构建DOM树,解析CSS产生CSS规则树。JavaScript代码在解析过程中, 可能会修改生成的DOM树和CSS规则树。之后根据DOM树和CSS规则树构建渲染树,在这个过程中CSS会根据选择器匹配HTML元素。渲染树包括了每 个元素的大小、边距等样式属性,渲染树中不包含隐藏元素及head元素等不可见元素。最后浏览器根据元素的坐标和大小来计算每个元素的位置,并绘制这些元 素到页面上。重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没用改变;回流则是元素的位置或尺寸发生了改变,浏览器需 要重新计算渲染树,导致渲染树的一部分或全部发生变化。渲染树重新建立后,浏览器会重新绘制页面上受影响的元素。回流的代价比重绘的代价高很多,重绘会影 响部分的元素,而回流则有可能影响全部的元素。#### 如下的这些DOM操作会导致重绘或回流:

    增加、删除和修改可见DOM元素

    页面初始化的渲染

    移动DOM元素

    修改CSS样式,改变DOM元素的尺寸

    DOM元素内容改变,使得尺寸被撑大

    浏览器窗口尺寸改变

    浏览器窗口滚动

    可以看出,这些操作都是DOM操作中比较常见的。现代浏览器会针对重排或重绘做性能优化,比如,把DOM操作积累一批后统一做一次重排或重绘。但在有些情况下,浏览器会立即重排或重绘。比如请求如下的DOM元素布局信息:offsetTop/Left/Width/Height、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()或currentStyle。因为这些值都是动态计算的,所以浏览器需要尽快完成页面的绘制,然后计算返回值,从而打乱了重排或重绘的优化。

    DOM操作带来的页面重绘或重排是不可避免的,但可以遵循一些最佳实践来降低由于重排或重绘带来的影响。如下是一些具体的实践方法:

    缓存DOM对象

    JavaScript的DOM操作可以说是JavaScript最重要的功能,我们经常要根据用户的操作来动态的增加和删除元素,或是通过AJAX返回的数据动态生成元素。比如我们获得了一个很多元素的数组data[],需要将其每个值生成一个li元素插入到一个id为container的ul元素中,最简单(最慢)的方式是:

    varliNode, i, m;
    
    for(i =0, m = data.length; i < m; i++) {
    
    liNode =document.createElement("li");
    
        liNode.innerText = data[i];
    
    document.getElementById("container").appendChild(liNode);
    
    }
    

    这里每一次循环都会去查找id为container的元素,效率自然非常低,所以我们需要将元素在循环前查询完毕,在循环中仅仅是引用就行了,修改代码为:

    varulNode =document.getElementById("container");
    
    varliNode, i, m;
    
    for(i =0, m = data.length; i < m; i++) {
    
    liNode =document.createElement("li");
    
        liNode.innerText = data[i];
    
        ulNode.appendChild(liNode);
    
    }
    

    缓存DOM对象的方式也经常被用在元素的查找中,查找元素应该是DOM操作中最频繁的操作了,其效率优化也是大头。在一般情况下,我们会根据需要,将一些频繁被查找的元素缓存起来,在查找它或查找它的子孙元素时,以它为起点进行查找,就能提高查找效率了。

    在内存中操作元素

    由于DOM操作会导致浏览器的回流,回流需要花费大量的时间进行样式计算和节点重绘与渲染,所以应当尽量减少回流次数。一种可靠的方法就是加入元素时不要修改页面上已经存在的元素,而是在内存中的节点进行大量的操作,最后再一并将修改运用到页面上。DOM操作本身提供一个创建内存节点片段的功能:document.createDocumentFragment(),我们可以将其运用于上述代码中:

    varulNode =document.getElementById("container");
    
    varliNode, i, m;
    
    varfragment =document.createDocumentFragment();
    
    for(i =0, m = data.length; i < m; i++) {
    
    liNode =document.createElement("li");
    
        liNode.innerText = data[i];
    
        fragment.appendChild(liNode);
    
    }
    
    ulNode.appendChild(fragment);
    

    这样就只会触发一次回流,效率会得到很大的提升。如果需要对一个元素进行复杂的操作(删减、添加子节点),那么我们应当先将元素从页面中移除,然后再对其进行操作,或者将其复制一个(cloneNode()),在内存中进行操作后再替换原来的节点

    这种方式是把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了,在内存中操作克隆元素不会引起页面上的性能损耗

    一次性DOM节点生成

    在这里我们每次都需要生成节点(document.createElement("li")),然后将其加入到内存片段中,我们可以通过innerHTML属性来一次性生成节点,具体的思路就是使用字符串拼接的方式,先生成相应的HTML字符串,最后一次性写入到ul的innerHTML中。修改代码为:

    varulNode =document.getElementById("container");
    
    varfragmentHtml ="", i, m;
    
    for(i =0, m = data.length; i < m; i++) {
    
    fragmentHtml +="<li>"+ data[i] +"</li>";
    
    }
    
    ulNode.innerHTML = fragmentHtml;
    

    这样效率也会有提升,不过手动拼写字符串是相当麻烦的一件事

    通过类修改样式

    有时候我们需要通过JavaScript给元素增加样式,比如如下代码:

    element.style.fontWeight ='bold';
    
    element.style.backgroundImage ='url(back.gif)';
    
    element.style.backgroundColor ='white';
    
    element.style.color ='white';
    
    //...
    

    这样效率很低,每次修改style属性后都会触发元素的重绘,如果修改了的属性涉及大小和位置,将会导致回流。所以我们应当尽量避免多次为一个元素设置style属性,应当通过给其添加新的CSS类,来修改其CSS

    .element {
    
        background-image: url(back.gif);
    
        background-color: #fff;
    
        color: #fff;
    
    font-weight:'bold';
    
    /*...*/
    
    }
    
    element.className +=" element";
    

    把DOM元素离线或隐藏后修改

    这种方式是通过隐藏页面的DOM元素,达到在页面中移除元素的效果,经过大量的DOM操作后恢复元素原来的display样式。对于这类会引起页面重绘或回流的操作,就只有隐藏和显示DOM元素这两个步骤了。代码类似如下:

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

    设置具有动画效果的DOM元素的position属性为fixed或absolute

    把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的回流,只涉及动画元素自身的回流了。这种做法可以提高动 画效果的展示性能。如果把动画元素设置为绝对定位并不符合设计的要求,则可以在动画开始时将其设置为绝对定位,等动画结束后恢复原始的定位设置。在很多的 网站中,页面的顶部会有大幅的广告展示,一般会动画展开和折叠显示。如果不做性能的优化,这个效果的性能损耗是很明显的。使用这里提到的优化方案,则可以 提高性能。

    谨慎取得DOM元素的布局信息

    前面讨论过,获取DOM的布局信息会有性能的损耗,所以如果存在重复调用,最佳的做法是尽量把这些值缓存在局部变量中。考虑如下的一个示例:

    for(vari=0;i< len;i++) {    myElements[i].style.top = targetElement.offsetTop + i*5+'px';}
    

    如上的代码中,会在一个循环中反复取得一个元素的offsetTop值,事实上,在此代码中该元素的offsetTop值并不会变更,所以会存在不必要的性能损耗。优化的方案是在循环外部取得元素的offsetTop值,相比较之前的方案,此方案只是调用了一遍元素的offsetTop值。更改后的代码如下:

    var targetTop = targetElement.offsetTop;
    for (var i=0; i < len; i++) {
        myElements[i].style.top = targetTop+ i*5 + 'px';
    }
    

    另外,因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或回流,所以在有大批量DOM操作时,应避免获取DOM元素 的布局信息,使得浏览器针对大批量DOM操作的优化不被破坏。如果需要这些布局信息,最好是在DOM操作之前就取得。考虑如下一个示例:

    ar newWidth = div1.offsetWidth + 10;
    div1.style.width = newWidth + 'px';
    var newHeight = myElement.offsetHeight + 10; // 强制页面回流
    myElement.style.height = newHeight + 'px'; // 又会回流一次
    

    根据上面的介绍,代码在遇到取得DOM元素的信息时会触发页面重新计算渲染树,所以如上的代码会导致页面回流两次,如果把取得DOM元素的布局信息提前,因为浏览器会优化连续的DOM操作,所以实际上只会有一次的页面回流出现,优化后的代码如下:

    var newWidth = div1.offsetWidth + 10;
    var newHeight = myElement.offsetHeight + 10;
    
    div1.style.width = newWidth + 'px';
    myElement.style.height = newHeight + 'px';
    

    通过事件代理批量操作事件

    还是之前那个ul和添加li,如果我们需要给每个li都绑定一个click事件,就可能写出类似如下代码:

    varulNode =document.getElementById("container");
    
    varfragment =document.createDocumentFragment();
    
    varliNode, i, m;
    
    varliFnCb =function(evt){
    
    //do something
    
    };
    
    for(i =0, m = data.length; i < m; i++) {
    
    liNode =document.createElement("li");
    
        liNode.innerText = data[i];
    
    liNode.addEventListener("click", liFnCb,false);
    
        fragment.appendChild(liNode);
    
    }
    
    ulNode.appendChild(fragment);
    

    这里每个li元素都需要执行一次addEventListener()方法,如果li元素数量一多,就会降低效率。所以我们可以通过事件代理的方式,将事件绑定在ul上,然后通过event.target来确定被点击的元素是否是li元素,同时我们也可以使用innerHTML属性一次性创建节点了,修改代码为:

    varulNode =document.getElementById("container");
    
    varfragmentHtml ="", i, m;
    
    varliFnCb =function(evt){
    
    //do something
    
    };
    
    for(i =0, m = data.length; i < m; i++) {
    
    fragmentHtml +="<li>"+ data[i] +"</li>";
    
    }
    
    ulNode.innerHTML = fragmentHtml;
    
    ulNode.addEventListener("click",function(evt){
    
    if(evt.target.tagName.toLowerCase() ==='li') {
    
            liFnCb.call(evt.target, evt);
    
        }
    
    },false);
    

    这样事件绑定的代码就只要执行一次,可以监听所有li元素的事件了。当然如果需要移除事件回调函数,我们也不需要循环遍历所有的li元素,只需要移除ul元素上的事件处理就行了。

    相关文章

      网友评论

        本文标题:前端优化(DOM篇)

        本文链接:https://www.haomeiwen.com/subject/fpvdgctx.html