We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
前面有提到 http 交互,那么接下来就是浏览器获取到 html,然后解析,渲染
这部分很多都参考了网上资源,特别是图片,参考了来源中的文章
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
1. 解析HTML,构建DOM树 2. 解析CSS,生成CSS规则树 3. 合并DOM树和CSS规则,生成render树 4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算 5. 绘制render树(paint),绘制页面像素信息 6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上
如下图:
整个渲染步骤中,HTML 解析是第一步。
简单的理解,这一步的流程是这样的:浏览器解析 HTML,构建 DOM 树。
但实际上,在分析整体构建时,却不能一笔带过,得稍微展开。
解析 HTML 到构建出 DOM 当然过程可以简述如下:
Bytes → characters → tokens → nodes → 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>
浏览器的处理如下:
列举其中的一些重点过程:
1. Conversion转换:浏览器将获得的HTML内容(Bytes)基于他的编码转换为单个字符 2. Tokenizing分词:浏览器按照HTML规范标准将这些字符转换为不同的标记token。每个token都有自己独特的含义以及规则集 3. Lexing词法分析:分词的结果是得到一堆的token,此时把他们转换为对象,这些对象分别定义他们的属性和规则 4. DOM构建:因为HTML标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样 例如:body对象的父节点就是HTML对象,然后段略p对象的父节点就是body对象
最后的 DOM 树如下:
同理,CSS 规则树的生成也是类似。简述为:
Bytes → characters → tokens → nodes → CSSOM
譬如style.css内容如下:
style.css
body { font-size: 16px } p { font-weight: bold } span { color: red } p span { display: none } img { float: right }
那么最终的 CSSOM 树就是:
当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了
一般来说,渲染树和 DOM 树相对应的,但不是严格意义上的一一对应
因为有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者display: none等
display: none
整体来说可以看图:
有了 render 树,接下来就是开始渲染,基本流程如下:
图中重要的四个步骤就是:
1. 计算CSS样式 2. 构建渲染树 3. 布局,主要定位坐标和大小,是否换行,各种position overflow z-index属性 4. 绘制,将图像绘制出来
然后,图中的线与箭头代表通过 js 动态修改了 DOM 或 CSS,导致了重新布局(Layout)或渲染(Repaint)
这里 Layout 和 Repaint 的概念是有区别的:
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
什么会引起回流?
1.页面渲染初始化 2.DOM结构改变,比如删除了某个节点 3.render树变化,比如减少了padding 4.窗口resize 5.最复杂的一种:获取某些属性,引发回流, 很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流, 但是除了render树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括 (1)offset(Top/Left/Width/Height) (2) scroll(Top/Left/Width/Height) (3) cilent(Top/Left/Width/Height) (4) width,height (5) 调用了getComputedStyle()或者IE的currentStyle
回流一定伴随着重绘,重绘却可以单独出现
所以一般会有一些优化方案,如:
documentFragment
transform
will-change
translate
再来看一个示例:
var s = document.body.style; s.padding = "2px"; // 回流+重绘 s.border = "1px solid red"; // 再一次 回流+重绘 s.color = "blue"; // 再一次重绘 s.backgroundColor = "#ccc"; // 再一次 重绘 s.fontSize = "14px"; // 再一次 回流+重绘 // 添加node,再一次 回流+重绘 document.body.appendChild(document.createTextNode('abc!'));
CSS 的标签与 JS 的<script>标签在 html 中放置位置
最好把 CSS 的<link>标签放在<head></head>之间,最好把 JS 的<script>标签恰好放在</body>之前。
<link>
<head></head>
<script>
</body>
把<link>放在<head>中
<head>
这种做法可以让页面逐步呈现,提高了用户体验。将样式表放在文档底部附近,会使许多浏览器(包括 Internet Explorer)不能逐步呈现页面。一些浏览器会阻止渲染,以避免在页面样式发生变化时,重新绘制页面中的元素。这种做法可以防止呈现给用户空白的页面或没有样式的内容。
把<script>标签恰好放在</body>之前
脚本在下载和执行期间会阻止 HTML 解析。把<script>标签放在底部,保证 HTML 首先完成解析,将页面尽早呈现给用户。
如果一定要放在 <head> 中,可以让 <script> 标签使用 defer 属性。
Chrome 的开发者工具中,Performance 中可以看到详细的渲染过程:
上面介绍了 html 解析,渲染流程。但实际上,在解析 html 时,会遇到一些资源连接,此时就需要进行单独处理了
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
遇到外链时的处理
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1 中是每一个资源的下载都要开启一个 http 请求,对应一个 tcp/ip 链接)
遇到 CSS 样式资源
CSS 资源的处理有几个特点:
media query
遇到 JS 脚本资源
JS 脚本资源的处理有几个特点:
注意,defer 和 async 是有区别的: defer 是延迟执行,而 async 是异步执行。
简单的说:
async
onload
DOMContentLoaded
defer
body
遇到 img 图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方
简单的对比:
展开是执行上下文,作用域链,事件循环等。
执行上下文可以简单理解为一个对象:
每一个执行上下文,都有三个重要属性:
它的类型:
代码执行过程:
VO 与 AO
VO 是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO 就被创建了 (变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。)
可以理解为:
在函数上下文中:VO === AO
在全局上下文中:VO === this === global
总的来说,VO 中会存放一些变量信息(如声明的变量,函数,arguments 参数等等)
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数,原理和原型链很相似。
由两部分组成:
如此 [[scope]]包含[[scope]],便自上而下形成一条链式作用域。
[[scope]]
参考链接
JS 中存在一个叫做执行栈的东西。JS 的所有同步代码都在这里执行,当执行一个函数调用时,会创建一个新的执行环境并压到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。 (2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。 (3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 (4)主线程不断重复上面的第三步。
下图就是主线程和任务队列的示意图。
只要主线程空了,就会去读取"任务队列",这就是 JavaScript 的运行机制。这个过程会不断重复。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部 API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。
当一个异步任务执行完毕后会将任务添加到任务队列中。例如:
setTimeout((_) => {}, 1000);
代码中 setTimeout 会在一秒后将回调函数添加到任务队列中。事实上异步队列也分两种类型:微任务、宏任务。
微任务和宏任务的区别是,当执行栈空了,会检查微任务队列中是否有任务,将微任务队列中的任务依次拿出来执行一遍。当微任务队列空了,从宏任务队列中拿出来一个任务去执行,执行完毕后检查微任务队列,微任务队列空了之后再从宏任务队列中拿出来一个任务执行。
属于微任务(microtask)的事件有以下几种:
属于宏任务(macrotask)的事件有以下几种:
浏览器架构
The text was updated successfully, but these errors were encountered:
No branches or pull requests
大纲
解析页面流程
前面有提到 http 交互,那么接下来就是浏览器获取到 html,然后解析,渲染
这部分很多都参考了网上资源,特别是图片,参考了来源中的文章
流程简述
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
如下图:
HTML 解析,构建 DOM
整个渲染步骤中,HTML 解析是第一步。
简单的理解,这一步的流程是这样的:浏览器解析 HTML,构建 DOM 树。
但实际上,在分析整体构建时,却不能一笔带过,得稍微展开。
解析 HTML 到构建出 DOM 当然过程可以简述如下:
譬如假设有这样一个 HTML 页面:(以下部分的内容出自参考来源,修改了下格式)
浏览器的处理如下:
列举其中的一些重点过程:
最后的 DOM 树如下:
生成 CSS 规则
同理,CSS 规则树的生成也是类似。简述为:
譬如
style.css
内容如下:那么最终的 CSSOM 树就是:
构建渲染树
当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了
一般来说,渲染树和 DOM 树相对应的,但不是严格意义上的一一对应
因为有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者
display: none
等整体来说可以看图:
渲染
有了 render 树,接下来就是开始渲染,基本流程如下:
图中重要的四个步骤就是:
然后,图中的线与箭头代表通过 js 动态修改了 DOM 或 CSS,导致了重新布局(Layout)或渲染(Repaint)
这里 Layout 和 Repaint 的概念是有区别的:
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流, 所以优化方案中一般都包括,尽量避免回流。
什么会引起回流?
回流一定伴随着重绘,重绘却可以单独出现
所以一般会有一些优化方案,如:
documentFragment
或 div,在它上面应用所有 DOM 操作,最后再把它添加到 window.document,避免多次读取 offset 等属性。无法避免则将它们缓存到变量transform
、will-change
等,比如改变元素位置,我们使用translate
会比使用绝对定位改变其 left 、top 等来的高效,因为它不会触发重排或重绘,transform
使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。再来看一个示例:
CSS 的标签与 JS 的<script>标签在 html 中放置位置
最好把 CSS 的
<link>
标签放在<head></head>
之间,最好把 JS 的<script>
标签恰好放在</body>
之前。把
<link>
放在<head>
中这种做法可以让页面逐步呈现,提高了用户体验。将样式表放在文档底部附近,会使许多浏览器(包括 Internet Explorer)不能逐步呈现页面。一些浏览器会阻止渲染,以避免在页面样式发生变化时,重新绘制页面中的元素。这种做法可以防止呈现给用户空白的页面或没有样式的内容。
把
<script>
标签恰好放在</body>
之前脚本在下载和执行期间会阻止 HTML 解析。把
<script>
标签放在底部,保证 HTML 首先完成解析,将页面尽早呈现给用户。如果一定要放在
<head>
中,可以让<script>
标签使用 defer 属性。Chrome 中的调试
Chrome 的开发者工具中,Performance 中可以看到详细的渲染过程:
资源外链的下载
上面介绍了 html 解析,渲染流程。但实际上,在解析 html 时,会遇到一些资源连接,此时就需要进行单独处理了
简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
遇到外链时的处理
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1 中是每一个资源的下载都要开启一个 http 请求,对应一个 tcp/ip 链接)
遇到 CSS 样式资源
CSS 资源的处理有几个特点:
media query
声明的 CSS 是不会阻塞渲染的遇到 JS 脚本资源
JS 脚本资源的处理有几个特点:
注意,defer 和 async 是有区别的: defer 是延迟执行,而 async 是异步执行。
简单的说:
async
是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在onload
前,但不确定在DOMContentLoaded
事件的前或后defer
是延迟执行,在浏览器看起来的效果像是将脚本放在了body
后面一样(虽然按规范应该是在DOMContentLoaded
事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)遇到 img 图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方
loaded 和 domcontentloaded
简单的对比:
JS 引擎解析过程
展开是执行上下文,作用域链,事件循环等。
执行上下文
执行上下文可以简单理解为一个对象:
每一个执行上下文,都有三个重要属性:
它的类型:
代码执行过程:
VO 与 AO
VO 是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO 就被创建了 (变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。)
可以理解为:
在函数上下文中:VO === AO
在全局上下文中:VO === this === global
总的来说,VO 中会存放一些变量信息(如声明的变量,函数,arguments 参数等等)
作用域链
我们知道,我们可以在执行上下文中访问到父级甚至全局的变量,这便是作用域链的功劳。作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数,原理和原型链很相似。
由两部分组成:
如此
[[scope]]
包含[[scope]]
,便自上而下形成一条链式作用域。参考链接
事件循环机制
JS 中存在一个叫做执行栈的东西。JS 的所有同步代码都在这里执行,当执行一个函数调用时,会创建一个新的执行环境并压到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)
下图就是主线程和任务队列的示意图。
只要主线程空了,就会去读取"任务队列",这就是 JavaScript 的运行机制。这个过程会不断重复。
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部 API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。
执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。
主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。
当一个异步任务执行完毕后会将任务添加到任务队列中。例如:
代码中 setTimeout 会在一秒后将回调函数添加到任务队列中。事实上异步队列也分两种类型:微任务、宏任务。
微任务和宏任务的区别是,当执行栈空了,会检查微任务队列中是否有任务,将微任务队列中的任务依次拿出来执行一遍。当微任务队列空了,从宏任务队列中拿出来一个任务去执行,执行完毕后检查微任务队列,微任务队列空了之后再从宏任务队列中拿出来一个任务执行。
属于微任务(microtask)的事件有以下几种:
属于宏任务(macrotask)的事件有以下几种:
浏览器架构
参考链接
The text was updated successfully, but these errors were encountered: