渲染
1. 多线程模型
浏览器每创建一个 tab,就会创建一个进程,该进程间由如下的多线程模型构成:
-
主线程
- 可以理解为下面互斥的 GUI 渲染线程 和 JS 引擎线程,在执行谁,谁就是主线程
-
GUI 渲染线程(主线程)
-
负责页面渲染,包括 HTML 和 CSS 解析,构建 DOM 树、布局和绘制等
- 与 JS 引擎线程互斥,所以 js 代码如果堵塞,页面就会很卡
- 任务队列空闲时,主线程才会执行 GUI 渲染
-
JS 引擎线程(主线程)
- 负责处理 JS 脚本和代码,包括各种异步事件的回调
- 与 GUI 渲染线程互斥(虽然实现上属于不同线程,但为了怕互相影响,特意做成像是一个线程一样互斥)
-
事件触发线程(工作线程)
- 负责将事件加入到任务队列尾部,包括用户触发点击事件,下面的 定时器到期 和 异步请求成功
-
定时器触发线程(工作线程)
- 负责异步定时器的计数
- 执行代码时如果遇到定时器,就交给该线程处理,计数完毕后,就通知事件触发线程将回调加入任务队列尾部,等待 JS 引擎线程执行
-
异步 http 请求线程(工作线程)
- 负责异步请求的监测
- 执行代码如果遇到异步请求,就交给该线程处理,当监测到状态码变更,就通知事件触发线程将回调加入任务队列尾部,等待 JS 引擎线程执行
-
web works 等用户执行耗时任务的线程(工作线程)
- 用户使用 web worker 单独开启的计算任务线程,为了不让耗时任务在主线程中造成堵塞
2. 渲染机制
-
主线程解析 HTML 生成 DOM 树- HTML parser 会解析 html 文档,将标签转换成 DOM node(包括 js 生成的标签),根据父子关系生成 DOM 树
-
主线程解析 CSS 生成 CSSOM 树- CSS Parser 会解析 css 样式(包括 js 生成的和外部 css 引入的,也包括 html 中表示样式的如 b 标签),生成 CSSOM 树(不包含 display 为 none 的节点,也不包含 head 节点),其中每一个节点都有自己的 style
-
主线程结合二者,生成一棵渲染树(Render Tree) -
计算布局
主线程对渲染树从根节点开始递归调用,计算每一个元素的大小和位置,给出每个节点应该在屏幕上出现的精确坐标,生成布局树(Layout Tree)
-
分层
主线程为了处理 z 维度的元素覆盖顺序,可能需要生成多个图层树(Layer Tree)
-
绘制
合成线程得到对应的层,会按视窗大小进行分割,生成要显示的分块,交给光栅化线程光栅化线程对分块进行光栅化,真正输出为像素点,存储到 GPU 中合成线程对光栅化结果合并成合成帧,发给 GPU 完成显示- 以上两种线程都是在主线程之外独立完成的
3. 重绘与重排
什么叫重绘(repaint)?
- 就是把
绘制重新进行一遍 - 触发条件:当部分元素的外观属性发生变化,但不影响布局,如颜色,可见性
什么叫重排(reflow)?
- 就是把
计算布局+分层+绘制重新进行一遍 - 触发条件:当部分元素的几何属性发生变化,同时影响到其他元素的布局更新,如宽高,位置
触发重排的场景
-
页面初始化渲染(不可避免)
-
添加或删除可见的 DOM 元素
-
元素位置改变,或者使用动画
-
元素尺寸改变,如大小,边距
-
浏览器窗口尺寸变化,如 resize 事件
-
填充内容的改变,如文本内容或图片大小改变
-
读取某些元素属性时:
- offsetLeft/Top/Width/Height
- clientLeft/Top/Width/Heigh
- scrollLeft/Top/Width/Heigh
- width/height
- getComputedStyle
- getBoundingClientRect
【问】 第 7 点中为何获取也会导致重排?
【答】因为浏览器通过队列来批量更新布局,修改操作会被排到队列中,至少一个刷新(16.6ms)才会清空队列。
但是当获取某些属性时,队列中可能会有影响这些属性的操作,即使没有,浏览器也会强制清空队列,触发重排
如何减少重排和重绘
四个使用,四个避免,五个尽可能
- 使用 transform 替代 margin-top/left
正确
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
错误
margin-top:-$(this).height()/2;
margin-left:-$(this).width()/2;
- 使用 visibility 替代 display
正确
visibility:hidden;
错误
display:none;
- 使用 class 代替多个 style
改 class 可一次性改变节点的所有属性,而不要一个个去改 style
正确
ele.className = "replaceclass";
ele.setAttribute("class","replaceclass");
ele.className += 'addclass';
ele.removeClass("oldclass");
错误
ele.style.height = '100px';
ele.style.textAlign = 'center'
ele.style['text-align'] = 'center'
ele.setAttribute('height',100) //只用于个别属性
ele.setAttribute('style', 'height: 100px !important');
ele.style.setProperty('height', '100px', 'important');//更适合强制场景
- 使用 documentFragment 代替多个 DOM 操作
新建对象;
var fragment = document.createDocumentFragment();
逐一填入;
for (let i = 0; i < 1000; i++) {
var li = document.createElement("li");
li.innerHTML = "apple" + i;
fragment.appendChild(li);
}
一次性进行DOM操作;
document.getElementById("fruit").appendChild(fragment);
- 避免使用 table 布局
table 及其内部元素,可能需要多次计算才能确定节点的属性值,比同等元素多花两倍时间,而一个小改动都可能造成整个重新布局
- 避免设置多层样式
div > a > span{ },这样要做 3 层判断,递归过程复杂,要尽量避免过于具体的选择器
- 避免使用 css 表达式
表达式可能会多次计算,引发重排
- 避免频繁读取引发重排的属性
多次使用的话,最好用一个变量先缓存下来
-
尽可能把动画效果放到 absolute 或 fixed 的元素上
-
尽可能把频繁重排的节点设置成图层
图层可以阻止节点的渲染影响别的节点,比如 will-change,video,iframe 等标签,浏览器会自动把这些标签节点变为图层
- 尽可能先隐藏操作再统一显示
进行多种页面操作前,先display:none全部隐藏,操作完成后,再display:block统一显示,这样只会触发两次重排
- 尽可能在 DOM 树最末端改变 class
这样能限制重排范围,减小影响
- 尽可能开启 css3 硬件加速
开启后,对于transform,opacity,filters等动画属性不会引起重排