浏览器渲染原理
Posted chaimi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器渲染原理相关的知识,希望对你有一定的参考价值。
渲染机制:几个概念,没有代码。一般会问:
1、什么是DOCTYPE以及作用。
2、浏览器渲染过程。有的公司会问:浏览器中输入url,发生哪些事情,让你一步一步描写清楚?其中有一步就是浏览器渲染过程。输入url,会有一个dms解析,然后发到服务器,服务器再响应,响应过来,到了客户端,浏览器这边就开始渲染过程。
3、重排Reflow
4、重绘Repaint
5、布局Layout 这个和css布局不同,这个是浏览器的布局方式。这五个大概念,弄清楚。
什么是DOCTYPE以及作用
DTD告诉浏览器,我是什么文档类型。浏览器根据这个来判断用什么浏览引擎来解析它渲染它。
DOCTYPE 就是直接告诉浏览器什么是DTD。告诉浏览器包含哪个DTD,也就是哪个文档类型。
常见的DOCTYPE 有哪些?写一下html5的DOCTYPE怎么写?
要会写HTML5 的文档类型声明
然后知道HTML4.01有严格模式和传统模式,严格模式不包含展示性元素或弃用元素,如 <font>
浏览器渲染过程
浏览器的渲染机制一般分为以下几个步骤
- 处理 HTML 并构建 DOM 树。
- 处理 CSS 构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树renderTree。
- 根据渲染树来布局layout,计算每个节点的位置。
- 调用 GPU 绘制,合成图层,显示在屏幕上。
补充 render tree不包含html具体内容,也不知道具体位置是什么。比如有一个div,layout之前,不知道画在具体什么位置。这时候通过layout就可以精确计算到要显示的这些dom真正的宽高,位置颜色,最后开始paint,画图,把内容呈现出来。最后display,在浏览器上能看到页面效果
为了形成渲染树,浏览器大致做的事情有:从DOM树根节点开始,遍历每一个可见的节点一些节点是完全不可见的(比如 script标签,meta标签等),这些节点会被忽略,因为他们不会影响渲染的输出一些节点是通过CSS样式隐藏了,这些节点同样被忽略——例如如果 span样式是display:none;
span节点在render tree中被忽略,因为对每一个可见的节点,找到合适的匹配的CSSOM规则,并且应用样式显示可见节点(节点包括内容和被计算的样式)
在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。
css 放在head
(css如果不放在head里,浏览器渲染出html元素后,发现了css,还要重新再渲染,会造成页面卡顿。所以尽量放在头部,一次将DOM树和CSSOM树渲染出来)
js放在前
当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM。
Load 和 DOMContentLoaded 区别
Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。
DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载。
window.addEventListener(‘load‘, ); window.addEventListener(‘DOMContentLoaded‘, )
图层
一般来说,可以把普通文档流看成一个图层。特定的属性可以生成一个新的图层。不同的图层渲染互不影响,所以对于某些频繁需要渲染的建议单独生成一个新图层,提高性能。但也不能生成过多的图层,会引起反作用。
通过以下几个常用属性可以生成新图层
3D 变换:translate3d、translateZ
will-change
video、iframe 标签
通过动画实现的 opacity 动画转换
position: fixed
重绘(Repaint)和回流(Reflow)
重绘和回流是渲染步骤中的一小节,但是这两个步骤对于性能影响很大。
重绘是当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘
回流是布局或者几何属性需要改变就称为回流。
回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变深层次的节点很可能导致父节点的一系列回流。
回流何时发生:
当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:
1、添加或者删除可见的DOM元素;
2、元素位置改变;
3、元素尺寸改变——边距、填充、边框、宽度和高度
4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;
5、页面渲染初始化;
6、浏览器窗口尺寸改变——resize事件发生时;
很多人不知道的是,重绘和回流其实和 Event loop 有关。
- 当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
- 然后判断是否有 resize 或者 scroll ,有的话会去触发事件,所以 resize 和 scroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
- 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行 requestAnimationFrame 回调
- 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
- 更新界面
以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。
以上内容来自于 HTML 文档
聪明的浏览器
var s = document.body.style;
s.padding = "2px"; // 回流+重绘
s.border = "1px solid red"; // 再一次 回流+重绘
s.color = "blue"; // 重绘
s.backgroundColor = "#ccc"; // 重绘
s.fontSize = "14px"; // 再一次 回流+重绘
document.body.appendChild(document.createTextNode(‘abc!‘));// 添加node,再一次 回流+重绘
从上个实例代码中可以看到几行简单的JS代码就引起了6次左右的回流、重绘。而且我们也知道回流的花销也不小,如果每句JS操作都去回流重绘的话,浏览器可能就会受不了。所以很多浏览器都会优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
虽然有了浏览器的优化,但有时候我们写的一些代码可能会强制浏览器提前flush队列,这样浏览器的优化可能就起不到作用了。当你请求向浏览器请求一些 style信息的时候,就会让浏览器flush队列,比如:
offsetTop, offsetLeft, offsetWidth, offsetHeight
scrollTop/Left/Width/Height
clientTop/Left/Width/Height
width,height
请求了getComputedStyle(), 或者 IE的 currentStyle // 这个属性表示经过计算过最终的样式,可以参考张鑫旭的博客
当你请求上面的一些属性的时候,浏览器为了给你最精确的值,需要flush队列,因为队列中可能会有影响到这些值的操作。即使你获取元素的布局和样式信息跟最近发生或改变的布局信息无关,浏览器都会强行刷新渲染队列。
减少重绘和回流
减少回流、重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改),并减少对一些style信息的请求,尽量利用好浏览器的优化策略。具体方法有:
直接改变className,如果动态改变样式,则使用cssText(考虑没有优化的浏览器)
// 不好的写法
var left = 1;
var top = 1;
el.style.left = left + "px";
el.style.top = top + "px";
// 比较好的写法
el.className += " className1";
// 比较好的写法
el.style.cssText += ";
left: " + left + "px;
top: " + top + "px;";
#### 把 DOM 离线后修改,
比如:先把 DOM 给 display:none (有一次 Reflow),然后你修改 100 次,然后再把它显示出来
a) 使用DocumentFragment进行缓存操作,引发一次回流和重绘;
复制代码
//不好的写法(模式中所说的反模式)
var p, t;
p = document.creatElement(‘p‘);
t = document.creatTextNode(‘fist paragraph‘);
p.appendChild(t);
document.body.appendChild(p); //将引起一次回流
p = document.creatElement(‘p‘);
t = document.creatTextNode(‘second paragraph‘);
p.appendChild(t);
document.body.appendChild(p); //将再引起一次回流
//好的写法
var p, t, frag;
frag = document.creatDocumentFragment();
p = document.creatElement(‘p‘);
t = document.creatTextNode(‘fist paragraph‘);
p.appendChild(t);
farg.appendChild(p);
p = document.creatElement(‘p‘);
t = document.creatTextNode(‘second paragraph‘);
p.appendChild(t);
farg.appendChild(p);
document.body.appendChild(frag); //相比前面的方法,这里仅仅引起一次回流,倘若页面里有很多这样的操作,利用文档随便将会提升很多
不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存
//BAD WAY
for(循环) {
el.style.left = el.offsetLeft + 5 + "px";
el.style.top = el.offsetTop + 5 + "px";
}
// 这样写好点
var left = el.offsetLeft,
top = el.offsetTop,
s = el.style;
for (循环) {
left += 10;
top += 10;
s.left = left + "px";
s.top = top + "px";
}
或者使用 translate 替代 top
查了下,好像translate会启动硬件加速
<div class="test"></div>
<style>
.test {
position: absolute;
top: 10px; //transform: translateY(50px);
width: 100px;
height: 100px;
background: red;
}
</style>
<script>
setTimeout(() => {
// 引起回流
document.querySelector(‘.test‘).style.top = ‘100px‘
}, 1000)
</script>
使用 visibility 替换 display: none
,因为前者只会引起重绘,后者会引发回流(改变了布局)
不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
所以面试的时候可能问你要给一个dom元素循环添加一组dom元素,要放在循环内还是循环外:其实考的是回流和重绘
for (let i = 0; i < 1000; i++) {
// 获取 offsetTop 会导致回流,因为需要去获取正确的值
console.log(document.querySelector(‘.test‘).style.offsetTop)
}
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
CSS 选择符从右往左匹配查找,避免 DOM 深度过深
将频繁运行的动画变为图层,图层能够阻止该节点回流影响别的元素。
比如对于video
标签,浏览器会自动将该节点变为图层。
以上是关于浏览器渲染原理的主要内容,如果未能解决你的问题,请参考以下文章