美文网首页
浏览器的渲染原理

浏览器的渲染原理

作者: 未路过 | 来源:发表于2022-10-01 16:14 被阅读0次

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后面的哈哈哈是看不见的。
标准是标准,浏览器怎么实现标准就不一定了。

相关文章

  • 浏览器原理相关视频课程

    大话浏览器渲染原理浏览器渲染原理和打开网页机制HTTPS 网络协议原理解析从零开始,彻底掌握浏览器渲染原理深入V8...

  • 前端性能优化-渲染优化

    一、浏览器渲染原理和关键渲染路径 浏览器渲染原理:读取html,css文本,构建DOM树。(DOM ,CSSOM)...

  • 略知一二之浏览器渲染原理

    浏览器渲染原理 推荐相关阅读文章 浏览器页面渲染机制,你真的弄懂了吗? 关键渲染路径 深入浅出浏览器渲染原理 [译...

  • 浏览器渲染机制

    参考链接:浏览器渲染的那些事(一)浏览器内部工作原理浏览器的渲染原理简介有关网页渲染,每个前端开发者都该知道的那点...

  • css渲染原理

    css渲染原理 浏览器渲染原理浏览器接收到服务器返回的html页面。浏览器开始构建DOM树(DOM TREE),遇...

  • 2017前端面试题之综合篇(1)

    1 . 浏览器如何渲染? 浏览器的渲染原理简介 专题:浏览器:渲染重绘、重排两三事 浏览器加载和渲染HTML的顺序...

  • 公开课一、浏览器渲染原理 ------ 2020-03-07

    1、知识铺垫: 2、框架性叙述浏览器渲染原理: 3、根据浏览器的渲染原理我们能做的性能优化:

  • 小皆学前端—主流浏览器的内核浅析

    浏览器内核,包括浏览器渲染引擎和JS解析引擎。负责对网页的语法进行解释并渲染(渲染再我另外一篇文章浏览器的渲染原理...

  • css知识总结

    一、浏览器渲染原理 要了解浏览器渲染页面的过程,首先得知道一个名词——关键渲染路径。关键渲染路径是指浏览器从最初接...

  • 浏览器渲染引擎

    浏览器的内核中主要分为渲染引擎和 javascript 引擎,本篇主要围绕渲染引擎介绍一下浏览器的工作原理。 渲染...

网友评论

      本文标题:浏览器的渲染原理

      本文链接:https://www.haomeiwen.com/subject/xbwrartx.html