美文网首页前端性能优化总结
渲染篇三:DOM优化原理与基本实践

渲染篇三:DOM优化原理与基本实践

作者: 六个周 | 来源:发表于2018-12-10 21:14 被阅读69次

    今天有点累,把小册的一章内容贴出,刷刷

    一、DOM 为什么这么慢

    JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。

    在渲染篇二中,我们知道JS 引擎和渲染引擎(浏览器内核)是独立实现的。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”。
    我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),就会产生比较明显的性能问题。

    二、回流与重绘

    很多时候,我们对 DOM 的操作都不会局限于访问,而是为了修改它。当我们对 DOM 的修改会引发它外观(样式)上的改变时,就会触发回流或重绘。

    回流:当我们对 DOM 的修改引发了 DOM 几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性(其他元素的几何属性和位置也会因此受到影响),然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。

    重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式(跳过了上图所示的回流环节)。这个过程叫做重绘。
    重绘不一定导致回流,回流一定会导致重绘。硬要比较的话,回流比重绘做的事情更多,带来的开销也更大。

    三、DOM提速

    首先看一段代码:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>DOM操作测试</title>
    </head>
    <body>
    <div id="container"></div>
    </body>
    </html>
    

    此时有一个假需求——往 container 元素里写 10000 句一样的话。如果这么做:

    for(var count=0;count<10000;count++){ 
      document.getElementById('container').innerHTML+='<span>我是一个小测试</span>'
    } 
    

    我们每一次循环都调用 DOM 接口重新获取了一次 container 元素,相当于每次循环都去操作DOM,这样不必要的DOM操作过多。
    我们可以通过另一个方式来节省不必要的渲染:

    let container = document.getElementById('container')
    let content = ''
    for(let count=0;count<10000;count++){ 
      // 先对内容进行操作
      content += '<span>我是一个小测试</span>'
    } 
    // 内容处理好了,最后再触发DOM的更改
    container.innerHTML = content
    

    在我们上面的例子里,字符串变量 content 就扮演着一个 DOM Fragment 的角色。其实无论字符串变量也好,DOM Fragment 也罢,它们本质上都作为脱离了真实 DOM 树的容器出现,用于缓存批量化的 DOM 操作。

    前面我们直接用 innerHTML 去拼接目标内容,这样做固然有用,但却不够优雅。相比之下,DOM Fragment 可以帮助我们用更加结构化的方式去达成同样的目的,从而在维持性能的同时,保住我们代码的可拓展和可维护性。我们现在用 DOM Fragment 来改写上面的例子:

    let container = document.getElementById('container')
    // 创建一个DOM Fragment对象作为容器
    let content = document.createDocumentFragment()
    for(let count=0;count<10000;count++){
      // span此时可以通过DOM API去创建
      let oSpan = document.createElement("span")
      oSpan.innerHTML = '我是一个小测试'
      // 像操作真实DOM一样操作DOM Fragment对象
      content.appendChild(oSpan)
    }
    // 内容处理好了,最后再触发真实DOM的更改
    container.appendChild(content)
    

    我们运行这段代码,可以得到与前面两种写法相同的运行结果。
    可以看出,DOM Fragment 对象允许我们像操作真实 DOM 一样去调用各种各样的 DOM API,我们的代码质量因此得到了保证。并且它的身份也非常纯粹:当我们试图将其 append 进真实 DOM 时,它会在乖乖交出自身缓存的所有后代节点后全身而退,完美地完成一个容器的使命,而不会出现在真实的 DOM 结构中。这种结构化、干净利落的特性,使得 DOM Fragment 作为经典的性能优化手段大受欢迎,这一点在 jQuery、Vue 等优秀前端框架的源码中均有体现。

    相关文章

      网友评论

      本文标题:渲染篇三:DOM优化原理与基本实践

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