zoukankan      html  css  js  c++  java
  • 浏览器运行机制详解

    前言

     大家肯定都听说过很多浏览器优化原则吧,例如说减少DOM操作,使用transformX(0)进行硬件优化,避免js文件执行时间过长使得页面卡顿等等。大部分人可能都知道,但也仅限于知道,即知其然,不知其所以然。

     学习要形成自己的知识体系,否则的话,往往是东一榔头西一榔头地学习知识,这样导致学习到的知识松散,无法形成内在的联系,也就导致了学习地不够深入,只是浮于表面,只是“记住”了知识。

     所以,接下来,我想来为大家梳理一下浏览器运行过程中需要理解的知识,如下:

    • 前言
    • 进程与线程
    • 浏览器进程
      • 浏览器都有哪些进程
      • 浏览器内核(renderer进程)
      • html解析
      • css解析
      • render树
      • 回流与重绘
        • 什么时候会发生回流与重绘
        • 具体什么操作会引起回流
        • 如何减少回流
      • 硬件加速
        • 如何才能使用硬件加速
        • 硬件加速使用z-index
      • 浏览器页面的渲染流程
      • DOMContentLoaded和load事件
      • css堵塞情况
      • js堵塞情况
      • css和js文件应当放在html哪个位置
      • 事件循环机制
      • 宏任务和微任务
      • 导致页面无法响应的原因
      • html文件解析过程
    • 参考链接

    # 进程与线程

     可以这样理解:


     - 进程是一个工厂,每个工厂有其独立的资源。

     - 线程是工厂中的工人,可能只有一个,可能有好多个。多个工人协同完成工作。工人共享工作资源。



     回到硬件上来理解:

     - 工厂的资源 -> 系统分配的内存。

     - 工厂之间相互独立 -> 进程之间相互独立,也即进程分配到的内存相互独立,无法读到对方内存中的数据。

     - 一个工厂有一个或多个工人 -> 一个线程中有一个或多个线程。

     - 多个工人协同完成工作 -> 进程中多个线程协同完成工作。即线程之间能互相发送请求与接收结果。

     - 工人共享工作资源 -> 进程中所有线程都能访问到相同一块内存,即信息是互通的。



     不过在这里要强调一点:**一个软件不等于一个进程,一个软件可能包含有多个互相独立的进程。**

     最后,再用官方的术语描述下进程与线程的差别


    - 进程是系统资源分配的最小单位(即系统以进程为最小单位分配内存空间,同时进程是能独立运行的最小单位)

    - 线程是系统调度的最小单位(即系统以线程为单位分配cpu中的核。)
     tips:
    - 进程之间也能互相通信,不过代价比较大。

    浏览器进程

     首先,明确的是:浏览器是多线程的。

     以Chrome浏览器为例:

     大家有兴趣的话,也可以打开Chrome的任务管理器测试。由图可知,Chrome中有多个进程(每个tab页面对应一个进程,以及Browser进程,GPU进程和插件进程)。


    ## 浏览器都有哪些进程

    浏览器中的进程分别是:


    - Browser进程 : 是浏览器的主进程,负责主控,协调,只有一个,可以看做是浏览器的大脑。 - 负责下载页面的网络文件 - 负责将renderer进程得到的存在内存中的位图渲染(显示)到页面上 - 负责创建和销毁tab进程(renderer进程) - 负责与用户的交互 - GPU进程 : 只有一个。 - 负责3D绘制,只有当该页面使用了硬件加速才会使用它,来渲染(显示)页面。否则的话,不使用这个进程,而是用Browser进程来渲染(显示)页面 - renderer进程:又名浏览器内核,每个tab页面对应一个独立的renderer进程,内部有多个线程。 - 负责脚本执行,位图绘制,事件触发,任务队列轮询等 - 第三方插件进程:每种类型的插件对应一个进程。
     浏览器是多进程的好处非常明显,**如果浏览器是单线程的话,则一个页面,一个插件的崩溃会导致整个浏览器崩溃,用户体验感会非常差。**
    ## 浏览器内核(renderer进程)

     ,弄懂了这一部分的知识,那么你对一个网页的运行机制也就能有个框架了。

     renderer进程是多线程的,以下是各个线程的名称及作用(仅列举常驻线程):


    - js引擎线程: - 也称js内核,解析js脚本,执行代码 - 与GUI线程互斥,即当js引擎线程运行时,GUI线程会被挂起,当js引擎线程结束运行时,才会继续运行GUI线程 - 由一个主线程和多个web worker线程组成,由于web worker是附属于主线程,无法操作dom等,所以js还是单线程语言(在主线程运行js代码) - GUI渲染线程: - 用于解析html为DOM树,解析css为CSSOM树,布局layout,绘制paint - 当页面需要重排reflow,重绘repaint时,使用该线程 - 与js引擎线程互斥 - 事件触发线程 - 当对应事件触发(不论是WebAPIs完成事件触发,还是页面交互事件触发)时,该线程会将事件对应的回调函数放入callback queue(任务队列)中,等待js引擎线程的处理 - 定时触发线程 - 对应于setTimeout,setInterval API,由该线程来计时,当计时结束,将事件对应的回调函数放入任务队列中 - 当setTimeout的定时的时间小于4ms,一律按4ms来算 - http请求线程 - 每有一个http请求就开一个该线程 - 当检测到状态变更的话,就会产生一个状态变更事件,如果该状态变更事件对应有回调函数的话,则放入任务队列中 - 任务队列轮询线程 - 用于轮询监听任务队列,以知道任务队列是否为空
     想必大家对renderer进程里的组成及职能有个大概的认知了,接下来,我们会着重于细节来进行研究。
    ## html解析

     html解析包含有一系列的步骤,过程为Bytes -> Characters -> Tokens -> Nodes -> DOM。最终将html解析为DOM树。

     假设有一html页面,代码如下:

    <html>
      <head>
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link href="style.css" rel="stylesheet">
        <title>Critical Path</title>
      </head>
      <body>
        <p>Hello <span>web performance</span> students!</p>
        <div><img src="awesome-photo.jpg"></div>
      </body>
    </html>
    

     处理过程如下:

     最终生成的DOM树:


    ## css解析

     与html解析类似,他解析最终形成CSSOM树,过程为Bytes -> Characters -> Tokens -> Nodes -> CSSOM。

     假设css代码如下:

    body { font-size: 16px }
    p { font-weight: bold }
    span { color: red }
    p span { display: none }
    img { float: right }
    

     得到的CSSOM为:

    render树

     由DOM树与CSS树结合形成的渲染树(其中无法显示的元素,如script,head元素或diplay:none的元素,不会在渲染树中,也就最终不会被渲染出来),页面的布局,绘制都是以render树为依据。

     由以上的DOM树与CSSOM树,最终得到的渲染树如下:


    ## 回流与重绘

     在此之前,我们先明确另外两个概念:布局与绘制。



     - 布局是页面首次加载时进行的操作,重新布局即为回流。

     - 绘制是页面首次加载时进行的操作,重新绘制即为重绘。

    什么时候会发生回流和重绘呢:


    - 当页面的某部分元素发生了尺寸、位置、隐藏发生了改变,页面进行回流。得对整个页面重新进行布局计算,将所有尺寸,位置受到影响的元素回流。 - 当页面的某部分元素的外观发生了改变,但尺寸、位置、隐藏没有改变,页面进行重绘。(同样,只重绘部分元素,而不是整个页面重绘)
     **回流的同时往往会伴随着重绘,重绘不一定导致回流。**所以回流导致的代价是大于重绘的。

     如果大家对这两者的差别还不是很清楚的话,我引用这两张图给大家:

    回流

    重绘

    那么具体什么操作会引起回流呢:


    - 页面初始化渲染 - 窗口的尺寸变化 - 元素的尺寸、位置、隐藏变化 - DOM结构发生变化,如删除节点 - 获取某些属性,引发回流 - 很多浏览器会对回流进行优化,一定时间段后或数量达到阕值时,做一次批处理回流。 - 当获取一些属性时,浏览器为了返回正确的值也会触发回流,导致浏览器优化无效,有: 1. offset(top/bottom/left/right) 2. client (top/bottom/left/right) 3. scroll (top/bottom/left/right) 4. getComputedStyle() 5. width,height - 其次,字体大小修改及内容更新也会导致回流
     频繁的回流与重绘会导致频繁的页面渲染,导致cpu或gpu过量使用,使得页面卡顿。

    那么如何减少回流呢:


    1. 减少逐项更改样式,最好一次性更改style,或是将更改的样式定义在class中并一次性更新 2. 避免循环操作DOM,而是新建一个节点,在他上面应用所有DOM操作,然后再将他接入到DOM中 3. 当要频繁得到如offset属性时,只读取一次然后赋值给变量,而不是每次都获取一次 4. 将复杂的元素绝对定位或固定定位,使他脱离文档流,否则回流代价很高 5. 使用硬件加速创建一个新的复合图层,当其需要回流时不会影响原始复合图层回流
    ## 硬件加速

     我们在未开启硬件加速的时候是使用cpu来渲染页面,只有开启了硬件加速了,才会使用到GPU渲染页面。

     在详细讲解硬件加速前,我们先来讲解一下简单图层和复合图层


    - DOM中的每个结点对应一个简单图层 - 复合图层是各个简单图层的合并,一个页面一般来说只有一个复合图层,无论你创建了多少个元素,都是在这个复合图层中 - 其次,absolute、fixed布局,可以使该元素脱离文档流,但还是在这个复合图层中,所以他还是会影响复合图层的绘制,但不会影响重排
     **当一个元素使用硬件加速后,会生成一个新的复合图层**,这样不管其如何变化,都不会影响原复合图层。不过不要大量使用硬件加速,会导致资源消耗过度,导致页面也卡。

     所以,使用了硬件加速后,会有多个复合图层,然后多个复合图层互相独立,单独布局、绘制。

    如何才能使用硬件加速;


    1. translate3d,translateZ
    2. opacity属性
    ### 硬件加速时请使用z-index

     具体原理是这样的:

     当一个元素使用了硬件加速,在其后的元素,若z-index比他大或者相同,且absolute或fixed的属性相同,则默认为这些元素也创建各自的复合图层。

     所以我们人为地为这个元素添加z-index值,从而避免这种情况


    ## 浏览器页面的渲染流程

     经过以上的学习,我们可以清楚浏览器的渲染过程了:


     1. 解析html得到DOM树

     2. 解析css得到CSS树

     3. 合并得到render树

     4. 布局,当页面有元素的尺寸、大小、隐藏有变化或增加、删除元素时,重新布局计算,并修改页面中所有受影响的部分

     5. 绘制,当页面有元素的外观发生变化时,重新绘制

     6. GUI线程将得到的各层的位图(每个元素对应一个普通图层)发送给Browser进程,由Browser进程将各层合并,渲染在页面上


    DOMContentLoaded和load事件

     这两者的差别,由其定义就可知:


     - DOMContentLoaded:当DOM加载完成触发

     - load:当DOM,样式表,脚本都加载完时触发



     所以可以知道,**DOMContentLoaded在load之前触发**
    ### css的堵塞情况

     首先,是在Browser进程中下载css文件,当下载完成后,发送给GUI线程。

     其次,是在GUI线程中解析html及css,不过这两者是并行的。

     由于css的下载和解析不会影响DOM树,所以不会堵塞html文件的解析,但会堵塞页面渲染。

     这样的设计是非常合理的,如果css文件的下载和解析不会堵塞页面渲染,那么在页面渲染的途中或结束后发现元素样式有变化,则又需要回流和重绘。


    ### js的堵塞情况

     明确的是,js文件的下载和解析执行都会堵塞html文件的解析及页面渲染。

     因为js脚本可能会改变DOM结构,若是其不堵塞html文件的解析及页面渲染的话,那么当js脚本改变DOM结构或元素样式时,会引发回流和重绘,会造成不必要的性能浪费,不如等待js执行完,在进行html解析和页面渲染。

     如果你不想js堵塞的话,则使用async属性,这样就可以异步加载js文件,加载完成后立即执行。


    ### css和js文件应当放在html哪个位置

    js:



     当需要在DOM树完成之前用js进行初始化操作的话,在head中使用js。

     如果是需要在DOM树形成之后,即要操作DOM,则在body元素的末尾。不过也可以使用load事件。

     如果js的内容比较小,则推荐使用内部js而不是引用js,这样可以减少http请求。


     **css:**

     一般放在head中,因为css的解析不影响html的解析,所以越早引入,越早同时解析。


    ## 事件循环机制

     事件循环机制在我的这篇文章有详细的说明:https://www.cnblogs.com/caiyy/p/10362247.html

     总结一句话:

    事件循环机制的核心是事件触发线程,由于执行栈产生异步任务,异步任务完成后事件触发线程将其回调函数传入到任务队列中,当执行栈为空,任务队列将队列头的回调函数入执行栈,从而新的一轮循环开始。这就是称为循环的原因。


    ### 宏任务和微任务

    宏任务(macrotask):


     - 主代码块和任务队列中的回调函数就是宏任务。




     - 为了使js内部宏任务和DOM任务能够有序的执行,每次执行完宏任务后,会在下一个宏任务执行之前,对页面重新进行渲染。(宏任务 -> 渲染 -> 宏任务)



    #### 微任务(microtask):

     - 在宏任务执行过程中,执行到微任务时,将微任务放入微任务队列中。




     - 在宏任务执行完后,在重新渲染之前执行。




     - 当一个宏任务执行完后,他会将产生的所有微任务执行完。



    分别在什么场景下会产生宏任务或微任务呢:

    • 宏任务:主代码块,setTimeout,setInterval(任务队列中的所有回调函数都是宏任务)

    • 微任务:Promise

    ## 导致页面无法立即响应的原因

     导致页面无法响应的原因是执行栈中还有任务未执行完,或者是js引擎线程被GUI线程堵塞。


    ## html文件解析过程

     这个过程是在下载html文件之后,不包括网络请求过程


     1. Browser进程下载html文件并将文件发送给renderer进程

     2. renderer进程的GUI进程开始解析html文件来构建出DOM

     3. 当遇到外源css时,Browser进程下载该css文件并发送回来,GUI线程再解析该文件,在这同时,html的解析也同时进行,但不会渲染(还未形成渲染树)

     4. 当遇到内部css时,html的解析和css的解析同时进行

     5. 继续解析html文件,当遇到外源js时,Browser进程下载该js文件并发送回来,此时,js引擎线程解析并执行js,因为GUI线程和js引擎线程互斥,所以GUI线程被挂起,停止继续解析html。直到js引擎线程空闲,GUI线程继续解析html。

     6. 遇到内部js也是同理

     7. 解析完html文件,形成了完整的DOM树,也解析完了css,形成了完整的CSSOM树,两者结合形成了render树

     8. 根据render树来进行布局,若在布局的过程中发生了元素尺寸、位置、隐藏的变化或增加、删除元素时,则进行回流,修改

     9. 根据render树进行绘制,若在布局的过程中元素的外观发生变换,则进行重绘

     10. 将布局、绘制得到的各个简单图层的位图发送给Browser进程,由它来合并简单图层为复合图层,从而显示到页面上

     11. 以上步骤就是html文件解析全过程,完成之后,如若当页面有元素的尺寸、大小、隐藏有变化时,重新布局计算回流,并修改页面中所有受影响的部分,如若当页面有元素的外观发生变化时,重绘


     (完)

    参考链接

    1.CSS3硬件加速也有坑http://web.jobbole.com/83575/

    2.浏览器渲染过程、回流、重绘简介https://blog.csdn.net/cxl444905143/article/details/42005333

    3.页面优化,谈谈重绘(repaint)和回流(reflow)https://www.cnblogs.com/echolun/p/10105223.html

    4.你真的了解回流和重绘吗https://www.cnblogs.com/chenjg/p/10099886.html

    5.css加载会造成阻塞吗?https://www.cnblogs.com/chenjg/p/7126822.html

    6.从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理https://segmentfault.com/a/1190000012925872#articleHeader20

    7.从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!https://segmentfault.com/a/1190000013662126

  • 相关阅读:
    3-1
    3-2
    习题二 8
    习题二 3
    习题二 5
    习题二 4
    习题二 6
    实验三-2未完成
    实验三
    心得
  • 原文地址:https://www.cnblogs.com/caiyy/p/10406934.html
Copyright © 2011-2022 走看看