引言
当我们被面试官问到说说浏览的渲染或者是谈谈浏览器的渲染这个问题时一时会想不到聊些什么,这时我们可以通过下面的几个方面对其展开回答,并且下面也有面试考题和考点。
正文
浏览器的渲染过程
当用户在浏览器中输入URL并按下回车键时,浏览器就开始了一系列复杂的操作,从请求资源到最终呈现页面。以下是这个过程的主要步骤:
- 解析数据包得到HTML文件和CSS文本:浏览器接收到服务器返回的数据后,将二进制数据转换成可读的HTML和CSS文本。
- 构建DOM树:浏览器将HTML文本解析成一系列标记(Token),进而构建DOM树。
- 构建CSSOM树:同时,CSS文本被解析成CSSOM树。
- 构建渲染树:DOM树和CSSOM树结合形成渲染树,这个树只包含了那些实际需要显示在屏幕上的元素。
- 计算布局:浏览器计算每个元素的位置和尺寸,这一过程称为布局或回流。
- 绘制:最后,浏览器将渲染树的内容绘制到屏幕上,这一过程称为重绘。
回流与重绘
回流 (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
也没有用,就发生两次几何属性的改变,也就是渲染两次。
减少回流的技巧
- 让元素脱离文档流 :通过设置
display: none
或者使用绝对定位 (position: absolute
) 使元素脱离文档流,完成修改后再让它重新显示。 - 借助文档碎片:创建文档片段,将元素添加到文档片段中,最后一次性将文档片段添加到DOM树中。
- 克隆节点:克隆现有的节点,进行修改后替换原有节点。
我们来看一段代码:
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.让需要修改几何属性的容器先脱离文档流不显示修改完后再回到文档流中
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;"
把他放出来,就实现了只回流一次。
- 借助文档碎片
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
,最后只发生一次回流。
- 克隆节点
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
的时候,就势必会涉及到两个线程的通信和切换,会带来性能上的损耗
总结
以上就是本文的全部内容。希望对您有所帮助!感谢您的阅读!