从输入 URL 到页面打开
1、浏览器渲染大致流程
- a、处理 HTML 标记并构造 DOM 树
- b、处理 CSS 标记并构造 CSSOM 树
- c、将 DOM 与 CSSOM 合并成一个渲染树
- d、根据渲染树来布局,以计算每个节点的几何信息
- e、将各个几点绘制到屏幕上
2、生成渲染树流程(渲染树只包含可见的节点)
- a、从 DOM 树的根节点开始遍历每个可见节点
- b、对于每个可见的节点,找到 CSSOM 树中对应的规则,并应用它们
- c、根据每个可见节点以及其对应的样式,组合生成渲染树
第一步遍历节点的时候,需要知道什么节点是不可见的
-
a、一些不会渲染输出的节点。比如 script、meta、link等
-
b、某些节点通过 CSS 隐藏。注意 visibility: hidden 与 display: none 是不一样的。前者隐藏元素,但元素仍占据着布局空间(即将其渲染成一个空框),而后者 (display: none) 将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分
3、回流(reflow)
回流:
当 Render Tree 中部分或全部, 因元素的尺寸、布局、隐藏等改变而需要重新构建,浏览器重新渲染的过程
会导致回流的操作
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或者位置发生改变
- 元素内容变化(文字数量或者图片大小发生改变)
- 元素字体大小的改变
- 添加或者删除可见的 DOM 元素
- 激活 CSS 伪类 (eg: :hover)
- 查询某些属性或调用某些方法
一些常用且会导致回流的属性和方法
- clientWidth、clientHeight、clientTop、clientLeft
- offsetWidth、offsetHeight、offsetTop、offsetLeft
- scrollWidth、scrollHeight、scrollTop、scrollLeft
- scrollIntoView()、scrollIntoViewIfNeeded()
- getComputedStyle()
- getBoundingClientRect()
- scrollTo()
4、重绘(repaint)
重绘:
当页面中元素样式的改变并不影响b布局时(eg:color、background-color等),浏览器会将新样式赋予给元素并重新绘制它的过程
==回流必将引起重绘,重绘不一定会引起回流==
性能影响
有时即使仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流
现代浏览器会对频繁的回流或重绘操作进行优化
浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次
当你访问以下属性或方法时,浏览器会立刻清空队列
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
因为队列中可能会有影响到这些属性或方法返回值的操作,即使你希望获取的信息与队列中操作引发的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的
优化重绘回流
- a、减少对 render tree 的操作 【合并多次多DOM和样式的修改】
- b、减少对一些style信息的请求,尽量利用好浏览器的优化策略
具体体现在 CSS 和 JS 方面为
-
1、CSS 方面
a、直接改变className. (把要修改的样式集中到一个 class 内统一修改)
b、避免使用 table 布局(尽量不要使用表格布局,如果没有定宽表格一列的宽度由最宽的一列决定,那么很可能在最后一行的宽度超出之前的列宽,引起整体回流造成table可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。)
c、尽可能在DOM树的最末端改变class,尽可能在DOM树的里面改变class(可以限制回流的范围)
d、将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位
e、使用display:none技术,只引发两次回流和重绘(可以先隐藏,执行修改等操作,好了之后在显示出来) -
2、js 方面
a、避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。(而不是利用js控制样式)
b、不要经常访问会引起浏览器缓存队列的属性(上述那些浏览器会立刻清空队列的属性)。如果确实要访问,利用缓存,具体如下代码
// bad
for (var i = 0; i < len; i++) {
el.style.left = el.offsetLeft + x + "px";
el.style.top = el.offsetTop + y + "px";
}
// good
var x = el.offsetLeft,
y = el.offsetTop;
for (var i = 0; i < len; i++) {
x += 10;
y += 10;
el.style = x + "px";
el.style = y + "px";
}
c、尽量将需要改变DOM的操作一次完成
let box = document.getElementById("box").style;
// bad
box.color = "red"; // 重绘
box.size = "14px"; // 回流、重绘
// good
box.bord = '1px solid red'
d、对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流
e、避免频繁操作 DOM,创建一个documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中
DocumentFragment:
文档片段接口,表示一个没有父级文件的最小文档对象. 与 Document 最大的区别是因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的(重新渲染) ,且不会导致性能等问题;可以使用 document.createDocumentFragment 方法或者构造函数来创建一个空的 DocumentFragment