-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
深入浅出浏览器渲染原理 #51
Comments
引申一下事件循环EventLoop |
不错。深入浅出。 |
最近觉得不满意,这篇重新改写,敬请期待! |
今天重新更新了 |
请问一下,比如用户在input框中输入文字,会造成整个页面的回流吗?还是局部的? |
很详细,棒棒哒! |
window.onload事件是发生在哪个阶段,就是页面加载完成在哪个阶段,为什么有时可以访问dom节点但页面空白 |
干货满满 |
DOM树,规则树构建不是在渲染线程吗,那个图里的规则树构建的时候是怎么做到同时解析script文件的?JS解析难道不在JS引擎线程? |
想请教一下浏览器如何可以得知发生了回流事件 |
有没有文章是讲 浏览器 如何解析React写的html,css,js的?全部都写在了一起 |
期待写的更深一点 |
补充说明1,情况2和情况3可以调换下位置 |
我觉得有点不严谨 |
妙哇 |
赞一个 |
写得非常不错,学习了。 |
只有 HTML 格式浏览器才能正确解析 这句话是不是不严谨,像pdf,视频,语音甚至json |
请问各位大佬,文章里 “将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。” 这句话中的图层是什么意思呢?我在百度上搜了好久,没找到答案,请各位大佬赐教了,谢谢 |
|
CSS的Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element(也即是每个Frame) 确定这个Frame不是Node吗?渲染树上的每个元素不是代表着渲染树上的每个节点?怎么会是框架呢? |
这一版感觉只是对浏览器原理的简单介绍。大佬有没有计划再出一版涉及整个渲染流程的文章? |
您好,您的来信已收到,我会尽快阅读且回复,谢谢!
|
前言
浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。
本文我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。
想阅读更多优质文章请猛戳GitHub博客,一年五十篇优质文章等着你!
页面加载过程
在介绍浏览器渲染过程之前,我们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。
要点如下:
例如在浏览器输入
https://juejin.im/timeline
,然后经过 DNS 解析,juejin.im
对应的 IP 是36.248.217.149
(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:
其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。
浏览器渲染过程
浏览器渲染过程大体分为如下三部分:
1)浏览器会解析三个东西:
2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。
3)最后通过调用操作系统Native GUI的API绘制。
构建DOM
浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:
在网络中传输的内容其实都是 0 和 1 这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。
<html>
、<body>
等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。这时候你一定会有疑问,节点与节点之间的关系如何维护?
事实上,这就是Token要标识“起始标签”和“结束标签”等标识的作用。例如“title”Token的起始标签和结束标签之间的节点肯定是属于“head”的子节点。
上图给出了节点之间的关系,例如:“Hello”Token位于“title”开始标签与“title”结束标签之间,表明“Hello”Token是“title”Token的子节点。同理“title”Token是“head”Token的子节点。
事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。注意:带有结束标签标识的Token不会创建节点对象。
接下来我们举个例子,假设有段HTML文本:
上面这段HTML会解析成这样:
构建CSSOM
DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。
构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。
在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。
注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。
构建渲染树
当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。
在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是
display: none
的,那么就不会在渲染树中显示。我们或许有个疑惑:浏览器如果渲染过程中遇到JS文件怎么处理?
渲染过程中,如果遇到
<script>
就停止渲染,执行 JS 代码。因为浏览器有GUI渲染线程与JS引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。JavaScript的加载、解析与执行会阻塞DOM的构建,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停构建DOM,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复DOM构建。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别)。
JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。
原本DOM和CSSOM的构建是互不影响,井水不犯河水,但是一旦引入了JavaScript,CSSOM也开始阻塞DOM的构建,只有CSSOM构建完毕后,DOM再恢复DOM构建。
这是什么情况?
这是因为JavaScript不只是可以改DOM,它还可以更改样式,也就是它可以更改CSSOM。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和DOM构建,直至其完成CSSOM的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建DOM。
布局与绘制
当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会立即发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。
几点补充说明
1.async和defer的作用是什么?有什么区别?
接下来我们对比下 defer 和 async 属性的区别:
其中蓝色线代表JavaScript加载;红色线代表JavaScript执行;绿色线代表 HTML 解析。
1)情况1
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
2)情况2
<script async src="script.js"></script>
(异步下载)async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行——无论此刻是 HTML 解析阶段还是 DOMContentLoaded 触发之后。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
3)情况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是有顺序的加载。
2.为什么操作 DOM 慢
把 DOM 和 JavaScript 各自想象成一个岛屿,它们之间用收费桥梁连接。——《高性能 JavaScript》
JS 是很快的,在 JS 中修改 DOM 对象也是很快的。在JS的世界里,一切是简单的、迅速的。但 DOM 操作并非 JS 一个人的独舞,而是两个模块之间的协作。
因为 DOM 是属于渲染引擎中的东西,而 JS 又是 JS 引擎中的东西。当我们用 JS 去操作 DOM 时,本质上是 JS 引擎和渲染引擎之间进行了“跨界交流”。这个“跨界交流”的实现并不简单,它依赖了桥接接口作为“桥梁”(如下图)。
过“桥”要收费——这个开销本身就是不可忽略的。我们每操作一次 DOM(不管是为了修改还是仅仅为了访问其值),都要过一次“桥”。过“桥”的次数一多,就会产生比较明显的性能问题。因此“减少 DOM 操作”的建议,并非空穴来风。
3.你真的了解回流和重绘吗
渲染的流程基本上是这样(如下图黄色的四个步骤):1.计算CSS样式 2.构建Render Tree 3.Layout – 定位坐标和大小 4.正式开画
注意:上图流程中有很多连接线,这表示了Javascript动态修改了DOM属性或是CSS属性会导致重新Layout,但有些改变不会重新Layout,就是上图中那些指到天上的箭头,比如修改后的CSS rule没有被匹配到元素。
这里重要要说两个概念,一个是Reflow,另一个是Repaint
我们知道,当网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断重新渲染。重新渲染会重复回流+重绘或者只有重绘。
回流必定会发生重绘,重绘不一定会引发回流。重绘和回流会在我们设置节点样式时频繁出现,同时也会很大程度上影响性能。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。
1)常见引起回流属性和方法
任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流,
2)常见引起重绘属性和方法
3)如何减少回流、重绘
性能优化策略
基于上面介绍的浏览器渲染原理,DOM 和 CSSOM 结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
<script>
标签加上 defer属性 和 async属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。<link>
标签的 rel属性 中的属性值设置为 preload 能够让你在你的HTML页面中可以指明哪些资源是在页面加载完成后即刻需要的,最优的配置加载顺序,提高渲染性能总结
综上所述,我们得出这样的结论:
欢迎关注公众号:前端工匠,你的成长我们一起见证!
参考文章
The text was updated successfully, but these errors were encountered: