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

  • 相关阅读:
    BZOJ 2212/BZOJ 3702
    BZOJ 4761 Cow Navigation
    BZOJ 3209 花神的数论题
    BZOJ 4760 Hoof, Paper, Scissors
    BZOJ 3620 似乎在梦中见过的样子
    BZOJ 3940 Censoring
    BZOJ 3942 Censoring
    BZOJ 3571 画框
    BZOJ 1937 最小生成树
    BZOJ 1058 报表统计
  • 原文地址:https://www.cnblogs.com/caiyy/p/10406934.html
Copyright © 2011-2022 走看看