唠一唠
一个在面试中老生常谈的话题,浏览器的渲染原理你懂吗?我在掘金看过很多类似的面试题总结的文章,基本都绕不开这个话题,似乎成了很多面试官都喜欢问的问题,今天我们就来一起唠一唠这个话题。
当我们在浏览器中输入一个URL并按下回车时,一系列复杂的过程就开始了,最终将网页呈现在我们眼前。这个过程涉及了诸多步骤,包括解析HTML、CSS,构建DOM和CSSOM树,计算布局,绘制页面等等
总得来说其实就是分为下面五步:
- 解析html代码,生成一个DOM树
- 解析css,生成 CSSOM 树
- 将DOM 树 和 CSSOM 树结合,去除不可见的元素,生成 Render Tree
- 计算布局(回流|重排),根据Render Tree 进行布局的计算,得到每一个节点的几何信息
- 绘制页面(重绘),GPU根据布局信息绘制
面试官问起来,就按上面答即可,可能略显简陋,但大致上也差不多。既然聊到这,写文章肯定不能就这样就结束是吧,这才多少个字,哈哈哈。我们来唠的稍微深入一点,并根据第四步和第五步我们稍后可以引申出另一个话题怎样做到对浏览器的优化
。
步骤
我们先把上述五步简单拓展一下:
在解析html代码之前,浏览器肯定需要先拿到这些代码,所以准确来说还有一步,我这里把它称为第0步,它在第一步之前 - 发送请求与获取资源。
0. 发送请求与获取资源
当用户在浏览器中输入URL并按下回车键时,浏览器会向服务器发送请求,请求网页的HTML、CSS、JavaScript等资源。服务器接收到请求后,会返回相应的资源文件。具体如何请求的这里就不拓展开来讲了。
1. 解析HTML与构建DOM树
一旦浏览器获取到HTML资源,它会开始解析HTML代码,并构建DOM树。DOM树表示了页面的结构,它由各种元素节点、文本节点等组成,形成了一个层次结构的树状模型。
2. 解析CSS与构建CSSOM树
同时,浏览器也会解析CSS资源,并构建CSSOM树。CSSOM树表示了页面的样式信息,包括各个元素的样式属性。CSSOM树和DOM树将会结合在一起,用于后续的渲染过程。
3. 构建Render Tree
接下来,浏览器将DOM树和CSSOM树结合起来,构建出一个称为Render Tree的渲染树。Render Tree只包含页面中需要显示的部分,即可见的元素。这个过程中,浏览器会根据CSS样式信息来确定哪些元素是可见的,哪些是隐藏的。
4. 布局计算与回流(Reflow)
有了Render Tree后,浏览器就可以开始计算每个元素在页面中的位置和大小,这个过程称为布局计算,或者回流。当页面的布局发生变化时,例如窗口大小改变、元素尺寸变化等,浏览器会触发回流过程,重新计算并更新页面布局。
5. 绘制页面与重绘(Repaint)
布局计算完成后,浏览器就可以开始将页面绘制到屏幕上,这个过程称为绘制页面或者重绘。在重绘阶段,浏览器会根据Render Tree中每个节点的绘制信息,使用GPU将页面绘制到屏幕上。重绘发生在元素的非几何属性发生变化时,例如颜色、背景等。
根据上面第四步的解释,我们可以知道,窗口大小改变、元素尺寸变化等,浏览器会触发回流过程,重新计算并更新页面布局。既然知道了什么情况会造成回流,下面我们一起来看看下面这些代码会造成多少次回流。
ini
<div id="app">hello</div>
<script>
let app = document.getElementById('app')
app.style.position = 'relative';
app.style.width = '100px'
app.style.height = '200px'
app.style.left= '20px'
app.style.right = '20px'
</script>
我刚开始学的时候,脱口而出,四次!!竟然都这样问了,显然结果就不是四次了。那究竟是多少次呢,这取决于浏览器的优化策略,若是放在以前老版本的浏览器,上述代码的确会造成四次回流。而如今的市面上可见的浏览器显然大多数都有自己的优化策略,比如上述代码,浏览器会将设置width
、height
、left
和right
属性的操作合并成一次回流。因为这些属性的变化都会影响到元素的布局,浏览器会在计算新的布局时一次性考虑这些属性的变化,而不是每次修改都触发一次回流。所以最终答案为一次。
再看下面这部分代码
ini
app.style.width = '100px'
console.log(app.style.width);
app.style.height = '200px'
console.log(app.style.height);
app.style.left= '10px'
console.log(app.style.left);
app.style.right = '10px'
console.log(app.style.right);
这部分代码又会造成几次回流呢?拿我举例,刚开始学的时候,知道了浏览器有优化策略之后,我当然是自信的说出,一次!!果不其然,又错了。
答案是四次,这又是为什么呢?原来浏览器读取到console.log(),里面计算了容器的属性,相当于获取到了容器的宽高等等,和offsetWidth属性一样。offsetWidth是什么?那就是我们下面要讲的。
之前我们说现代浏览器有自己的优化策略,那么究竟是什么呢?
其实底层原理实现就是通过一个队列来实现,具体来说当代浏览器都有一个渲染队列机制,当一个元素的样式变更导致需要回流的时候,这个操作会进入渲染队列,然后浏览器会继续往下执行代码,如果还有相同行为, 继续进入队列,直到下面没有样式修改,浏览器会批量执行渲染队列中的回流过程, 这只发生一次回流。
那为什么上述浏览器读取到console.log()会造成一次回流呢?
原来,JavaScript中存在一些属性(包括上面的offsetWidth),他们会造成浏览器的渲染队列强制执行,他们分别是"盒模型属性"、"位置属性"和"滚动属性"。具体如下:
- 盒模型属性(Box Model Properties): 包括
offsetWidth
、offsetHeight
、clientWidth
、clientHeight
,它们提供了有关元素盒模型的信息,即元素在页面中所占据的空间大小、可见内容的大小等。 - 位置属性(Position Properties): 包括
offsetTop
、offsetLeft
、clientTop
、clientLeft
,它们提供了有关元素位置的信息,即元素相对于其父元素或视口的偏移量、边框的大小等。 - 滚动属性(Scroll Properties): 包括
scrollHeight
、scrollWidth
、scrollTop
、scrollLeft
,它们提供了有关元素滚动状态的信息,即元素内容的总大小和当前滚动位置。
以上这些属性都会造成浏览器的渲染队列强制执行,破坏浏览器的优化策略。
讲到这里,我们似乎已经明白了如何对浏览器渲染进行优化了,无非就是减少回流和重绘,而由于页面渲染的原理的第四步是回流,第五步是重绘,也就是说,回流一定会造成重绘,所以,看起来减少回流才是重中之重,当然减少重绘也可以对浏览器渲染进行优化。
如何减少回流
我们之前讲到了哪些方法会造成回流,以及现代浏览器存在优化策略,但是使用某一些JavaScript属性会破坏浏览器的优化策略。也就是说,使用这些方法或者少用会破坏浏览器的优化策略的这些JavaScript属性,就能到达减少回流的目的了。还有一些其他补充,具体总结为以下六点:
-
使用 classList 替代直接操作 style: 使用
classList
属性来添加、删除或切换元素的类名,而不是直接修改元素的样式属性。这样可以避免触发回流,因为添加或删除类名不会影响到元素的布局。ini// 不推荐的做法 element.style.width = '100px'; element.style.height = '100px'; element.style.backgroundColor = 'red'; // 推荐的做法 element.classList.add('new-style');
-
一次性修改多个样式属性: 尽量将多个样式属性的修改合并成一次操作,可以使用
element.style.cssText
或者element.setAttribute('style', '...')
来一次性设置多个样式属性,而不是分开设置。csselement.style.cssText = 'width: 100px; height: 100px; background-color: red;';
-
避免使用会造成浏览器的渲染队列强制执行的属性: 尽量避免在频繁操作 DOM 或读取元素样式时使用之前讲到的那些属性,特别是在需要高性能的场景下。如果需要频繁访问这些信息,可以尝试缓存这些值,以避免重复计算带来的性能开销。缓存示例如下:
ini
// 缓存元素的宽度和高度
let elementWidth = element.clientWidth;
let elementHeight = element.clientHeight;
// 缓存元素的滚动位置
let scrollTop = element.scrollTop;
let scrollLeft = element.scrollLeft;
// 缓存计算后的样式信息
let computedStyle = window.getComputedStyle(element);
let backgroundColor = computedStyle.backgroundColor;
let fontSize = computedStyle.fontSize;
// 使用缓存的值进行后续操作
element.style.width = (elementWidth + 10) + 'px'; // 使用缓存的宽度进行操作
element.style.height = (elementHeight + 20) + 'px'; // 使用缓存的高度进行操作
// 使用缓存的滚动位置进行操作
element.scrollTop = scrollTop + 100;
element.scrollLeft = scrollLeft + 50;
// 使用缓存的样式信息进行操作
console.log('Background color: ' + backgroundColor);
console.log('Font size: ' + fontSize);
-
离线操作 DOM: 在需要对 DOM 进行多次操作时,可以先将元素从文档中移除,然后进行操作,最后再将其放回文档中。这样可以避免每次操作都触发回流。
scss// 脱离文档流 parentElement.removeChild(element); // 执行多次样式修改 // 放回文档流 parentElement.appendChild(element);
-
使用文档片段(DocumentFragment): 如果需要多次向 DOM 中添加新元素,可以先将这些元素添加到文档片段中,然后一次性将文档片段添加到 DOM 中。这样可以减少多次回流带来的性能损耗。
ini// 创建文档片段 let fragment = document.createDocumentFragment(); // 向文档片段中添加新元素 for (let i = 0; i < 10; i++) { let div = document.createElement('div'); div.textContent = 'New Element ' + i; fragment.appendChild(div); } // 一次性将文档片段添加到 DOM 中 parentElement.appendChild(fragment);
-
使用 flexbox 或 grid 布局: 使用弹性盒子布局(flexbox)或网格布局(grid)等现代 CSS 布局方式,可以更方便地进行布局调整,而不会触发过多的回流。这些布局方式对于响应式设计也更加友好。
如何减少重绘
-
使用 CSS 动画代替 JavaScript 动画: 使用 CSS 的动画属性(如
transition
、transform
、animation
等)来实现动画效果,而不是使用 JavaScript 来修改样式属性。因为浏览器可以对 CSS 动画进行优化,减少重绘次数。 -
使用 will-change 属性: 将预期进行动画或变换的元素的
will-change
属性设置为对应的属性值(如transform
、opacity
等),可以提前告诉浏览器该元素将会发生变化,从而优化渲染过程,减少重绘次数。csscssCopy code .animated-element { will-change: transform, opacity; }
-
使用 CSS transform 和 opacity 来触发硬件加速: 使用 CSS 的
transform
属性和opacity
属性来触发硬件加速,可以减少重绘次数。尤其是在移动端设备上,使用硬件加速可以提高动画的流畅度和性能。 -
避免频繁修改样式属性: 尽量避免在 JavaScript 中频繁修改样式属性,特别是影响元素布局的属性,如宽度、高度、位置等。可以将多个样式属性的修改合并成一次操作,以减少重绘次数。
-
使用文档片段(DocumentFragment): 如果需要多次向 DOM 中添加新元素,可以先将这些元素添加到文档片段中,然后一次性将文档片段添加到 DOM 中。这样可以减少重绘次数。
-
避免使用table布局: table布局通常会触发大量的重绘,因为它的渲染方式与常规文档流有所不同。如果可能的话,尽量使用其他布局方式来减少重绘。
至此,本篇文章也就到此结束了。那就,多读书,多看报,少吃零食,多睡觉。
假如您也和我一样,在准备春招。欢迎加我微信biangbiang6161,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!