面试经典考题:浏览器渲染

189 阅读5分钟

本文内容可能会比较干燥,但是干货满满

前言

     每当我们打开浏览器,浏览器都会加载一遍,这个加载就是浏览器需要向当前的URL发送请求。浏览器请求的目的就是为了获取到用户所需要的资源以及内容的呈现,也就是浏览器还会解析和渲染这些资源。那么在本文中Virtual09将会为大家着重介绍浏览器的渲染。

正文

浏览器渲染过程

     我们在浏览器请求的过程中可以获取到我们需要的资源,以用于浏览器的渲染。浏览器会从资源中解析出html文件,css文件,但是此时这些文件都不是真正意义上的html文件,css文件,由于在网络数据传输过程中,传输的都是二进制流,所以需要将二进制的字节数据解析为字符串才行。得到字符串之后,浏览器会对字符串进行一个Toekn标记,标记完之后就会将字符串数据变成一个个Node节点,然后再构建DOM树,将css文件构成CSSOM树。
此时浏览器会结合DOM树和CSSOM树。创建渲染树render 树。渲染树会包含我们屏幕上可见的元素,并且每一个元素都有几何信息。
接下来浏览器就会开始计算布局,浏览器的计算布局是浏览器解析和渲染网页的一个关键步骤。在这个过程中,浏览器会根据文档对象模型(DOM)和CSS样式信息,计算出每个元素在页面上的确切位置和尺寸。这里我们需要注意的是当某一个元素的css属性写了display:none,这个元素就不会被包含在渲染树上。
浏览器计算布局结束之后,就开始GPU绘制过程了,将渲染树中的的信息转化为屏幕上像素的实际颜色.
那么这时候就需要提起两个概念了
回流:

  1. 页面初次渲染
  2. 增加,删除可见的DOM元素
  3. 改变元素的几何信息(元素的尺寸或位置相关的CSS属性)
  4. 窗口大小改变 回流其实也就是计算布局

重绘:

  1. 非几何信息被修改

这里呢重绘就是GPU绘制

现在大家思考一个问题?回流必重绘? 重绘必回流?

当页面发生回流时,意味着元素的几何信息发生了变化,这通常需要浏览器重新计算渲染树,然后重新绘制受影响的元素。因此,回流总是伴随着重绘,因为即使大小或位置的微小变化也需要重新绘制元素的视觉表现。
如果元素的视觉表现变化不涉及其大小或位置,那么只会发生重绘,而不需要重新计算布局信息。例如,改变一个元素的背景颜色不会影响到其他元素的位置,所以只会发生重绘而不引起回流。 所以结果是回流必重绘 重绘不一定回流

那么又一个问题 为什么操作DOM慢?

因为JS 引擎线程和渲染线程互斥,(所以JS引擎线程会阻塞渲染线程,导致渲染慢)当我们通过Js来操作DOM的时候,势必会涉及到两个线程的通信和切换,会带来性能上的损耗。

我们现在来看一道面试题

<script>
    let el = document.getElementById('app')
    el.style.width = (el.offsetWidth + 1) + 'px'
    el.style.width = 1 + 'px'
  </script>

请问这个页面渲染了几次?有人肯呢个说是2次,也有3次.但是答案却是1次。啊?一次? 这是为什么呢?这时候就不得不把浏览器的优化这个东西拎出来给大家伙见见面了 浏览器会维护一个渲染队列中,当改变元素的几何属性导致回流发生时,回流行为会被加入到渲染队列中,在达到阈值或者一定时间之后会一次性将渲染队列中所有的回流生效 只要渲染队列中有回流行为,碰到了offsetLeft offsetTop等属性,都会触发回流 可以强制渲染队列刷新有
offset可以读到边框
offsetLeft offsetTop offsetWidth offsetHeight
client不可以读到边框
clientLeft clientTop clientWidth clientHeight
scrollLeft scrollTop scrollWidth scrollHeight

我们现在再来看看这道题目,在读到el.style.width = (el.offsetWidth + 1) + 'px'之间,我们渲染队列中是没有需要被渲染的元素在等待,所以遇到el.offsetWidth强制回流是不操作的,因为我们渲染队列为空。我们说的是当渲染队列中里面有需要被渲染的元素,遇见offsetWidth等一系列可以实现强制回流的操作时,渲染队列才会被强制渲染。这里当我们当走完offsetWidth之后,后面的操作都会被推进渲染队列中,等待执行,当在达到阈值或者一定时间之后会一次性将渲染队列中所有的回流生效。

那么假设我们向浏览器渲染10000个li呢,我们又不知道渲染队列的阈值在哪里,这时我们就需要减少回流了; 第一种方法,我们先把ul设置为display:none,因为display:none的元素是不会被挂在渲染树上的,当我们添加完了一万个li时,我们再把ul设置display:block,把ul重新挂回去,这时就等于我们只回流了一次。

<body>
  <ul id="demo"></ul>


  <script>
    let ul = document.getElementById("demo");
    ul.style.display = "none"
    for (let i = 0; i < 10000; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      clone.appendChild(li);
    }
    ul.style.display = "block";
  </script>
  </body>

第二种方法创建文档碎片 let frg = document.createDocumentFragment();会创建一个虚拟的标签 借助文档碎片,能够优化一些植入html这种操作

<body>
  <ul id="demo"></ul>


  <script>
    let ul = document.getElementById("demo");
    let frg = document.createDocumentFragment() // 文档碎片
 
    for (let i = 0; i < 10000; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      clone.appendChild(li);
    }
    ul.appendChild(frg)
  </script>
</body>

第三种方法使用克隆clone()直接克隆节点 let clone = <标签>.cloneNode(true) 先拿到该标签的父节点<标签>.parentNode.replaceChild(clone,<标签>),然后替换

<body>
  <ul id="demo"></ul>


  <script>
    let ul = document.getElementById("demo");
    let clone = ul.cloneNode(true)

    for (let i = 0; i < 10000; i++) {
      let li = document.createElement("li");
      let text = document.createTextNode(i)
      li.appendChild(text)
      clone.appendChild(li);
    }

    ul.parentNode.replaceChild(clone, ul);
  </script>
</body>

本文到此结束,感谢大家阅读,文章若有不足,恳请指出,谢谢大家!!