Skip to content

一. 网页的解析过程


  • 大家有没有深入思考过:一个网页从输入URL到浏览器中到显示经历过怎么样的解析过程呢?

    image-20220530160113751
  • 要想深入理解下载的过程,我们还要先理解,一个index.html被下载下来后是如何被解析和显示在浏览器上的

  • 说出浏览器输入一个URL到页面显示的过程

    1. 先通过DNS服务器进行域名解析
    2. 解析出对应的IP地址然后向IP地址对应的主机发送http请求,获取对应的静态资源
    3. 默认情况服务器会返回index.html文件
    4. 然后浏览器内核开始解析HTML
    5. 首先,会解析对应的html 生成DOM Tree
    6. 解析过程中,如果遇到css文件后会先下载然后进行解析,生成CSSOM(CSS object mode)
    7. DOM TreeCSS Tree都解析完成之后,会进行合并生成Render Tree(渲染树)
    8. 初步生成的渲染树会显示节点以及部分样式,但是并不表示每个节点的尺寸,位置信息
    9. 于是进行Layout(布局)来生成渲染树节点的宽高位置等信息
    10. 经过Layout之后,浏览器内核将布局时的每个frame转换为屏幕上实际的像素点,将每个节点绘制到屏幕上
  • 浏览器的工作原理:https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work

二. 浏览器渲染流程


1. 浏览器内核

  • 常见的浏览器内核有:

    内核浏览器
    Trident (三叉戟)IE、360安全浏览器、搜狗高速浏览器、百度浏览器、UC浏览器
    Gecko(壁虎)Mozilla Firefox (火狐)
    Presto(急板乐曲)—> Blink(眨眼)Opera
    WebKitSafari、移动端浏览器(Androidios)、360极速浏览器、搜狗高速浏览器
    WebKit(分支优化) —> Blink(目前渲染最快)Google ChromeEdge
    image-20220325182402397
    • 我们经常说的浏览器内核指的是浏览器的排版引擎

      • 排版引擎layout engine),也称为浏览器引擎browser engine)、页面渲染引擎rendering engine)或样版引擎
    • 也就是一个网页下载下来后,就是由我们的渲染引擎来帮助我们解析的

2. 渲染引擎如何解析页面呢?

  • 渲染引擎在拿到一个页面后,如何解析整个页面并且最终呈现出我们的网页呢?

    • 我们之前学习过下面的这幅图,现在让我们更加详细的学习它的过程

      image-20220530161953242
    1. 首先下载HTML文件,下载完成后开始解析HTML文件
    2. 解析的过程中遇到外部CSS引入,就去下载外部CSS文件
    3. 下载完开始解析CSS文件,CSS文件下载和解析过程不会阻塞HTML的解析过程
    4. HTML解析完成就会创建DOM树,CSS解析完成会绑定到对应的DOM节点上
    5. 然后就会生成对应的render tree
    6. 然后经过布局
    7. 布局之后就展示网页

3. 渲染页面的详细流程

4. 解析一:HTML解析过程

  • 因为默认情况下服务器会给浏览器返回index.html文件,所以解析HTML是所有步骤的开始:

  • 解析HTML,会构建DOM Tree

    image-20220530163338721

5. 解析二:生成CSS规则

  • HTML解析的过程中,如果遇到CSSlink元素,那么会由浏览器(单独的一个线程(执行单元) )负责下载对应的CSS文件:

  • 注意:下载CSS文件是不会影响DOM的解析的(不会阻塞HTML的解析)

  • 浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:

    • 我们可以称之为 CSSOMCSS Object ModelCSS对象模型)
    image-20220530163521154

6. 解析三:构建render tree

  • 当有了DOM Tree CSSOM Tree后,就可以两个结合来构建Render Tree

    image-20220530165651256
  • 注意一:link元素不会阻塞DOM tree的构建过程,但是会阻塞Render tree的构建过程

    • 这是因为render tree在构建时,需要对应的CSSOM tree
    • 具体会不会阻塞取决于浏览器会不会做优化,浏览器可能觉得这个解析等的时间太久了,就会先把之前解析好的先渲染出来
  • 注意二:render treeDOM tree并不是一一对应的关系,比如displaynone的元素,压根不会出现在render tree

7. 解析四:布局(layout)和绘制(paint)

  • 当渲染树构建完成,就会对渲染树上运行布局(layout)以计算每个节点的几何体

    • 渲染树只会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
    • 布局是确定呈现树中所有节点的宽度、高度和位置等信息
  • 布局完成,就会将每个节点绘制(paint)到屏幕上

    • 在绘制阶段,浏览器将布局阶段计算的每个frame(具体的矩形空间)转为屏幕上实际的像素点
    • 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素(比如img
    image-20220530170605521
    • 对应的内容 => 匹配对应CSS规则 => 计算样式 => 构建每个元素对应的frame(具体的结构体) => 布局(确定每个节点的位置大小等) => 绘制(每个节点对应的frame结构体转化为实际屏幕上的像素) => 合成图层

三. 回流和重绘


  • 理解回流reflow(重排):

    • 第一次确定节点的大小和位置,称之为布局(layout),之后对节点的大小、位置修改重新计算称之为回流
  • 什么情况下引起回流呢?

    • 比如DOM结构发生改变(添加新的节点或者移除节点)
      • 修改了DOM树,渲染树也跟着修改,重新布局、绘制、展示
    • 比如改变了布局(修改了widthheightpaddingfont-size等值)
      • 修改了布局、重新计算布局、绘制、展示
    • 比如窗口resize(修改了窗口的尺寸等)
      • 页面布局发生改变,重新计算布局
    • 比如调用getComputedStyle()方法获取尺寸、位置信息
      • 会重新计算整个frame,引起layout,重新计算布局
      • 具体看浏览器会不会做些优化,比如获取的时候,就不会重新计算
  • 理解重绘repaint

    • 第一次渲染内容称之为绘制(paint),之后重新渲染称之为重绘
  • 什么情况下会引起重绘呢?

    • 比如修改背景色、文字颜色、边框颜色、边框样式(实线或虚线)等
      • 边框宽度会引起回流
  • 回流一定会引起重绘,所以回流是一件很消耗性能的事情

  • 重绘不一定是由回流引起的,比如只有颜色改变时,是不会产生回流的,但是会重绘

  • 所以在开发中要尽量避免发生回流:

    1. 修改样式时尽量一次性修改
      • 比如通过cssText修改,比如通过添加class修改
    2. 尽量避免频繁的操作DOM
      • 我们可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成,再一次性的操作
    3. 尽量避免通过getComputedStyle()获取尺寸、位置等信息
    4. 对某些元素使用positionabsolute
      • 并不是不会引起回流,而是开销相对较小,因为是脱标元素不会对其他元素造成影响
    5. 必要时可以使用 CSS contain 属性限制计算布局、样式和绘制等的范围

四. 合成和性能优化


1. 特殊解析 – composite合成

  • 绘制的过程,可以将布局后的元素绘制到多个合成图层中

    • 这是浏览器的一种优化手段
  • 默认情况下,标准流中的内容都是被绘制在同一个图层(Layer)中的

  • 一些特殊的属性,会创建一个新的合成层CompositingLayer ),并且新的图层可以利用GPU来加速绘制

    • 因为每个合成层都是单独渲染的
    image-20220530231238865
  • 那么哪些属性可以形成新的合成层呢?常见的一些属性:

    • 3D相关的transform函数
    • videocanvasiframe
    • opacity动画转换时(小于1)
    • position: fixed
    • will-change:一个实验性的属性,提前告诉浏览器哪些属性会发生变化
    • animationtransition设置了opacitytransform
      • 尽量避免修改元素的margin-left、宽高等,因为会产生回流,回流是件十分消耗性能的操作,回流会导致重新计算布局,重新渲染,重新合成图层,合成完再渲染图层,使用transition搭配transform会在新的图层执行动画,不影响其他图层中的元素,不会产生回流
  • 分层确实可以提高性能,但是它以内存管理为代价(会增加浏览器内存使用),因此不应作为web性能优化策略的一部分过度使用

五. defer和async属性


1. script元素和页面解析的关系

  • 我们现在已经知道了页面的渲染过程,但是js在哪里呢?
    • 事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的
    • 它会停止继续构建DOM tree,首先下载js代码,并且执行js的脚本
    • 只有等到js脚本执行结束后,才会继续解析HTML,构建DOM
  • 为什么要这样做呢?
    • 这是因为js的作用之一就是操作DOM,并且可以修改DOM
    • 如果我们等到DOM树构建完成并且渲染再执行js,会造成严重的回流和重绘,影响页面的性能
    • 所以会在遇到script元素时,优先下载和执行js代码,再继续构建DOM
  • 但是这个也往往会带来新的问题,特别是现代页面开发中:
    • 在目前的开发模式中(比如VueReact),脚本往往比HTML页面更“重”,处理时间需要更长
    • 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
  • 从标准规范的角度来说,脚本没执行完就会阻塞构建DOM树,为了考虑用户体验,浏览器会做一些优化,一些已经解析构建了的DOM tree会被先渲染出来,但对应的dom元素并不会提前被脚本获取到
  • 渲染引擎会力求尽快呈现内容显示在屏幕上,它不必等到整个HTML文档解析完毕,就会开始构建渲染树和设置布局,在不断接受和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来
  • 为了解决这个问题,script元素给我们提供了两个属性(attribute):deferasync

2. defer属性

  • defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree

    • 脚本会由浏览器来进行下载,但是不会阻塞DOM Tree的构建过程
    • 如果脚本提前下载好了,它会等待DOM Tree构建完成,在DOMContentLoaded事件之前先执行defer中的代码
      • 所以在defer中可以操作DOM,因为defer中的脚本,不管提前下载好还是之后,都是在DOM tree构建完成后执行
  • 因为defer中的代码可能会操作DOM,所以如果DOM操作还没完成,就不算DOM加载完成,所以**DOMContentLoaded总是会等待defer中的代码先执行完成**

    html
    <script defer src="./js/defer-demo.js"></script>
    <script>
    	window.addEvenetListener("DOMContentLoaded", () => {
        console.log("DOMContentLoaded")
      })
    </script>
  • 另外多个带defer的脚本是可以保持正确的顺序执行的

  • 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中

注意:

  • defer仅适用于外部脚本,对于script默认内容会被忽略

3. async属性

  • async 特性与defer有些类似,它也能够让脚本不阻塞页面
  • async是让一个脚本完全独立的:
    • 浏览器不会因async脚本而阻塞DOM tree的构建(与defer类似)
    • async脚本不能保证顺序,它是独立下载、独立运行不会等待其他脚本
    • async不会能保证在DOMContentLoaded之前或者之后执行

总结:

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

Released under the MIT License.