1.网页的解析过程
2. 浏览器渲染流程
3.回流和重绘解析
4.合成和性能优化
5.defer和async属性
1.网页的解析过程
image.png输入网址,DNS解析,返回ip(服务器地址),一般情况下,服务器会给我们返回一个index.html的网页。
浏览器解析html页面,如果遇到css的话,会去服务器下载css文件;遇到script标签的话,会加载和执行对应的js代码;
把这些东西都下载下来之后,接下来浏览器的内核和js引擎会对css,js,html进行相关的操作;
然后,一个网页下载下来后,就是由我们的渲染引擎来帮助我们解析的。
1.1 浏览器内核和js引擎
1.1.1 浏览器内核
Rendering Engine,排版引擎,页面渲染引擎。一般习惯将之称为“浏览器内核”,主要功能是解析HTML/CSS进行渲染页面,渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。
1、 IE浏览器内核:Trident内核,也是俗称的IE内核;
2、Chrome浏览器内核:Blink内核;
3、Firefox浏览器内核:Gecko内核,俗称Firefox内核;
4、Safari浏览器内核:Webkit内核;
1.1.2 js引擎
js引擎是解析,执行js代码。
chrom,node:v8
webkit:JavaScriptCore
2. 渲染引擎如何解析页面
2.PNG1.HTML解析过程
image.png先下载index.html,所有的解析过程都是通过index.html开始的。
浏览器内核里面有个HTML Parser;它通过Parser将html转换成dom树,
2.生成css规则
image.pngcss也会被css Parser进行解析,解析成css规则(display:xxx;color:xxx);
首先,如果是head里面写的style样式,那么就不用下载了,就直接一个单独的线程帮助我们解析css,生成css规则
遇到link的时候,浏览器会使用一个独立的线程下载对应的css文件,(下载css也不会影响dom解析)把
css文件下载下来之后,也会有一个单独的线程会帮助我们解析css规则。下载css是不会阻塞dom解析的,你html Parser该生成dom树生成dom树,继续解析html。
但是需要注意的是如果html parser已经解析完html,生成好了dom tree,但是这个时候css parser并没有生成好style rules(规则树 css om /css object model),那么这个时候没办法生成render tree。webkit的话,这个时候html parser会等到sytle rules生成,等两个都有的时候,我在生成render tree。
总结就是解析css并不影响dom tree的生成,但是影响render tree的生成。
3.构建Render Tree
image.png就是css规则(cssom)和dom树结合到一起变成attachment,然后就生成了render tree渲染树;
5.布局和绘制
image.png- render tree只是告诉我们要显示什么节点,节点里面有什么样式,但是不会有节点的位置,大小信息,尺寸之类的。
布局就是确认render tree上面的所有节点的宽度,高度和位置信息。
有了layout之后,我们可以确认每个节点的里面的样式,里面的内容位置信息,大小信息。 - 一旦有了布局layout,我们就可以绘制了paint。
paint就是将每个box盒子转换成屏幕上实际的像素点。
包括文本,颜色,边框,阴影,替换元素等。
3.回流和重绘
3.1回流(重排)
1.理解回流reflow
- 第一次确定节点的大小和位置,称之为布局(layout)。
- 之后对节点的大小、位置修改重新计算称之为回流。
2.什么情况下引起回流呢
1. 比如DOM结构发生改变(添加新的节点或者移除节点);
添加新节点或者移除节点,其他dom的位置也会发生变化
--> domtree发生了变化,所以render tree一定会变化,render tree变化了,就要重新计算layout,就是每个节点的尺寸,大小,在浏览器的位置都得重新计算。重新计算之后就得重新绘制,painting,就是转换为实际的像素点显示在屏幕上。
-
比如改变了布局(修改了width、height、padding、font-size等值)
改变字体大小改变布局是因为span元素是由文字大小撑起来的,字体变大,span这个盒子变大,就得重新绘制。
boxEl.style.height="200px"
修改某个节点的高度的话,dom 树没有发生改变,render树没有发生改变,但是节点的大小改变了,就需要重新进行layout计算,重绘。
改变文字颜色的话,是不需要做重新布局的,因为节点的内容,大小,在浏览器的位置都没有改变,不需要重新布局。
如果修改了某个东西之后,你得再次计算这些元素的布局(node 的节点大小,位置信息),不论一个节点还是多个节点,有些时候你改了一个,影响其他的节点,这个时候也得重新layout。
如果一个元素是绝对定位,脱离了标准流,改变它对其他节点没影响,但是它本身大小发生变化,也会重新layout。
总结就是对节点的大小,位置修改重新计算的,就叫做回流 -
比如窗口resize(修改了窗口的尺寸等)
比如flex布局,窗口缩小之后,一行放不下,就放到第二行,float布局也是。 -
比如调用getComputedStyle方法获取尺寸、位置信息;这个取决于浏览器。
获取颜色信息不会,但是获取尺寸的话,就会重新construct frames构建frame,就会重新layout
image.png
3.2 重绘repaint
1.理解重绘
- 第一次渲染内容称之为绘制(paint)。
- 之后重新渲染称之为重绘。
2.什么情况下引起重绘呢
比如修改背景色、文字颜色、边框颜色、样式(实线变成虚线)等;
但是如果改变border-width,就会回流,重新layout,因为节点大小改变,boder-style,bordr-color就只是引起重绘。
3.3回流和重绘之间的关系
- 回流一定会引起重绘,所以回流是一件很消耗性能的事情。
- 所以在开发中要尽量避免发生回流:
3.4 如何尽量避免发生回流
◼ 1.修改样式时尽量一次性修改
比如通过cssText修改,比如通过添加class修改
比如这个样子
box.style.width="200px"
box.style.height="200px"
这个样子会引起2次回流,不过放在同一个脚本里面的话,浏览器会最后房子一起只进行一次重绘。但是依赖于浏览器,所以我们还是避免进行这个样子的操作。样式尽量一次性修改完成。比如cssText进行修改,或者动态地添加class,在class里面把css写好。
◼ 2.尽量避免频繁的操作DOM
我们可以在一个DocumentFragment或者父元素中,将要操作的DOM操作完成,再一次性的操作;
把操作放到父元素里面,操作完成之后,最后再添加到dom里面。
这也是虚拟dom提高性能的原因。
◼ 3.尽量避免通过getComputedStyle获取尺寸、位置等信息;
◼ 4.对某些元素使用position的absolute或者fixed
并不是不会引起回流,而是开销相对较小,不会对其他元素造成影响。 改变absolute或者fixed的大小之后,指会对自己的大小进行重新计算,重新layout,其他节点不需要重新计算,整体来说计算量相对小一些。
4.合成和性能优化
4.1 特殊解析-composite合成
-
绘制的过程,可以将布局后的元素会知道多个合成图层中。这是浏览器的一种优化手段。
-
默认情况下,标准流中的内容都是被绘制在同一个图层layer中的。
如果当前元素是在标准流里面的,经过各自解析,变成render tree,经过layout,生成了一种树结构,也就是layout tree。 如果这些元素都是标准流里面的,那么会生成render layer,渲染图层。
如果有个元素position:absolute,他会生成另外一个render layer。
然后它会将我们多个render layer做一个合成。 -
而一些特殊的属性,会创建一个新的合成层( CompositingLayer ),并且新的图层可以利用GPU来加速绘制;
除了标准流和固定定位之类的会被分别创建图层,然后放到一个合成层里面, 一些特殊的元素,也会创建一个新的合成层,compositing layer。
因为新的合成层的每一个单独的合成层都是单独渲染的,都是可以利用GPU来加速绘制的。
就是如果生成一份单独的合成层,原来的哪些合成词就不需要动,只需要单独改这一个合成层,并且进行渲染。
4.2 那么哪些属性可以形成新的合成层呢
- 3D transforms
- video、canvas、iframe
- opacity 动画转换时;
- position: fixed(absolute是普通的图层,不会生成 新的图层)
- will-change:一个实验性的属性,提前告诉浏览器元素可能发生哪些变化;
- animation 或 transition 设置了opacity、transform;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
#test,
#container {
height: 100px;
width: 100px;
background-color: yellow;
}
#container {
background-color: red;
/* position: fixed; 新的图层*/
/* transform: translateZ(0);新的图层*/
transition: transform 1s ease;
/* 利用transform去执行动画的时候,对浏览器来说性能会更高,因为他是在独立 的图层里面给你做动画,*/
}
#container:hover {
transform: translateX(100px); /* 不会引起回流 */
/* margin-left:100px这样的话就是在默认图层里面,效率很低 ,而且会引起回流*/
}
</style>
</head>
<body>
<div id="test">test</div>
<div id="container"></div>
</body>
</html>
4.3 缺点
分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用。
5.defer和async属性
5.1 script元素和页面解析的关系
1. js加载执行过程
- 浏览器在解析HTML的过程中,遇到了script元素是不能继续构DOM树的;
- 它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本;
- 只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树;
2. 原因
-
这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM;js代码可以直接对dom树进行操作
image.png
紫色三角形里面指的是js代码对dom进行操作。
- 如果我们等到DOM树构建完成并且渲染再执行JavaScript,修改dom, 会造成严重的回流和重绘,影响页面的性能;
- 所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树;
3.问题
- 在目前的开发模式中(比如Vue、React),脚本往往比HTML页面更“重”,处理时间需要更长;
- 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到;
注意:dom树什么时候解析完,是html元素解析完成之后才算解析完。不解析完就不会生成render树之类的,界面没有显示。
在div后面又一大堆script标签,一部分浏览器为了优化,会将这部分div先进行dom,然后render tree,layout painting,进行显示出来。但是script后面的dom元素是不会进行显示的。
4.解决方法
为了解决这个问题,script元素给我们提供了两个属性(attribute):defer和async。
5.2 defer属性
image.png- defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。 脚本会由浏览器来进行单独地下载,但是不会阻塞DOM Tree的构建过程;
如果脚本提前下载好了,也不会立即执行,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码;
DOMContentLoaded事件就是内容加载完毕,也就是dom树构建完成的时候。浏览器先去执行defer中的代码,因为defer里面的js有可能操作dom。然后再去回调DOMContentLoaded事件。 - 所以DOMContentLoaded总是会等待defer中的代码先执行完成。
- 另外多个带defer的脚本是可以保持正确的顺序执行的。
◼ 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;
◼ 注意:defer仅适用于外部脚本,对于script默认内容会被忽略。
5.2.1 举例1:不阻塞后面的东西去构建。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<!-- 加上defer以后,js文件的下载和执行,不会影响后面的DOM Tree的构建 -->
<script src="./test.js"></script>
<h1>哈哈哈哈</h1>
</body>
</html>
test.js
console.log('test')
debugger
var boxEl = document.querySelector('.box')
console.log(boxEl)
这个时候如果没有defer,打开页面,打开开发者模式,之后刷新,哈哈哈是没有加载出来的,因为debugger阻塞了dom渲染。
image.png
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script src="./test.js" defer></script>
<h1>哈哈哈哈</h1>
</body>
如果给script标签defer属性,是会出现哈哈哈的,因为defer不会影响dom的渲染。defer你自己去一边下载去了,你下载好以后等着,等我对dom树生成好以后去执行你。
image.png
5.2.2 DOMContentLoaded事件之前执行
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<!-- 加上defer以后,js文件的下载和执行,不会影响后面的DOM Tree的构建 -->
<script src="./test.js" defer></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
test.js
console.log('test')
var boxEl = document.querySelector('.box')
console.log(boxEl)
打印结果
test
test.js:3 <h1 class="box">哈哈哈哈</h1>
index.html:18 DOMContentLoaded
defer是在DOMContentLoaded事件执行的。在defer中是可以操作dom的,因为dom树已经构建完成了。
总结1:加上defer以后,js文件的下载和执行,不会影响后面的DOM Tree的构建
总结2: 在defer中是可以操作dom的,因为dom树已经构建完成了。
总结3:defer代码是在DOMContentLoaded事件触发之前执行的。
总结4:另外多个带defer的脚本是可以保持正确的顺序执行的
test.js
console.log('------------------------------')
console.log('i am test js')
var boxEl = document.querySelector('.box')
console.log(boxEl)
console.log('set message to "test message"')
var message = 'test message'
console.log('------------------------------')
demo.js
console.log('------------------------------')
console.log('i am demo js')
console.log(message)
console.log('------------------------------')
index.html
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<!-- 加上defer以后,js文件的下载和执行,不会影响后面的DOM Tree的构建 -->
<script src="./test.js" defer></script>
<script src="./demo.js" defer></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
test.js一定是在demo之前执行的,就是意味着demo可以使用test中的变量
------------------------------
test.js:2 i am test js
test.js:4 <h1 class="box">哈哈哈哈</h1>
test.js:5 set message to "test message"
test.js:7 ------------------------------
demo.js:1 ------------------------------
demo.js:2 i am demo js
demo.js:3 test message
demo.js:4 ------------------------------
index.html:19 DOMContentLoaded
从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中;
提高页面性能是因为使用defer不会阻塞dom tree的构建 。
一般把defer放到head里面,提前告诉浏览器,让浏览器先去下载,domtree构建,比起放到最后让他下载会比较提高速度。dom tree构建完了,你才去下载,浏览器还是要等。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="./test.js" defer></script>
<script src="./demo.js" defer></script>
</head>
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
</html>
这么加是没有意义的。
defer仅适用于外部脚本,对于script默认内容会被忽略。
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script defer>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
5.3 async属性
image.png- async 特性与 defer 有些类似,它也能够让脚本不阻塞页面.
举例:
test.js
console.log('------------------------------')
console.log('i am test js')
debugger
var boxEl = document.querySelector('.box')
console.log(boxEl)
console.log('set message to "test message"')
var message = 'test message'
console.log('------------------------------')
index.html
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script src="./test.js" async></script>
<script defer>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
然后打开页面,打开开发者工具,刷新页面,发现哈哈哈也是显示的。
image.png
DOMContentLoaded
test.js:1 ------------------------------
test.js:2 i am test js
打印结果
- async是让一个脚本完全独立的
- async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;
- async不会能保证在DOMContentLoaded之前或者之后执行;
它只要下载下来之后就立刻马上执行,不用等到dom树构建完成以后才执行。
所以它用起来比较危险,1. 它不能随意的操作dom,有可能它下载下来执行的时候dom树构建完成,也有可能下载完成执行的时候dom树还没构建完成。这时候拿到的就是null。如果想使用,就判断一下是不是null,不是的话再使用。
- 而且他也不能保证其他async脚本的顺序。
<body>
<div id="app">app</div>
<div id="title">title</div>
<div id="nav">nav</div>
<div id="product">product</div>
<script src="./test.js" async></script>
<script src="./demo.js" async></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
console.log('DOMContentLoaded')
})
</script>
<h1 class="box">哈哈哈哈</h1>
</body>
这里面的test和demo两个js文件,并不能保证谁先下载执行完。
这个代码里面,
DOMContentLoaded和demo,test谁先执行是不知道的,没有顺序。
- defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
- async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;
6.浏览器对渲染内容进行优化
浏览器对标准进行改进,对渲染内容进行优化,不会等到整个html标签解析完成以后再去构建dom tree,再render tree等等进行显示。
比如说是一个script里面有代码,写了个debuuger,打开页面,打开开发者工具,执行,这个时候,script阻塞了dom树的生成,页面应该啥也没有,但是script以前的dom元素是可以看见的,这是因为浏览器对渲染内容做了优化。
渲染引擎会力求尽快地将内容显示再屏幕上,它不必等到整个html文档解析完毕之后,就开始构建render tree和layout,painting。 这也就是script前面的dom可以看见的原因。但是script阻塞dom树渲染,所以script后面的哈哈哈是看不见的。
标准是标准,浏览器怎么实现标准就不一定了。
网友评论