当前位置: 首页 > Web前端 > CSS

浏览器渲染机制

时间:2023-03-30 16:53:23 CSS

本文示例源码请戳github博客,建议大家手敲代码。前言浏览器渲染一个页面的过程从耗时的角度来看,浏览器请求、加载、渲染一个页面,时间花在了以下五件事上:DNS查询TCP连接HTTP请求就是响应serverresponseclientrendering本文讨论第五部分,即浏览器对内容的渲染,这部分(渲染树构建、布局和绘制),可以分为以下五个步骤:处理HTML标签和构建一个DOM树。处理CSS标记并构建CSSOM树将DOM和CSSOM合并到一个渲染树中。根据渲染树布局计算每个节点的几何信息。将单个节点绘制到屏幕上。需要理解的是,这五个步骤不必一次按顺序完成。如果修改了DOM或者CSSOM,就需要重复上面的过程,这样它才能计算出哪些像素点需要重新渲染到屏幕上。在实际页面中,CSS和JavaScript经常会多次修改DOM和CSSOM。1.浏览器的线程在详细讲解之前先了解一下浏览器的线程。这将帮助我们理解接下来的内容。浏览器是多线程的,它们在内核控制下相互协作以保持同步。浏览器至少实现了三个常驻线程:JavaScript引擎线程、GUI渲染线程和浏览器事件触发线程。GUI渲染线程:负责渲染浏览器界面的HTML元素。当界面需要重绘(Repaint)或由于某种操作导致回流(reflow)时,该线程就会执行。当Javascript引擎运行脚本时,GUI渲染线程被挂起,也就是“冻结”。JavaScript引擎线程:主要负责处理Javascript脚本程序。Timertriggersthread:浏览器计时计数器不被JavaScript引擎统计。JavaScript引擎是单线程的。如果处于线程阻塞状态,会影响计时的准确性。因此,浏览器使用单独的线程来计时和触发计时。事件触发线程:当事件触发时,线程会将事件添加到待处理队列的末尾,等待JS引擎处理。这些事件包括当前正在执行的代码块如定时任务、浏览器内核的其他线程如鼠标点击、AJAX异步请求等。由于JS的单线程关系,这些事件都需要排队等待处理JS引擎。对于定时块、ajax请求等任何异步任务,事件触发线程只有在定时时间到达或ajax请求成功后,才会将回调函数放入事件队列。异步HTTP请求线程:连接XMLHttpRequest后,通过浏览器开启一个新的线程请求。当检测到状态变化时,如果设置了回调函数,异步线程会产生一个状态变化事件,放入JavaScript引擎的处理队列中进行处理。当发起异步请求时,http请求线程负责向服务器请求。收到响应后,事件触发线程将返回函数放入事件队列。2、构建DOM树和CSSOM树浏览器从网络或硬盘上获取到HTML字节数据后,会经过一个过程将字节解析成DOM树:编码:首先将HTML的原始字节数据转换成字符文件指定的代码。Tokenization:然后浏览器会根据HTML规范将字符串转换成各种token(、等标签,标签中的字符串和属性都会被转换成token,每个token都有特殊的含义,并有一套的规则)。令牌记录标记的开始和结束。通过这个特性,很容易判断一个标签是否是子标签(假设有两个标签和,当标签的token还没有遇到它的结束token遇到标签标记,则是的子标签)。生成对象:接下来,每个token都会被转换成一个对象,定义了它的属性和规则(这个对象就是节点对象)。构造:构造DOM树,整个对象集合就像一个树状结构。可能有人会疑惑为什么DOM是树状结构。这是因为标签包含了复杂的父子关系,而树结构正好可以说明这种关系(CSSOS也是一样,层叠样式也包含父子关系。例如:divp{font-size:18px},它会先找到所有的p标签,判断其父标签是否为div,再决定是否使用该样式进行渲染)。整个DOM树的构建过程其实是:字节->字符->令牌->节点对象->对象模型。下面将通过一个示例HTML代码和一张图片来更形象地解释这个过程。关键路径

webperformance同学们好!

当上面的HTML代码遇到标签时,浏览器会发送请求获取标签中标记的CSS文件(使用内联CSS可以省略请求步骤提高速度,但不是这种速度所必需的模块化和可维护性丢失了),style.css中的内容如下:body{font-size:16px}p{font-weight:bold}span{color:red}pspan{display:none}img{float:right}浏览器获取到外部CSS文件的数据后,会像构建DOM树一样开始构建CSSOM树。这个过程没有什么特别的区别。3.构建渲染树在构建DOM树和CSSOM树之后,浏览器只剩下两个独立的对象集合。DOM树描述了文档的结构和内容,CSSOM树描述了应用于文档的样式规则。渲染一个页面,需要结合DOM树和CSSOM树,也就是渲染树。浏览器会先从DOM树的根节点开始遍历每一个可见节点(不可见节点自然不需要渲染到页面,不可见节点还包括CSS设置了display:none属性的节点,值得注意的是isvisibility:hidden属性不认为是不可见属性,它的语义是隐藏元素,但元素仍然占据布局空间,所以会被渲染成一个空盒子)对于每一个可见的节点,找到其匹配的CSS样式规则并应用。渲染树构建完成,每个节点都是一个可见的节点,包含了它的内容和对应的规则样式。4.布局和绘制CSS使用一种称为盒模型的心智模型来表示每个节点与其他元素之间的距离。盒模型包括Margin、Padding、Border、Content。页面中的每个标签实际上是一个框。布局阶段会从渲染树的根节点开始遍历,然后确定每个节点对象在页面上的确切大小和位置。布局阶段的输出是一个盒子模型,它会精确捕获屏幕内每个元素的确切位置和大小,并将所有相对测量值转换为屏幕内的绝对像素值。关键路径:Helloworld!Helloworld!
当Layout布局事件完成后,浏览器会立即发出PaintSetup和Paint事件,开始将渲染树绘制成像素。绘制所需的时间与CSS样式的复杂程度成正比。绘制完成后,用户就可以看到页面最终的渲染效果了。我们可能需要1~2秒的时间才能向一个网页发送请求并得到渲染后的页面,但是浏览器实际上已经完成了上面提到的很多工作。总结一下浏览器关键渲染路径的整个过程:处理HTML标记数据,生成DOM树。处理CSS标记数据并生成CSSOM树。将DOM树与CSSOM树合并以生成渲染树。遍历渲染树开始布局,计算每个节点的位置信息。将每个节点绘制到屏幕上。5、如何请求外部资源为了直观的观察浏览器加载渲染的细节,在本地用nodejs搭建了一个简单的HTTPServer。index.jsconsthttp=require('http');constfs=require('fs');consthostname='127.0.0.1';constport=8080;http.createServer((req,res)=>{if(req.url=='/a.js'){fs.readFile('a.js','utf-8',function(err,data){res.writeHead(200,{'Content-Type':'text/plain'});setTimeout(function(){res.write(data);res.end()},5000)})}elseif(req.url=='/b.js'){fs.readFile('b.js','utf-8',function(err,data){res.writeHead(200,{'Content-Type':'text/plain'});res.write(data);res.end()})}elseif(req.url=='/style.css'){fs.readFile('style.css','utf-8',function(err,data){res.writeHead(200,{'Content-Type':'text/css'});res.write(data);res.end()})}elseif(req.url=='/index.html'){fs.readFile('index.html','utf-8',function(err,data){res.writeHead(200,{'Content-Type':'text/html'});res.write(data);res。结尾()})}}).listen(port,hostname,()=>{console.log('Serverrunningat'+hostname+':'+port);});index.html浏览器渲染1111111

222222

3333333

style.css#header{color:red;}a.js和b.js暂时为空。可以看到服务端会延迟请求a.js5秒后返回服务器启动后,在chrome浏览器中打开http://127.0.0.1:8080/index.html。我们第一次打开chrome的debug面板解析html的时候,好像是一起请求外部资源的,说的是资源是预解析加载的,也就是说style.css和b.js是在加载的时候发起的请求a.js被阻止。看图也能说明,因为先ParseHTML遇到了阻塞,然后pre-parsed来发起请求,所以好像是一起请求的。6.如果解析了部分HTML,就会显示出来。让我们修改html代码浏览器渲染1111111

222222

3333333

由于a.js的延迟,在解析到a.js所在的script标签时,a.js还没有下载,阻塞停止解析,而之前的已解析的已被绘制并显示出来。下载并执行a.js后,继续后续分析。当然,浏览器在解析一个标签后并不会绘制并显示一次。当它遇到阻塞或耗时的操作时,它会先绘制一部分已解析的。7.js文件的位置如何影响HTML解析?7.1头部加载js文件。修改index.html:浏览器渲染1111111

222222

3333333

由于a.js的阻塞导致解析停止,页面无法显示任何内容,直到a.js下载完成。7.2.中间加载了js文件。浏览器渲染1111111

<脚本src='http://127.0.0.1:8080/b.js'>

222222

3333333

解析为js文件阻止。阻塞后续解析,导致后续无法快速显示。7.3.最后加载js文件。浏览器渲染1111111

222222

3333333

解析为一个In.js部分,页面要显示的内容已经解析完毕,a.js不会影响页面的渲染速度。从上面我们可以总结出直接导入JS会阻塞页面的渲染(GUI线程和JS线程是互斥的)。JS不会阻塞资源的加载。JS的顺序执行会阻塞后续JS逻辑的执行。我们来看看异步js7.4,async和defer的作用是什么?有什么不同?接下来我们比较一下defer和async属性的区别:蓝线代表JavaScript加载;红线代表JavaScript执行;绿线代表HTML解析。情况一没有defer或者async,浏览器会立即加载并执行指定的脚本,也就是说不等待,直接加载并执行文档元素后续加载。案例2(异步下载)async属性表示引入的JavaScript是异步执行的。与defer的区别在于,如果已经加载,就会开始执行——无论此刻HTML解析阶段也是在DOMContentLoaded之后触发的。需要注意的是,以这种方式加载的JavaScript仍然会阻塞load事件。也就是说,async-script可以在DOMContentLoaded触发之前或之后执行,但必须在load触发之前执行。案例3(延迟执行)defer属性表示引入的JavaScript延迟执行,即加载这段JavaScript时HTML不停止解析,而两个过程是并行的。在解析完整个文档并加载defer-script之后(这两件事的顺序无关紧要),将执行defer-script加载的所有JavaScript代码,然后触发DOMContentLoaded事件。defer与普通脚本相比有两个不同:加载JavaScript文件时不阻塞HTML解析,执行阶段放在HTML标签解析完成之后。加载多个JS脚本时,async是乱序加载的,而defer是顺序加载的。8、受css文件的影响,服务器会延迟style.css的响应。fs.readFile('style.css','utf-8',function(err,data){res.writeHead(200,{'Content-Type':'text/css'});setTimeout(function(){res.write(data);res.end()},5000)})浏览器渲染1111111

222222

3333333

可以看出css文件不会阻塞html的解析,但是会阻塞渲染,这样在css文件之前已经解析好的html下载完成不能先显示。我们调整css到尾部浏览器渲染1111111

222222

3333333

这是可以渲染的页面,但是没有样式。直到css加载完成,我们可以简单总结一下上面的内容。放在head的CSS会阻塞页面的渲染(页面的渲染会等到css加载完成)CSS会阻塞JS的执行(因为GUI线程和JS线程是互斥的,因为有可能JS会操作CSS)CSS不阻塞外部脚本(不阻塞JS的加载,而是阻塞JS的执行,因为浏览器会有预扫描器)参考浏览器渲染流程和性能优化谈浏览器的渲染机制你所不知道的浏览器页面渲染机制