技术分享PPT整理(三):网页渲染流程
在我刚开始学习Web开发的时候,一直有个疑问——我写出的代码究竟是在什么时候发生作用的呢?是不是每次我修改代码网页都随之变化了?当然,现在来看这肯定是一个错误的想法,经过一段时间的工作和学习后,代码到页面转换的路径在我的脑海里愈发清晰,虽然“输入URL到网页显示之间发生了什么?”是个老生常谈的问题,但我还是想按自己的理解来说明一遍。
浏览器架构
首先从我们最熟悉的朋友开始说起,Web开发离不开浏览器,我在查资料的时候有开很多选项卡的习惯,每次打开任务管理器都能看到Chrome浏览器在内存占用方面一枝独秀,另外还能看到应用名称后面的括号里有个数字,如下图所示,但我打开的标签页是不到23的,那么剩下的进程是什么呢?
我们来看一张经典的图,它描绘了Chrome浏览器中四种进程的位置和作用:
- 浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
- 渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
- 插件进程 (Plugin Process):负责控制网页使用到的插件
- GPU进程 (GPU Process):负责处理整个应用程序的GPU任务
渲染进程较为特殊,每个选项卡里都需要一个渲染进程,它也是网页渲染的核心,我们在下一节详细说明,关于这些进程,可以在浏览器自带的进程管理器中详细查看:
由于经常要做多浏览器兼容,经常同时打开几个浏览器,即使没有仔细对比还是可以发现Chrome浏览器在内存占用方面算是相对比较高的,而Firefox则相对要低很多,这是因为Firefox的Tab进程和IE的Tab进程都采用了类似的策略:有多个Tab进程,但都不一定是一个页面一个Tab进程,一个Tab进程可能会负责多个页面的渲染。而作为对比,Chrome是以一个页面一个渲染进程,加上站点隔离的策略来进行的。虽然内存占用确实比较高,但是这种多进程架构也有独特的优势:
- 更高的容错性。当今WEB应用中,HTML,JavaScript和CSS日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG,而有些BUG会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。
- 更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠
- 更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。
网页渲染
大致来说,输入URL后要经过五个步骤网页才会渲染完成:
- DNS 查询
- TCP 连接
- HTTP 请求即响应
- 服务器响应
- 客户端渲染
首先,如果输入的是域名,浏览器会先从hosts文件中查找有没有对应的设置,如果没有则访问附近的DNS服务器进行DNS查询获取正确的IP地址,之后进行TCP连接,通过三次握手建立连接后开始处理HTTP请求,服务器端收到请求返回响应文档,拿到响应文档的浏览器开始使用渲染引擎进行页面渲染。
这里说到的渲染引擎就是我们经常说到的浏览器内容,例如Webkit、Gecko这些。
渲染引擎
浏览器内核是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- GUI 渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- JavaScript引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 定时触发器线程
- 传说中的setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
- 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
这五个线程各司其职,但我们这里还是将目光放到GUI渲染上:
渲染流程
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局,以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
1. DOMTree的构建(Document Object Model)
第一步(解析):从网络或者磁盘下读取的HTML原始字节码,通过设置的charset编码,转换成字符
第二步(token化):通过词法分析器,将字符串解析成Token,Token中会标注出当前的Token是开始标签,还是结束标签,或者文本标签等。
第三步(生成Nodes并构建DOM树):浏览器会根据Tokens里记录的开始标签,结束标签,将Tokens之间相互串联起来(带有结束标签的Token不会生成Node)。
2. CSSOMTree的构建(CSS Object Model)
当HTML代码遇见标签时,浏览器会发送请求获得该标签中标记的CSS文件(使用内联CSS可以省略请求的步骤提高速度,但没有必要为了这点速度而丢失了模块化与可维护性),style.css中的内容见下图:
浏览器获得外部CSS文件的数据后,就会像构建DOM树一样开始构建CSSOM树,这个过程没有什么特别的差别。
从图中可以看出,最开始body有一个样式规则是font-size:16px,之后,在body这个样式基础上每个子节点还会添加自己单独的样式规则,比如span又添加了一个样式规则color:red。正是因为样式这种类似于继承的特性,浏览器设定了一条规则:CSSOMTree需要等到完全构建后才可以被使用,因为后面的属性可能会覆盖掉前面的设置。比如在上面的css代码基础上再添加一行代码p {font-size:12px},那么之前设置的16px将会被覆盖成12px。
看到这里,感觉好像少了什么?我们的页面不会只包含HTML和CSS,JavaScript通常也在页面中占有很大的比重,并且JavaScript也是引发性能问题的重要因素,这里通过解答下面几个问题来说明JavaScript在页面渲染中的情况。
问题:渲染过程中遇到JS文件怎么处理?
由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。
因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起, GUI更新则会被保存在一个队列中等到JS引擎
线程空闲时立即被执行。
也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。
问题:为什么有时在js中访问DOM时浏览器会报错?
因为在解析的过程中,如果碰到了script或者link标签,就会根据src对应的地址去加载资源,在script标签没有设置async/defer属性时,这个加载过程是下载并执行完全部的代码,此时,DOM树还没有完全创建完毕,这个时候如果js企图访问script标签后面的DOM元素,浏览器就会抛出找不到该DOM元素的错误。
问题:平时谈及页面性能优化,经常会强调css文件应该放在html文档中的前面引入,js文件应该放在后面引入,这么做的原因是什么呢?
本来,DOM构建和CSSOM构建是两个过程,井水不犯河水。假设DOM构建完成需要1s,CSSOM构建也需要1s,在DOM构建了0.2s时发现了一个link标签,此时完成这个操作需要的时间大概是1.2s,如下图所示:
但JS也可以修改CSS样式,影响CSSOMTree最终的结果,而我们前面提到,不完整的CSSOMTree是不可以被使用的。
问题:如果JS试图在浏览器还未完成CSSOMTree的下载和构建时去操作CSS样式,会发生什么?
我们在HTML文档的中间插中入了一段JS代码,在DOM构建中间的过程中发现了这个script标签,假设这段JS代码只需要执行0.0001s,那么完成这个操作需要的时间就会变成:
那如果我们把css放到前面,js放到最后引入时,构建时间会变成:
由此可见,虽然只是插入了小小的一段只运行0.0001s的js代码,不同的引入时机也会严重影响DOMTree的构建速度。
简而言之,如果在DOM,CSSOM和JavaScript执行之间引入大量的依赖关系,可能会导致浏览器在处理渲染资源时出现大幅度延迟:
- 当浏览器遇到一个script标签时,DOMTree的构建将被暂停,直至脚本执行完毕
- JavaScript可以查询和修改DOMTree与CSSOMTree
- 直至CSSOM构建完毕,JavaScript才会执行
- 脚本在文档中的位置很重要
3. 渲染树的构建
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
- Render 树上的每一个节点被称为:RenderObject。
- RenderObject跟 DOM 节点几乎是一一对应的,当一个可见的 DOM 节点被添加到 DOM 树上时,内核就会为它生成对应的 RenderOject 添加到 Render 树上。
- 其中,可见的DOM节点不包括:
- 一些不会体现在渲染输出中的节点(
<html><script><link>
….),会直接被忽略掉。 - 通过CSS隐藏的节点。例如上图中的span节点,因为有一个CSS显式规则在该节点上设置了display:none属性,那么它在生成RenderObject时会被直接忽略掉。
- 一些不会体现在渲染输出中的节点(
- Render 树是衔接浏览器排版引擎和渲染引擎之间的桥梁,它是排版引擎的输出,渲染引擎的输入。
4. 布局
到目前为止,浏览器计算出了哪些节点是可见的以及它的信息和样式,接下来就需要计算这些节点在设备视口内的确切位置和大小,这个过程我们称之为“布局”。
布局最后的输出是一个“盒模型”:将所有相对测量值都转换成屏幕上的绝对像素。
5. 渲染
当Layout布局事件完成后,浏览器会立即发出Paint Setup与Paint事件,开始将渲染树绘制成像素,绘制所需的时间跟CSS样式的复杂度成正比,绘制完成后,用户就可以看到页面的最终呈现效果了。
总结
我们对一个网页发送请求并获得渲染后的页面可能也就经过了1~2秒,但浏览器其实已经做了上述所讲的非常多的工作,总结一下浏览器关键渲染路径的整个过程:
- 处理HTML标记数据并生成DOM树。
- 处理CSS标记数据并生成CSSOM树。
- 将DOM树与CSSOM树合并在一起生成渲染树。
- 遍历渲染树开始布局,计算每个节点的位置信息。
- 将每个节点绘制到屏幕。
相关问题
defer 和 async
上面我们还提到一个小知识点:在script标签没有设置async/defer属性时,这个加载过程是下载并执行完全部的代码。如果有设置这两个属性会有什么不同呢?
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
情况1
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。情况2
<script async src="script.js"></script>
(异步下载)
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。情况3
<script defer src="script.js"></script>
(延迟执行)
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
defer 与相比普通 script,有两点区别:
- 载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
- 在加载多个JS脚本的时候,async是无顺序的加载,而defer是有顺序的加载。
回流(reflow)和重绘(repaint)
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复上图中的第四步(回流)+第五步(重绘)或者只有第五个步(重绘)。
- 重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的,比如background-color。
- 回流:当render tree中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建
回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,添加或者删除可见的DOM元素:
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容变化,比如用户在input框中输入文字
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
如何减少回流、重绘
- 使用 transform 替代 top
- 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
- CSS 选择符从右往左匹配查找,避免节点层级过多
- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
为什么操作 DOM 慢
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们通过 JS 操作 DOM 的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作 DOM 次数一多,也就等同于一直在进行线程之间的通信,并且操作 DOM 可能还会带来重绘回流的情况,所以也就导致了性能上的问题。
这也就是为什么我们在使用Vue.js框架时会感觉流畅程度明显高于传统的页面,因为Vue.js使用的是虚拟DOM,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。
技术分享PPT整理(三):网页渲染流程的更多相关文章
- 技术分享PPT整理(一):Bootstrap基础与应用
最近在复习的时候总感觉有些知识点总结过,但是翻了一下博客没有找到,才想起来有一些内容是放在部门的技术分享里的,趁这个时候跳了几篇相对有价值的梳理一下,因为都是PPT,所以内容相对零散,以要点和图片为主 ...
- 技术分享PPT整理(二):C#常用类型与数据结构
这篇博客起源于我对Dictionary.List.ArrayList这几个类区别的好奇,当时在改造公司的旧系统,发现很多地方使用了ArrayList,但我们平时用的多是泛型集合List,改造的时候要全 ...
- 教你制作高逼格的技术分享Keynote(PPT)
本文来自 网易云社区 . 作为一个程序猿/媛,想必大家都参与过大大小小各式各样的技术分享,异或在不同的场合分享自己的技术心得.抛开分享内容的质量不谈,笔者发现通常这些分享者的演示文稿(Keynot或P ...
- 社区活动分享PPT:使用微软开源技术开发微服务
上周六在成都中生代技术社区线下活动进行了一个名为"微软爱开源-使用微软开源技术开发微服务"的技术分享. 也算是给很多不熟悉微软开源技术的朋友普及一下微软最近几年在开源方面所做的努力 ...
- 内部技术分享的 PPT
本文的基础是搞了一次内部的技术分享,在此也分享一下本次的PPT的一些内容.先列一下大概内容吧. EF-Code First API(WCF.WebAPI) Xaml MVVM AOP Xamarin. ...
- C#技术分享【PDF转换成图片——13种方案】(2013-07-25重新整理)
原文:C#技术分享[PDF转换成图片--13种方案](2013-07-25重新整理) 重要说明:本博已迁移到 石佳劼的博客,有疑问请到 文章新地址 留言!!! 写在最前面:为了节约大家时间,撸主把最常 ...
- webkit技术--网页渲染原理
Webkit渲染 Webkit 是苹果发起的一个开源项目,后来谷歌用这个项目以 webkit 创建了一个新的项目 Chromium,我们平常用的 Chrome 浏览器一般都是基于 Chromium 开 ...
- 【Stage3D学习笔记续】山寨Starling(三):Starling核心渲染流程
这篇文章我们剔除Starling的Touch事件体系和动画体系,专门来看看Starling中的渲染流程实现,以及其搭建的显示列表结构. 由于Starling是模仿Flash的原生显示列表,所以我们可以 ...
- GreatSQL特性介绍及前景展望 | 数据技术嘉年华2021分享PPT发布
欢迎来到 GreatSQL社区分享的MySQL技术文章,如有疑问或想学习的内容,可以在下方评论区留言,看到后会进行解答 GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 全 ...
- 爱奇艺技术分享:爱奇艺Android客户端启动速度优化实践总结
本文由爱奇艺技术团队原创分享,原题<爱奇艺Android客户端启动优化与分析>. 1.引言 互联网领域里有个八秒定律,如果网页打开时间超过8秒,便会有超过70%的用户放弃等待,对Andro ...
随机推荐
- .Net内存管理释放的两种方式
在.Net中,资源回收主要是指内存管理和非托管资源的释放.分别提供了两种主要的方式进行处理: 垃圾回收(GC) 确认性资源释放(DRD) 官网相关文档的链接:https://learn.microso ...
- AI围棋项目:KataGo
网站地址: https://katagotraining.org/ 项目地址: https://github.com/lightvector/KataGo
- Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
Canvas简历编辑器-图形绘制与状态管理(轻量级DOM) 在前边我们聊了数据结构的设计和剪贴板的数据操作,那么这些操作都还是比较倾向于数据相关的操作,那么我们现在就来聊聊基本的图形绘制以及图形状态管 ...
- Mysql 不用Json存储 List<Long>的方式
public static void main(String[] args) { List<Long> idList = new ArrayList<>(); for (int ...
- 原生js操作dom的总结
一.学习DOM之前需要知道的 1.什么是window? window:是一个全局对象, 代表浏览器中一个打开的窗口, 每个窗口都是一个window对象 2.什么是document? d ...
- HTTP 常见状态码【5种最最最常见的状态码】
HTTP 常见状态码 一.200 [ok] 一切正常 二.400 [Bad Request] 客户端 出现问题 需要注意:前端传入的参数与后台接收数据时的 参数名 必须保持一致 三.500 [Inte ...
- vscode 下配置 clang
需要在workspace的文件夹下添加文件: .clang-format 更多参数说明: https://clang.llvm.org/docs/ClangFormatStyleOptions.htm ...
- 粉丝提问|c语言:如何定义一个和库函数名一样的函数,并在函数中调用该库函数
问题描述: 某个函数fun_1()是在lib内,没法修改的,在程序中大量的使用了该函数,现在想把原本fun_1失效(现在失效的方法是#define fun_1(..)),用另外一个函数fun_2(), ...
- Zabbix监控可视化
一.监控系统的架构体系 大家都知道,监控系统由三大部分组成,一,监控数据采集:二,监控告警分析:三,监控数据报表.可视化.在市面上常见的开源监控软件,或者商业监控软件中,均有很好的实践和体现. 监控系 ...
- 安装 AWS CLI
安装 macOS 使用 Homebrew: brew install awscli 手动安装: curl "https://awscli.amazonaws.com/AWSCLIV2.pkg ...