浏览器的渲染原理

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.PNG

1.HTML解析过程

image.png

先下载index.html,所有的解析过程都是通过index.html开始的。
浏览器内核里面有个HTML Parser;它通过Parser将html转换成dom树,

2.生成css规则

image.png

css也会被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

  1. 第一次确定节点的大小和位置,称之为布局(layout)。
  2. 之后对节点的大小、位置修改重新计算称之为回流。

2.什么情况下引起回流呢

1. 比如DOM结构发生改变(添加新的节点或者移除节点);
添加新节点或者移除节点,其他dom的位置也会发生变化
--> domtree发生了变化,所以render tree一定会变化,render tree变化了,就要重新计算layout,就是每个节点的尺寸,大小,在浏览器的位置都得重新计算。重新计算之后就得重新绘制,painting,就是转换为实际的像素点显示在屏幕上。

  1. 比如改变了布局(修改了width、height、padding、font-size等值)
    改变字体大小改变布局是因为span元素是由文字大小撑起来的,字体变大,span这个盒子变大,就得重新绘制。
    boxEl.style.height="200px"
    修改某个节点的高度的话,dom 树没有发生改变,render树没有发生改变,但是节点的大小改变了,就需要重新进行layout计算,重绘。
    改变文字颜色的话,是不需要做重新布局的,因为节点的内容,大小,在浏览器的位置都没有改变,不需要重新布局。
    如果修改了某个东西之后,你得再次计算这些元素的布局(node 的节点大小,位置信息),不论一个节点还是多个节点,有些时候你改了一个,影响其他的节点,这个时候也得重新layout。
    如果一个元素是绝对定位,脱离了标准流,改变它对其他节点没影响,但是它本身大小发生变化,也会重新layout。
    总结就是对节点的大小,位置修改重新计算的,就叫做回流

  2. 比如窗口resize(修改了窗口的尺寸等)
    比如flex布局,窗口缩小之后,一行放不下,就放到第二行,float布局也是。

  3. 比如调用getComputedStyle方法获取尺寸、位置信息;这个取决于浏览器。
    获取颜色信息不会,但是获取尺寸的话,就会重新construct frames构建frame,就会重新layout


    image.png

3.2 重绘repaint

1.理解重绘

  1. 第一次渲染内容称之为绘制(paint)。
  2. 之后重新渲染称之为重绘。

2.什么情况下引起重绘呢

比如修改背景色、文字颜色、边框颜色、样式(实线变成虚线)等;
但是如果改变border-width,就会回流,重新layout,因为节点大小改变,boder-style,bordr-color就只是引起重绘。

3.3回流和重绘之间的关系

  1. 回流一定会引起重绘,所以回流是一件很消耗性能的事情。
  2. 所以在开发中要尽量避免发生回流:

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合成

  1. 绘制的过程,可以将布局后的元素会知道多个合成图层中。这是浏览器的一种优化手段。

  2. 默认情况下,标准流中的内容都是被绘制在同一个图层layer中的。
    如果当前元素是在标准流里面的,经过各自解析,变成render tree,经过layout,生成了一种树结构,也就是layout tree。 如果这些元素都是标准流里面的,那么会生成render layer,渲染图层。
    如果有个元素position:absolute,他会生成另外一个render layer。
    然后它会将我们多个render layer做一个合成。

  3. 而一些特殊的属性,会创建一个新的合成层( 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
  1. defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree。 脚本会由浏览器来进行单独地下载,但是不会阻塞DOM Tree的构建过程;
    如果脚本提前下载好了,也不会立即执行,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码;
    DOMContentLoaded事件就是内容加载完毕,也就是dom树构建完成的时候。浏览器先去执行defer中的代码,因为defer里面的js有可能操作dom。然后再去回调DOMContentLoaded事件。
  2. 所以DOMContentLoaded总是会等待defer中的代码先执行完成。
  3. 另外多个带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
  1. 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

打印结果

  1. async是让一个脚本完全独立的
  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本;
  • async不会能保证在DOMContentLoaded之前或者之后执行;
    它只要下载下来之后就立刻马上执行,不用等到dom树构建完成以后才执行。
    所以它用起来比较危险,1. 它不能随意的操作dom,有可能它下载下来执行的时候dom树构建完成,也有可能下载完成执行的时候dom树还没构建完成。这时候拿到的就是null。如果想使用,就判断一下是不是null,不是的话再使用。
  1. 而且他也不能保证其他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谁先执行是不知道的,没有顺序。

  1. defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的;
  2. async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的;

6.浏览器对渲染内容进行优化

浏览器对标准进行改进,对渲染内容进行优化,不会等到整个html标签解析完成以后再去构建dom tree,再render tree等等进行显示。
比如说是一个script里面有代码,写了个debuuger,打开页面,打开开发者工具,执行,这个时候,script阻塞了dom树的生成,页面应该啥也没有,但是script以前的dom元素是可以看见的,这是因为浏览器对渲染内容做了优化。
渲染引擎会力求尽快地将内容显示再屏幕上,它不必等到整个html文档解析完毕之后,就开始构建render tree和layout,painting。 这也就是script前面的dom可以看见的原因。但是script阻塞dom树渲染,所以script后面的哈哈哈是看不见的。
标准是标准,浏览器怎么实现标准就不一定了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容