在我们正式开始学习之前,首先让我们了解一些浏览器的一些知识点,然后再对浏览器渲染原理深刻的理解一下,这次学习是针对Chrome浏览器的渲染机制。
最新的Chrome浏览器包括:1个浏览器主进程,一个GPU进程,一个网络进程,多个渲染进程和多个插件进程。
浏览器进程:主要负责界面显示、用户交互、子进程管理、同时提供存储等功能
渲染进程:核心任务将HTML、CSS和JavaScript转化为用户可以交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该线程。默认情况下,Chrome浏览器为每个标签页创建一个渲染进程,但从一个页面打开另一个页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染流程。
GPU进程:GPU的初衷是为了使用3D CSS效果,随后网页、Chrome的UI界面都采用了GPU来绘制,使得GPU成为浏览器普遍的需求,Chrome在其多进程架构上也引入了GPU进程。
网络进程:主要负责页面的网络资源加载。
插件进程:主要负责插件的运行,因插件容易崩溃,所以需要通过插件进程来隔离,保证插件进程崩溃不会对浏览器和页面造成影响。
1. 构建DOM树
服务器响应浏览器的HTML请求后,浏览器进程开始准备渲染流程,浏览器进程解析HTML文件,构建DOM树。
2. 样式计算
CSS解析
CSS样式来源主要有三种:
-
通过link或@import引入外部CSS
-
标记内的CSS
-
元素style的属性内嵌的CSS
解析CSS的顺序是浏览器样式>用户自定义样式>页面link标签等引进来的样式>style标签的内联样式
浏览器的渲染引擎接收到CSS文本时,将CSS文本转换为浏览器可以理解的结构styleSheets(CSSOM)
3. 布局阶段
- 创建布局树
DOM树中的所有可见节点,并把这些节点加到布局中
忽略不可见节点,比如:head标签,具有display:node样式的元素
- 布局计算
根据布局树对各节点的几何坐标位置进行计算,输出带有坐标位置的布局树
4. 分层(layer)
在生成布局树之后不能直接绘制,渲染进程会将一些复杂的3D动画,滚动条,z-index层级高的生成图层,并生成一颗图层树交给GPU加速渲染。
可以通过chrome开发工具,选择more tool下的layers标签,来查看网页的图层状态。
拥有层叠上下文对象的元素会被提升为单独的一层。
文档中层叠上下文满足以下任意一个条件的元素形成:
-
根元素(html)
-
z-index值不为auto的绝对/相对定位元素
-
fixed/sticky定位
-
z-index值不为auto的flex子项
-
opacity属性值小于1的元素
-
transform、filter、perspective、clip-path、mask、mask-image、mask-border属性值不为none的元素
-
isolation属性被设置为isolate的元素
-
-webkit-overflow-scrolling属性值为touch的元素
-
在will-change中指定任意CSS属性
-
contain属性为layout、paint或者综合值(如:strict、content)
在层叠上下文中,子元素同样按照这个规则进行层叠。重要的是,子级层叠上下文的z-idnex值只在父级中有意义,子级层叠上下文被自动视为父级层叠上下文的一个独立单元。
需要剪裁(clip)的地方会被创建图层
当子元素需要展示的区域大于父元素的大小时,就会需要剪裁。出现剪裁情况的时候,渲染引擎回味文字部分单独擦护功能键一个层,如果出现滚动条,滚动条也会被提升为单独的层。
下面看这么一个案例:
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />div {width: 200px;height: 200px;overflow: auto;background: gray;}Document 我是子元素一:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长 我是子元素二:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长 我是子元素三:我很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长 |
|---|
页面渲染效果:
浏览器分层效果:
总结:元素有了层叠上下文属性或者需要被剪裁,满足任意一点,就会提升为单独一层。
5. 图层绘制:
渲染引擎会把一个图层的绘制拆分成很多小绘制指令,然后把这些指令按照顺序组成待绘制列表
6.栅格化(光栅化):
栅格化的过程就是将图块转化成位图的过程。图块是栅格化的最小单位(通常为256×256或者512×512)。合成线程会按照视口附近的图块优先生成位图的原则将图块生成位图,而实际生成位图的操作是由栅格化来执行的。渲染过程中借用GPU来加速生成,最后将生成的位图保存到GPU内存中。
7. 合成:
图块被栅格化后完成后,合成线程会生成绘制图块的指令-DrawQuad并提交给浏览器进程。浏览器进程viz组件,用来接收DrawQuad指令,然后将页面内容会知道内存中,最后将内存数据显示在屏幕上。
8.总结
完整的渲染流程大致总结如下:
1.渲染引擎将HTML内容转换为DOM树结构
2.渲染引擎将CSS样式表转化为浏览器理解的styleSheets,计算DOM节点样式
3.创建布局树,计算元素的布局信息
4.对布局树进行分层,生成分层树
5.对每个图层生成绘制列表,并将其提交到合成线程
6.合成线程将图层分成图块,并在栅格化线程池中将图块转换成位图
7.合成线程发送绘制图块命令DrawQuad给浏览器进程
8.浏览器进程根据DrawQuad生成页面,显示在显示器上
CSS解析和JS解析
- CSS不会阻塞DOM解析,但会阻塞页面渲染
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />div {width: 100px;height: 100px;background-color: red;position: absolute;}<link rel="stylesheet" href="./index.css" /><script src="./index.js" defer>//index.jsconst div = document.querySelector("div");console.log(div);//index.cssdiv {width: 200px;height: 200px;background: gray;}//server.jsconst express = require("express");const fs = require("fs");const app = express();app.get("/", (req, res) => {fs.readFile("./index.html", (err, data) => {res.send(data.toString());});});app.get("/index.css", (req, res) => {res.set("Content-Type", "text/css");fs.readFile("./index.css", (err, data) => {console.log(data);setTimeout(() => {res.send(data.toString());}, 3000);});});app.get("/index.js", (req, res) => {fs.readFile("./index.js", (err, data) => {res.send(data.toString());});});app.listen(8080, (err) => {if (err) {console.log(err);} else {console.log("服务器启动成功");}}); |
|---|
script标签上的defer属性是通知浏览器该脚本在文档完成解析后执行,这样在DOM解析后马上能打印出来div。
通过结果看的出来,css文件内容会延迟3秒后响应,通过效果看出来的是控制台先打印的div标签,3秒后div蓝色背景样式出来,这就证明了css是不会阻塞DOM解析的。
CSS解析不会阻塞DOM解析,那我们认为的应该是div背景色的一个改变,从红色变为灰色,但是效果是仅仅呈现了最终结果灰色,没有变色这个过程,因此CSS是阻塞页面渲染的。
- JS阻塞DOM解析,浏览器遇到
当把scrpit标签的defer属性删除掉,我们延迟加载js文件,发现我们打印的结果是null,随后页面才会出现效果,这个效果说明JS阻塞了DOM解析。
现代浏览器会先提前下载script,link,src等标签的资源,不会等解析到标签的时候才下载。
当我们把script标签和link标签放在div标签下面加载的时候,发现页面会先展示红色然后变成灰色的一个过程,浏览器在遇到script标签的时候渲染了一次页面,加载完css文件后,由重新渲染了一次。这是因为浏览器不知道脚本内容,因此碰到脚本内容时,先渲染一遍页面,确保脚本获取到最新的DOM信息。
总结:
-
CSS不会阻塞DOM解析,但会阻塞DOM渲染
-
JS阻塞DOM解析,但浏览器会预先下载相关资源
-
浏览器遇到scirpt且没有defer或async属性的时,会触发页面渲染,如果前面CSS资源未加载完毕,浏览器会等待加载完成后执行脚本
DOM的重绘和回流(Repaint&Reflow)
-
重绘:元素样式的改变(但宽高、大小、位置等不变)如:outline,visibility,color等
-
回流(重排):元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染。如:添加或删除可见的DOM元素;元素的位置、尺寸发生变化,内容发生变化(文本变化或图片被另一个尺寸图片替代)
注意:回流一定会触发重绘,重绘不一定发生回流,回流比重绘会更消耗性能,他们会导致web应用的UI反映迟钝。
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />Document#box {width: 100px;height: 100px;background-color: red;position: absolute;}<div id="box">//批量处理(样式集中改变)box.style.cssText="width:20px,height:20px"box.className="box"//缓存布局信息因为有读操作box.clientWidth和box.clientHeight 会触发两次回流box.style.width = box.clientWidth+10+'px'box.style.heght = box.clientHeight+10+'px'实现(本质:分离读写):a=box.clientWidth b=box.clientHeightbox.style.width = a+10+'px'box.style.heght = b+10+'px'//元素批量修改1.文档碎片let frg = document.createDocumentFragment()for (let i = 0; i < 5; i++) {let newLi = document.createElement("li");newLi.innerHTML = i;//box.appendChild(frg); 触发5次回流frg.appendChild(newLi);}// 一次性把内容放到容器中,触发一次回流box.appendChild(frg);frg = null;2. 字符串拼接let str=''for (let i = 0; i < 5; i++) {str+= ' |
|---|
我们可以通过以下几种方式来避免DOM的回流
-
放弃传统操作DOM,基于vue、react数据影响视图模数
-
分离读写操作(现代浏览器都有渲染队列的机制)
-
样式集中改变
-
缓存布局信息
-
元素批量修改
-
动画效果应用到position属性为absolute或fixed的元素(脱离文档流)
-
CSS3硬件加速
-
牺牲平滑度换取速度
-
避免table布局
想了解更多精彩内容,快来关注尚硅谷教育