html页面的渲染过程(前端处理数据渲染页面的方式)
页面的渲染有以下特点:
单线程事件轮询
定义明确、连续、操作有序(HTML5)
分词和构建DOM树
请求资源并预加载
构建渲染树并绘制页面
具体来说:
当我们从网络上得到HTML的相应字节时,DOM树就开始构建了。由浏览器更新UI的线程负责。当遇到以下情况时,DOM树的构建会被阻塞:
HTML的响应流被阻塞在了网络中
有未加载完的脚本
遇到了script节点,但是此时还有未加载完的样式文件
渲染树构建自DOM树,并且会被样式文件阻塞。 由于是基于单线程的事件轮询,即使没有脚本和样式的阻塞,当这些脚本或样式被解析、执行并且应用的时候,也会阻塞页面的渲染。
一些不会阻塞页面渲染的情况:
定义的defer属性和async属性的
defer相当于 window.onLoad,等页面DOM解析完了,执行这个 js 文件的内容
没有匹配的媒体类型的样式文件
没有通过解析器插入script节点或样式节点
1 <html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script> 6 document.write('<script src="other.js"></scr' + 'ipt>'); 7 </script> 8 <div>Hi again!</div> 9 <script src="last.js"></script> 10 </body> 11 </html>复制代码
让我们用慢镜头回放的方式来看看它究竟是怎么渲染的
<html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script>复制代码
首先,解析器遇到了a.css,并将它从网络中下载下来。下载样式表的过程是耗时的,但是解析器并没有被阻塞,继续往下解析。接下来,解析器遇到script标签,但是由于样式文件没有加载下来,阻塞了该脚本的执行。解析器被阻塞住,不能继续往下解析。
渲染树也会被样式文件阻塞,所以这时候没有浏览器不会去渲染页面。
接下来,继续。。。
<html> <body> <link rel="stylesheet" href="a.css"> <div>Hi there!</div> <script> document.write('<script src="other.js"></scr' + 'ipt>'); </script> 复制代码
一旦a.css文件加载完成,渲染树也就被构建好了。
内联的脚本执行完之后,解析器就会立即被other.js阻塞住。一旦解析器被阻塞,浏览器就会收到绘制请求,"Hi there!"也就显示在了页面上。
当other.js加载完成之后,解析器继续向下解析。。。
<html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script> 6 document.write('<script src="other.js"></scr' + 'ipt>'); 7 </script> 8 <div>Hi again!</div> 9 <script src="last.js"></script>复制代码
解析器遇到last.js之后会被阻塞,然后浏览器收到了另一个绘制请求,"Hi again!"就显示在了页面上。最后last.js会被加载,并且会被执行。
但是,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载(speculative loading)。
在上面这种情况下,脚本和样式文件会严重阻塞页面的渲染。猜测预加载的目的就是减少这种阻塞时间。当渲染被阻塞的时候,它会做以下一些事:
轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描
查找那些将来可能能够用到的资源文件的url
在渲染器使用它们之前将其下载下来
回过来再看上面的例子,通过猜测预加载这种方式是怎么工作的。
<html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script>复制代码
解析器返现了a.css,并从网络获取,解析器没有被阻塞,继续解析,当遇到了内联的script节点时,被阻塞住,由于样式文件没有加载完成,阻塞了脚本的执行。渲染树同样也被样式文件阻塞住,所以浏览器没有收到渲染请求,看不到任何东西。到目前为止,和刚才提到的那种方式是一样的。但是接下来就有变化了。
预加载器(Speculative loader)继续“阅读”文档,发现了last.js并试图加载它,(注:此时,如果a.css没有加载下来,last.js是不会加载的,一直处于pending状态)。接下来:
<html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script> 6 document.write('<script src="other.js"></scr' + 'ipt>'); 7 </script>复制代码
一旦a.css文件加载完成,渲染树也就完成了构建,内联的脚本也可以执行,之后解析器又被other.js阻塞住。解析器被阻塞住之后,浏览器会收到第一个渲染请求,“Hi there!” 会被现实在页面上。这个步骤和刚才那种情况是一致的。然后:
<html> 2 <body> 3 <link rel="stylesheet" href="a.css"> 4 <div>Hi there!</div> 5 <script> 6 document.write('<script src="other.js"></scr' + 'ipt>'); 7 </script> 8 <div>Hi again!</div> 9 <script src="last.js"></script>复制代码
解析器发现了last.js,但是由于预加载器刚才已经把它给加载下来了,放在了浏览器的缓存里,所以last.js会被立即执行。之后,浏览器会收到渲染请求“Hi again”也被显示在了页面上
重排、重绘、
从字面意思来看重排就是重新排列,而重绘就是重新绘制。很显然既然是重新排列,自然就要重新绘。重排和重绘都是是根据dom
元素来说的?其实并不完全是。而是render tree
的节点
将HTML
构建成一个DOM
树(DOM = Document Object Model
文档对象模型),DOM
树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。
将CSS
解析成CSS
去构造CSSOM
树( CSSOM
= CSS Object Model CSS
对象模型)
根据DOM
树和CSSOM
来构造 Rendering Tree
(渲染树)。注意:Rendering Tree
渲染树并不等同于 DOM
树,因为一些像 display:none 的东西就没必要放在渲染树中了。
“display:none”的元素是将节点从整个render tree中移除,所以不是布局中的一部分
有了Render Tree
,浏览器已经能知道网页中有哪些节点、各个节点的CSS
定义以及他们的从属关系。
再来就是Layout
,顾名思义就是计算出每个节点在屏幕中的位置 layout render tree
。
而后就是绘制,即遍历render
树,并使用浏览器UI
后端层绘制每个节点。
在整个过程中,javascript
可能会改变dom
树和cssom
的结构,所以浏览器往往会等待js的加载,因此搁置整个渲染数的构建。
不同浏览器对布局和绘制的顺序可以是不同的,根据不同的浏览器优化策略,浏览器能够更好的协调布局和绘制过程。
而我们说重绘和重排,就是重新去布局的过程和重新绘制的过程。
渲染树的变更会导致重排,而渲染数的节点的属性变化会导致重绘。
重排:DOM的变化影响到了元素的宽高,导致浏览器要重新计算元素的宽高(影响到页面布局),甚至影响到渲染数中的某些部分就需要重新渲染。改变窗口大小、文字大小、内容变化、浏览器窗口大小、style属性的改变等会导致重排。 重排一定会导致重绘。重绘不一定导致重排。 重绘:一个元素的外观发生了改变,但是没有改变元素的宽高,比如改变元素的背景色、outline、visibility等会导致重绘
重排必定会影响重绘,但重绘不一定会引起重排
优化
浏览器自己的优化:浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
我们要注意的优化:我们要减少重绘和重排就是要减少对渲染树的操作,则我们可以合并多次的DOM和样式的修改。并减少对style样式的请求。
//例如myElement元素沿对角线移动,每次移动一个像素。到500*500像素的位置结束。timeout循环体中可以这么做 myElement.style.left = 1 + myElement.offsetLeft + 'px'; myElement.style.top = 1 + myElement.offsetTop + 'px'; if(myElement.offsetLeft >= 500){ stopAnimation(); } //显然这种方法低效,每次移动都要查询偏移量,导致浏览器刷新渲染队列而不利于优化。好的办法是获取一次起始位置的值,然后赋值给一个变量。如下 var current = myElement.offsetLeft; current++; myElement.style.left = current + 'px'; myElement.style.top = current + 'px'; if(myElement.offsetLeft >= 500){ stopAnimation(); }复制代码
阻塞
使用cloneNode(true or false) 和 replaceChild 技术,引发一次回流和重绘
将需要多次重排的元素,position属性设为absolute或fixed,元素脱离了文档流,它的变化不会影响到其他元素;
如果需要创建多个DOM节点,可以使用DocumentFragment创建完后一次性的加入document;
尽量不要使用table布局。
直接改变元素的className
display:none;先设置元素为display:none;然后进行页面布局等操作;设置完成后将元素设置为display:block;这样的话就只引发两次重绘和重排;
不要经常访问浏览器的flush队列属性;如果一定要访问,可以利用缓存。将访问的值存储起来,接下来使用就不会再引发回流;
从上面的过程,我们知道:dom
节点是边加载边渲染的。同时css资源和js资源也是同时在加载,文档边渲染的。
由此就会带来一些阻塞问题,也就是用户看到白屏。比如:浏览器不知道xx.js中执行了哪些脚本,会对页面造成什么影响,所以浏览器会等js文件下载并执行完成后才继续渲染,如果这个时间过长,会白屏。
如果dom
节点中涉及到资源加载会影响到dom
的渲染。像js
加载和执行会影响dom
树的解析。 css
加载会影响到cssom
的形成,但是不影响dom
树的形成。所以js资源我们会放到文档末尾的原因,而样式是放在前面。
不管是js资源也好还是css
资源也好,我们都要求尽量压缩和小,也是同一个道理。
CSS阻塞DOM渲染
无论是外链CSS还是内联CSS都会阻塞DOM渲染(Rendering),然而DOM解析(Parsing)会正常进行。 这意味着在CSS下载并解析结束之前,它后面的HTML都不会显示。 这也是为什么我们把样式放在HTML内容之前,以防止被呈现内容发生样式跳动。 当然代价就是显示延迟,所以性能攸关的站点都会内联所有CSS。CSS本来是可以并行下载的,在什么情况下会出现阻塞加载了(在测试观察中,IE6下CSS都是阻塞加载)
当CSS后面跟着嵌入的JS的时候,该CSS就会出现阻塞后面资源下载的情况。而当把嵌入JS放到CSS前面,就不会出现阻塞的情况了。
根本原因:因为浏览器会维持html中css和js的顺序,样式表必须在嵌入的JS执行前先加载、解析完。而嵌入的JS会阻塞后面的资源加载,所以就会出现上面CSS阻塞下载的情况。
嵌入JS应该放在什么位置? 1、放在底部,虽然放在底部照样会阻塞所有呈现,但不会阻塞资源下载。 2、如果嵌入JS放在head中,请把嵌入JS放在CSS头部。 3、使用defer(只支持IE)anysc 4、不要在嵌入的JS中调用运行时间较长的函数,如果一定要用,可以用setTimeout
来调用
JS 阻塞 DOM 解析
不论是内联还是外链JavaScript都会阻塞后续DOM解析(Parsing)后续的 DOM 渲染(Rendering)也会被阻塞。 这意味着脚本执行过程中插入的元素会先于后续的 HTML 展现,即使脚本是外链资源也是如此。 由于 JavaScript 只会阻塞后续的 DOM,前面的 DOM 在解析完成后会被立即渲染给用户。 这也是为什么我们把脚本放在页面底部:脚本仍在下载时页面已经可以正常地显示了。
解决方法
延时加载
如果页面初始的渲染并不依赖于js或者CSS可以用推迟加载,就是最后在加载js和css,把引用外部文件的代码写在最后。比如一些按钮的点击事件,比如轮播图动画的脚本也可以放在最后。
defer 延时加载
在文档解析完成开始执行,并且在事件之前执行完成,会按照他们在文档出现的顺序去下载解析。效果和把script放在文档最后之前是一样的。
异步加载
就是告诉浏览器不必等到加载完外部文件,可以边渲染边下载,什么时候下载完成什么时候执行。
作者:为了认识你
链接:https://juejin.cn/post/7031482791221182495