资讯专栏INFORMATION COLUMN

浏览器渲染机制

FullStackDeveloper / 2972人阅读

摘要:修改浏览器渲染因为的阻塞使得解析停止,下载完成之前,页面无法显示任何东西。浏览器渲染解析到文件时出现阻塞。我们把调整到尾部浏览器渲染这是页面可以渲染了,但是没有样式。

本文示例源代码请戳github博客,建议大家动手敲敲代码。
前言

浏览器渲染页面的过程

从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:

DNS 查询

TCP 连接

HTTP 请求即响应

服务器响应

客户端渲染

本文讨论第五个部分,即浏览器对内容的渲染,这一部分(渲染树构建、布局及绘制),又可以分为下面五个步骤:

处理 HTML 标记并构建 DOM 树。

处理 CSS 标记并构建 CSSOM 树

将 DOM 与 CSSOM 合并成一个渲染树。

根据渲染树来布局,以计算每个节点的几何信息。

将各个节点绘制到屏幕上。

需要明白,这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。

1、浏览器的线程

在详细说明之前我们来看一下浏览器线程。这将有助于我们理解后续内容。

浏览器是多线程的,它们在内核制控下相互配合以保持同步。一个浏览器至少实现三个常驻线程:JavaScript 引擎线程,GUI 渲染线程,浏览器事件触发线程。

GUI 渲染线程:负责渲染浏览器界面 HTML 元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在 Javascript 引擎运行脚本期间,GUI 渲染线程都是处于挂起状态的,也就是说被”冻结”了。

JavaScript 引擎线程:主要负责处理 Javascript 脚本程序。

定时器触发线程:浏览器定时计数器并不是由 JavaScript 引擎计数的, JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此浏览器通过多带带线程来计时并触发定时。

事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JS 引擎的处理。这些事件包括当前执行的代码块如定时任务、浏览器内核的其他线程如鼠标点击、AJAX 异步请求等。由于 JS 的单线程关系所有这些事件都得排队等待 JS 引擎处理。定时块任何和 ajax 请求等这些异步任务,事件触发线程只是在到达定时时间或者是 ajax 请求成功后,把回调函数放到事件队列当中。

异步 HTTP 请求线程:在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求, 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。在发起了一个异步请求时,http 请求线程则负责去请求服务器,有了响应以后,事件触发线程再把回到函数放到事件队列当中。

2、构建DOM树与CSSOM树

浏览器从网络或硬盘中获得HTML字节数据后会经过一个流程将字节解析为DOM树:

编码: 先将HTML的原始字节数据转换为文件指定编码的字符。

令牌化: 然后浏览器会根据HTML规范来将字符串转换成各种令牌(如这样的标签以及标签中的字符串和属性等都会被转化为令牌,每个令牌具有特殊含义和一组规则)。令牌记录了标签的开始与结束,通过这个特性可以轻松判断一个标签是否为子标签(假设有两个标签,当标签的令牌还未遇到它的结束令牌就遇见了标签令牌,那么就是的子标签)。

生成对象: 接下来每个令牌都会被转换成定义其属性和规则的对象(这个对象就是节点对象)

构建完毕: DOM树构建完成,整个对象集合就像是一棵树形结构。可能有人会疑惑为什么DOM是一个树形结构,这是因为标签之间含有复杂的父子关系,树形结构正好可以诠释这个关系(CSSOS同理,层叠样式也含有父子关系。例如: div p {font-size: 18px},会先寻找所有p标签并判断它的父标签是否为div之后才会决定要不要采用这个样式进行渲染)。

整个DOM树的构建过程其实就是: 字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型,
下面将通过一个示例HTML代码与配图更形象地解释这个过程。


  
    
    
    Critical Path
  
  
    

Hello web performance students!

浏览器渲染

222222

3333333

style.css

#header{
    color: red;
}

a.js、b.js暂时为空
可以看到,服务端将对a.js的请求延迟5秒返回。Server启动后,在chrome浏览器中打开http://127.0.0.1:8080/index.html
我们打开chrome的调试面板

第一次解析html的时候,外部资源好像是一起请求的,说资源是预解析加载的,就是说style.css和b.js是a.js造成阻塞的时候才发起的请求,图中也是可以解释得通,因为第一次Parse HTML的时候就遇到阻塞,然后预解析就去发起请求,所以看起来是一起请求的。

6、HTML 是否解析一部分就显示一部分

我们修改一下html代码




    
    
    
    浏览器渲染
    





222222

3333333


因为a.js的延迟,解析到a.js所在的script标签的时候,a.js还没有下载完成,阻塞并停止解析,之前解析的已经绘制显示出来了。当a.js下载完成并执行完之后继续后面的解析。当然,浏览器不是解析一个标签就绘制显示一次,当遇到阻塞或者比较耗时的操作的时候才会先绘制一部分解析好的。

7、js文件的位置对HTML解析有什么影响 7.1 js文件在头部加载。

修改index.html:




    
    
    
    浏览器渲染
    
    
    



222222

3333333


因为a.js的阻塞使得解析停止,a.js下载完成之前,页面无法显示任何东西。

7.2、js文件在中间加载。



    
    
    
    浏览器渲染
    





222222

3333333


解析到js文件时出现阻塞。阻塞后面的解析,导致后面的不能很快的显示。

7.3、js文件在尾部加载。



    
    
    
    浏览器渲染
    



222222

3333333

解析到a.js部分的时候,页面要显示的东西已经解析完了,a.js不会影响页面的呈现速度。

由上面我们可以总结一下

直接引入的 JS 会阻塞页面的渲染(GUI 线程和 JS 线程互斥)

JS 不阻塞资源的加载

JS 顺序执行,阻塞后续 JS 逻辑的执行

下面我们来看下异步js

7.4、async和defer的作用是什么?有什么区别?

接下来我们对比下 defer 和 async 属性的区别:

其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。

情况1

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。

情况2 (异步下载)

async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。

情况3 (延迟执行)

defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。

defer 与相比普通 script,有两点区别:

载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。

在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。

8、css文件的影响

服务端将style.css的相应也设置延迟。

fs.readFile("style.css", "utf-8", function (err, data) {
  res.writeHead(200, {"Content-Type": "text/css"});
  setTimeout(function () {
    res.write(data);
    res.end()
  }, 5000)
})



    
    
    
    浏览器渲染
    



222222

3333333

可以看出来,css文件不会阻塞HTML解析,但是会阻塞渲染,导致css文件未下载完成之前已经解析好html也无法先显示出来。

我们把css调整到尾部




    
    
    
    浏览器渲染



222222

3333333

这是页面可以渲染了,但是没有样式。直到css加载完成

以上我们可以简单总结。

CSS 放在 head 中会阻塞页面的渲染(页面的渲染会等到 css 加载完成)

CSS 阻塞 JS 的执行 (因为 GUI 线程和 JS 线程是互斥的,因为有可能 JS 会操作 CSS)

CSS 不阻塞外部脚本的加载(不阻塞 JS 的加载,但阻塞 JS 的执行,因为浏览器都会有预先扫描器)

参考
浏览器渲染过程与性能优化
聊聊浏览器的渲染机制
你不知道的浏览器页面渲染机制

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/103661.html

相关文章

  • webkit渲染机制浅析

    ...点,一直没继续看完它,现在才看完,,,说来惭愧。 览器内核 览器中,内核模块(渲染引擎)作用是把HTML页面转变为可视化,可听化的图像结果。 下面我们来逐步打开渲染引擎这个黑盒: 渲染引擎模块及其依赖模块 ht...

    Cobub 评论0 收藏0
  • 前端页面渲染机制-笔记

    前端页面渲染机制-笔记 览器基础结构 1.用户界面(user interface):用户所看到及与之交互的功能组件,如地址栏、返回、前进按钮 2.览器引擎(browser engine):用户界面和呈现引擎之间传递指令 3.渲染引擎(呈现引擎)...

    tuantuan 评论0 收藏0
  • 前端页面渲染机制-笔记

    前端页面渲染机制-笔记 览器基础结构 1.用户界面(user interface):用户所看到及与之交互的功能组件,如地址栏、返回、前进按钮 2.览器引擎(browser engine):用户界面和呈现引擎之间传递指令 3.渲染引擎(呈现引擎)...

    nanchen2251 评论0 收藏0
  • JS JavaScript事件循环机制

    ...FIFO(命名管道)、消息队列 一个进程里有单个或多个线程 览器是多进程的,因为系统给它的进程分配了资源(cpu、内存)(打开Chrome会有一个主进程,每打开一个Tab页就有一个独立的进程) 览器渲染进程是多线程的 GUI...

    dantezhao 评论0 收藏0
  • 览器多进程到JS单线程,JS运行机制最全面的一次梳理

    ...----- 如果看完本文后,还对进程线程傻傻分不清,不清楚览器多进程、览器内核多线程、JS单线程、JS运行机制的区别。那么请回复我,一定是我写的还不够清晰,我来改。。。 ----------正文开始---------- 最近发现有不少介绍J...

    wanghui 评论0 收藏0
  • 渲染机制

    渲染机制 览器 1. 渲染机制 什么是 DOCTYPE 及作用 DTD 告诉览器文件是什么文档类型,览器根据它来判断用什么引擎来解析渲染文件。DOCTYPE 用来声明文档类型和 DTD 规范。 览器是怎么渲染过程 HTML 5: HTML 4.01 Strict: 严...

    Big_fat_cat 评论0 收藏0

发表评论

0条评论

FullStackDeveloper

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<