面试题:浏览器的渲染

引言

当我们被面试官问到说说浏览的渲染或者是谈谈浏览器的渲染这个问题时一时会想不到聊些什么,这时我们可以通过下面的几个方面对其展开回答,并且下面也有面试考题和考点。

正文

浏览器的渲染过程

当用户在浏览器中输入URL并按下回车键时,浏览器就开始了一系列复杂的操作,从请求资源到最终呈现页面。以下是这个过程的主要步骤:

  1. 解析数据包得到HTML文件和CSS文本:浏览器接收到服务器返回的数据后,将二进制数据转换成可读的HTML和CSS文本。
  2. 构建DOM树:浏览器将HTML文本解析成一系列标记(Token),进而构建DOM树。
  3. 构建CSSOM树:同时,CSS文本被解析成CSSOM树。
  4. 构建渲染树:DOM树和CSSOM树结合形成渲染树,这个树只包含了那些实际需要显示在屏幕上的元素。
  5. 计算布局:浏览器计算每个元素的位置和尺寸,这一过程称为布局或回流。
  6. 绘制:最后,浏览器将渲染树的内容绘制到屏幕上,这一过程称为重绘。

回流与重绘

回流 (Reflow)

回流发生在页面初次渲染、增加或删除可见的DOM元素、改变元素的几何信息、窗口大小改变或字体大小改变等情况下。回流是比较耗时的过程,因为它涉及到计算元素的尺寸和位置。

重绘 (Repaint)

重绘通常发生在非几何信息被修改的情况下,如颜色、背景等。重绘不会改变元素的尺寸或位置,因此比回流更快。

关系

回流必然导致重绘,但重绘不一定需要回流。

浏览器的优化机制

为了减少不必要的回流,浏览器维护了一个渲染队列。当元素的几何属性发生变化时,回流行为会被加入到队列中,在达到一定的阈值或者特定时间点后,浏览器会批量处理这些回流事件。

示例

问:下面代码浏览器会渲染几次页面?

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        div.style.left='10px'
        div.style.top='10px'
        div.style.width='10px'
        div.style.height='10px
    </script>
</body>
</html>

解析:这时我们会想,几何属性发生改变就会发生回流,回流就会重新渲染页面,既然如此,那就发生四次咯,大错特错了,如果是以前老版本的浏览器那确实会回流四次,但是现在的浏览器有了优化机制,这四次几何属性都会存入到渲染队列当中去,然后一定时间后一次性执行掉,所有实际上就发生一次。

强制渲染队列刷新

然而,某些属性的读取会强制刷新渲染队列 ,例如 offsetTop, offsetLeft, offsetWidth, offsetHeight, clientTop, clientLeft, clientWidth, clientHeight, scrollTop, scrollLeft, scrollTop, scrollWidth, scrollHeight 等。(其实很好理解,因为要读取容器属性值,如果不强制刷新,那读取的值不就错误了吗)

示例:

问:下面代码浏览器会渲染几次?

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        div.style.left='10px'
        console.log(div.offsetLeft)

        div.style.top='10px'
        console.log(div.offsetTop)

        div.style.width='10px'
        console.log(div.offsetWidth)

        div.style.height='10px'
        console.log(div.offsetHeight)        
    </script>
</body>
</html>

解析:当遇到上面列举的属性后,渲染队列会强制刷,每遇到一次就刷新一次队列,队列里有回流行为就会发生回流,,所有这里有四个,就发生四次。

我们再看一道面试题,问:下面代码浏览器会渲染几次?

面试题:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        let el = document.createElement('div');
        el.style.width = (el.offsetWidth+1) + 'px';
        el.style.width=1+'px';
    </script>
</body>
</html>

解析 :当我们不知道上面的东西时,我们可能会说两次,三次,其实但是当我们知道了上面的知道点后,我们就可以很好理解这道题的答案了,(el.offsetWidth+1)会刷新渲染队列对吧,但是渲染队列有回流行为吗?没有对吧,所有它只是刷新了队列没有回流,重新渲染页面,el.style.width = (el.offsetWidth+1) + 'px';这行代码是有回流行为的,放入到渲染队列当中去,el.style.width=1+'px';这行队列也是有回流行为的,也放入到渲染队列当中去,当一定时间后一次性执行掉,所以只发生一次 ,那放在老版本的浏览器发生几次呢,老版本没有渲染队列,offsetWudth也没有用,就发生两次几何属性的改变,也就是渲染两次。

减少回流的技巧

  1. 让元素脱离文档流 :通过设置 display: none 或者使用绝对定位 (position: absolute) 使元素脱离文档流,完成修改后再让它重新显示。
  2. 借助文档碎片:创建文档片段,将元素添加到文档片段中,最后一次性将文档片段添加到DOM树中。
  3. 克隆节点:克隆现有的节点,进行修改后替换原有节点。

我们来看一段代码:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>Document</title>
</head>
<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)
            ul.appendChild(li);
      }
    </script>
</body>
</html>

解析这段代码里会发生很多次回流对吧,渲染队列满了就会刷新一次进行回流,我们也不知道多少会满,不知道会发生几次回流,但肯定有很多次对吧,那如何减少回流呢,我们往下看:

示例 1 - 减少回流的三种方法

  1. 1.让需要修改几何属性的容器先脱离文档流不显示修改完后再回到文档流中
xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>Document</title>
</head>
<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)
            ul.appendChild(li);
      }
      ul.style.display = "block";
    </script>
</body>
</html>

解析 :我们先用 ul.style.display = "none";让使元素脱离文档流,循环结束后,再用ul.style.display = "block;"把他放出来,就实现了只回流一次。

  1. 借助文档碎片
xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>Document</title>
</head>
<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)
        frg.appendChild(li);
      }
      ul.appendChild(frg)
    </script>
</body>
</html>

解析 :我们创建一个文档碎片,也称之为虚拟文档片段,它会创建一个标签,这个标签你能用,但是它在浏览器不被真实显示出来,我们每次循环都往这个虚拟的标签里放li,循环完后再给ul,最后只发生一次回流。

  1. 克隆节点
xml 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0">
    <title>Document</title>
</head>
<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>
    </script>
</body>
</html>

解析 :我们可以看到克隆出了一个ul,我们循环把li都给clone,通过 ul.parentNode.replaceChild(clone, ul);这段代码先拿到ul的父节点body,告诉父节点要把clone替换掉ul,也只发生一次回流。

为什么操作DOM慢?

因为js引擎线程和渲染线程是互斥,所有,当我们通过js来操作DOM的时候,就势必会涉及到两个线程的通信和切换,会带来性能上的损耗


总结

以上就是本文的全部内容。希望对您有所帮助!感谢您的阅读!

相关推荐
阑梦清川2 小时前
在鱼皮的模拟面试里面学习有感
学习·面试·职场和发展
鱼跃鹰飞10 小时前
大厂面试真题-简单说说线程池接到新任务之后的操作流程
java·jvm·面试
程序员清风13 小时前
浅析Web实时通信技术!
java·后端·面试
测试199814 小时前
外包干了2年,快要废了。。。
自动化测试·软件测试·python·面试·职场和发展·单元测试·压力测试
mingzhi6115 小时前
渗透测试-快速获取目标中存在的漏洞(小白版)
安全·web安全·面试·职场和发展
嚣张农民15 小时前
一文简单看懂Promise实现原理
前端·javascript·面试
Liknana17 小时前
Android 网易游戏面经
android·面试
威哥爱编程20 小时前
MongoDB面试专题33道解析
数据库·mongodb·面试
程序猿进阶21 小时前
Redis 基础数据改造
java·开发语言·数据库·redis·后端·面试·架构