知识列表:进程和线程渲染进程解析、加载、渲染页面
参考链接:
图解浏览器的基本工作原理
进程与线程的一个简单解释
浏览器的渲染:过程与原理
defer和async的区别
DOMContentLoaded与load的区别
当考虑网页首屏加速的时候,我们在考虑什么
JS 一定要放在 Body 的最底部么?聊聊浏览器的渲染机制
css加载会造成阻塞吗? - 掘金
Fundebug:经典面试题:浏览器是怎样解析CSS的?
(一)前置知识
如果你够了解浏览器底层运行机制,你会发现我们常说的页面渲染是我们浏览器架构模式中的其中一环。不同的浏览器又不同的架构模式。待会我们会以Chrome为例,Chrome 采用的就是 多进程架构 。
不过在弄清楚浏览器中多进行架构里的工作模式,需要知道一些基本知识点。一、进程(process)和线程(thread)
先推荐一个讲进程和线程比较好的教程,漫画形式,通俗易懂:
看完之后,是不是对进程和线程有所了解。让我们再对他们深入了解一下:
1、进程和线程如何产生?
这里涉及了几个名词:操作系统、CPU、浏览器
操作系统你暂且理解为一种应用程序,相当于手机中搭载的Android系统和Mac OS系统。而CPU(计算机的核心),执行着操作系统中发布的各类指令,承担所有的计算任务,这里我们把它当作一个有很多车间的 工厂。
当我们启动一个应用(类似于打开浏览器),计算机会创建一个进程,操作系统会为进程分配一部分内存,应用的所有状态都会保存在这块内存中。这里我们把它当作工厂里的一间间 车间(记得一个车间可以有很多工人)。
应用也许还会创建多个线程来辅助工作,这些线程可以共享这部分内存中的数据。这里我们把它当作车间中的一个个 工人 。注意:如果应用关闭,进程会被终结,操作系统会释放相关内存。
当然,一个进程还可以要求操作系统生成另一个进程来执行不同的任务,系统会为新的进程分配独立的内存,两个进程之间可以使用 IPC (Inter Process Communication)进行通信。很多应用都会采用这样的设计,如果一个工作进程反应迟钝,重启这个进程不会影响应用其它进程的工作。
以Chrome为例,这个浏览器由多个进程组成,每个进程都有自己核心的职责,它们相互配合完成浏览器的整体功能。每个进程中又包含多个线程,一个进程内的多个线程也会协同工作,配合完成所在进程的职责。
Chrome 采用多进程架构,其顶层存在一个 Browser process 用以协调浏览器的其它进程。
Chrome的主要进程和主要任务
如图:
- Browser Process:
(1)负责包括地址栏,书签栏,前进后退按钮等部分的工作;
(2)负责处理浏览器的一些不可见的底层操作,比如网络请求和文件访问; - Renderer Process:
(1)负责一个 tab 内关于网页呈现的所有事情 - Plugin Process:
(1)负责控制一个网页用到的所有插件,如 flash - GPU Process
(1)负责处理 GPU 相关的任务
Chrome主要线程和基本任务
浏览器 Tab 外(即除掉 ”Render Process控制网页呈现所有事情“ 外)的工作主要由 Browser Process 掌控,Browser Process 又对这些工作进一步划分,使用不同线程进行处理:
- UI thread : 控制浏览器上的按钮及输入框;
- network thread: 处理网络请求,从网上获取数据;
- storage thread: 控制文件等的访问;
这样一来,在Render Process渲染网页前,这几个线程会执行这些复杂的操作:处理输入——开始导航——读取响应——查找渲染进程——确认导航——额外的步骤
以上,只是网页首帧渲染完成。
以上也是我阅读以下文章之后梳理的一些逻辑,对浏览器多进程架构感兴趣的同学,可以详细阅读这篇文章:
(二)渲染页面是怎么发生的?
看完之前的前置知识,才发现渲染页面的工作,只是浏览器多进程中的一个环节,即Renderer Process(渲染进程)
一、渲染进程的核心目的
渲染页面就是让用户看见页面,懂不。渲染进程几乎负责Tab内所有事情。渲染进程的核心目的在于,转换 HTML CSS JS 为用户可交互的 web 页面。渲染进程中主要包含以下线程:
渲染进程 中的 几个主要线程 和 主要职责
1. Main thread 主线程
2. Worker thread 工作线程
3. Compositor thread 排版线程
4. Raster thread 光栅线程
二、页面渲染
我们熟悉的”从URL到页面展现“的问题,其实是实质是:浏览器的解析、加载(请求+响应)和渲染整个过程,包含了:
- DNS查询寻址
- TCP连接
- HTTP请求(即响应)
- 服务器响应并返回
- 页面(客户端)渲染
页面渲染,即浏览器对页面内容的渲染(包括了各渲染树的构建、页面布局、绘制),浏览器渲染页面流程可分为 5 步骤:
(1)解析 HTML 标签, 构建 DOM 树
(2)解析 CSS 标签, 构建 CSSOM 树
(3)把 DOM 和 CSSOM 组合成 渲染树 (Render tree)
(4)在渲染树的基础上进行布局, 计算每个节点的几何结构
(5)根据计算好的信息把每个节点绘制到屏幕上 (Painting)
以上其实是一个按顺序的完美状态,但你要知道现实中的页面并不一定一次性顺序完成。我们还应该想到
假如DOM和CSSOM被修改的话,以上过程会重复执行,这样的话才能计算出哪些像素需要在屏幕上进行重新渲染。实际项目中,JavaScript与CSS就是会多次修改DOM和CSSOM。
原理解析
浏览器在渲染的过程中存在 并行加载 是怎么回事?
不同浏览器使用的内核不同,所以他们的渲染过程也是不一样的。目前主要有两个:webkit渲染过程
Gecko渲染过程
从流程我们可以看出来:
- DOM解析和CSS解析是两个并行的进程,所以这也解释了为什么CSS加载不会阻塞DOM的解析。
- 然而,由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。
- 由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行。
浏览器渲染页面时重点解析加载渲染步骤:
- DOM解析 CSS加载(并行加载)
- 组合Render Tree依赖于DOM Tree和CSSOM Tree(CSS加载完毕,DOM才能渲染,否则会阻塞)
- CSS样式表会先加载执行完毕(谁都不能阻挡CSS的解析加载)
- 以上步骤弄完,才最好执行JS脚本(因为JS会读取Dom节点和css样式)
(一)搞清页面渲染流程之前,先让我们搞清楚以下几个现象:
1、白屏和FOUC
这种现象的出现,前提是将CSS和JS随意放置,如CSS放在文档底部,JS脚本放在head的话,就会因为本身阻塞特性,导致CSS加载未完成,JS加载时机过早而导致白屏。
(1)白屏
CSS全部载入解析、加载完后渲染展示页面,如果没有加载完,就会出现白屏。如果把样式放在文档底部,浏览器会等HTML和CSS完全加载完成之后再绘制到屏幕上去。
比如我们打开某些国外的网站可能出现加载时间过长,内容不是逐步展现,页面会先出现白屏,然后等CSS全部解析加载完成渲染到页面,页面才全部展现。题外话:现代浏览器执行的First Paint
现代浏览器为更好的用户体验,浏览器中的渲染引擎将尝试尽快在屏幕上显示的内容,不会等到所有HTML解析前开始构建和布局渲染树。部分的内容将被解析显示 。 浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来, 尽快的 减少白屏的时间 。
(2)FOUC,无样式内容闪烁
CSS未完全加载前,会先渲染显示已解析的HTML内容,然后CSS完全加载完成后,再次渲染。如果把样式放在底部,浏览器逐步载入无样式的内容,等CSS载入后页面才突然展现出样式。
现代浏览器(如FireFox)渲染逻辑是解析HTML就会直接画到页面上,这时你会看到没有样式的内容,CSS再通过不断的解析将页面重绘一遍,也就是闪烁一下突然展现样式,这就是FOUC。
2、DOMConentLoaded 和 load
控制台,打开network面板中有这两个数值(感兴趣的同学立刻打开看一下咯,实践!!实践!!),这两条标志线分别对应网络请求上两个事件: DOMConentLoaded 和 load。
(1)DOMConentLoaded
DOMConentLoaded,即 dom 内容加载完毕 。
什么是都dom内容加载完毕?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后,过一会页面会展示出内容,但页面有些资源比如图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发 DOMContentLoaded 事件。而这段时间就是HTML文档被加载和解析完成。什么又是 HTML文档被加载和解析完成?要从渲染原理开始讲起。当我们在浏览器地址输入URL时,浏览器会发送请求到服务器,服务器将请求的HTML文档发送回浏览器,浏览器将文档下载下来后,便开始从上到下解析,解析完成之后,会生成DOM。
如果页面中有CSS,会根据CSS内容形成CSSOM,然后DOM和CSSOM会生成一个渲染树,最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上。浏览器加载和解析页面的一个快照:
上图看到在解析html的过程中,html的解析会被中断,这是因为javascript大概率放置在文档的头部header,阻塞了dom解析。当解析过程中遇到<script>标签的时候,便会停止解析过程,转而去处理脚本。如果脚本是内联的,浏览器会先去执行这段内联的脚本;如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析HTML文档。
同时javascript的执行会受到标签前面样式文件的影响。如果在标签前面有样式文件,需要样式文件加载并解析完毕后才执行脚本。这是因为javascript可读取和修改 CSSOM 属性。
在这里我们可以明确 DOMContentLoaded 所计算的时间,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
DOMContentLoaded事件不同的浏览器对其支持不同,需要绑定事件进行调用,这里暂时就不展开代码说了网上超多代码演示。(2)load
页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的 load 事件会在 DOMContentLoaded 被触发之后才触发。load在浏览器中均能兼容,只需要调用它的事件: window.onload(){ }(二)为什么我们一再强调将CSS放在头部,将JS放在尾部?
1、阻塞渲染情况: CSS和JavaScript
在讨论资源阻塞渲染的时候,现代浏览器总是并行加载资源。例如,当 HTML 解析器(HTML Parser)被脚本阻塞时,解析器虽然会停止构建 DOM,但仍会识别该脚本后面的资源,并进行预加载。CSS
- 默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。
- 存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建。
JavaScript
- 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
- JavaScript 不仅可以读取和修改 DOM 属性,还可以读取和修改 CSSOM 属性。
2、Script 标签的位置很重要。实际使用时,可以遵循下面两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
- JavaScript 应尽量少影响 DOM 的构建。
3、阻塞资源的阻塞原理
CSS阻塞
<style> p { color: red; }</style>
<link rel="stylesheet" href="index.css">
link 标签(无论是否 inline)会被视为阻塞渲染的资源,浏览器会优先处理这些 CSS 资源,直至 CSSOM 构建完毕。
渲染树(Render-Tree)的关键渲染路径中,要求同时具有 DOM 和 CSSOM,之后才会构建渲染树。即,HTML 和 CSS 都是阻塞渲染的资源。HTML 显然是必需的,因为包括我们希望显示的文本在内的内容,都在 DOM 中存放,那么可以从 CSS 上想办法。
以Chrome为例,先在Chrome控制台设置一下网速(百度一下),资源的下载速度上限就会被限制成20kb/s。CSS加载会阻塞DOM树的解析渲染吗?
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
h1 {
color: red !important
}
</style>
<script>
function h () {
console.log(document.querySelectorAll('h1'))
}
setTimeout(h, 0)
</script>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<h1>这是红色的</h1>
</body>
</html>
假设结果: CSS加载会阻塞DOM树解析和渲染 假设画面: 按道理在 CSS 还没加载完前,下面的内容不会被解析渲染,那么我们一开始看到的应该是白屏,h1 不会显示出来。并且此时 console.log 结果应该是一个空数组。 实际结果: 如下图
由上图我们可以看到,当CSS还没加载完成的时候,h1并没有显示,但是此时控制台输出如下
由上图,我们也可以看到,当CSS还没加载出来的时候,页面显示白屏,直到CSS加载完成之后,红色字体才显示出来,也就是说,下面的内容虽然解析了,但是并没有被渲染出来。所以,CSS加载会阻塞DOM树渲染。
评价该机制: 有过项目经验的人估计马上反应,如果是对DOM节点和CSS样式加载操作,一般都是冲着优化性能去的。原作者认为这是浏览器的其中一种优化机制。当加载CSS时,可能会修改下面DOM节点的样式,如果CSS加载不阻塞DOM树渲染的话,那么当CSS加载完之后,DOM树可能又得重新重绘或者回流了,这就造成了一些没必要的损耗。所以,干脆先将DOM树结构解析完,把可以做的工作做完,然后等你CSS加载完后,再根据最终的样式来渲染DOM树,这种做法性能方面确实会比较好一点。 由此得出: CSS加载不会阻塞DOM树解析,但是会阻塞DOM树渲染。
CSS加载会不会阻塞js执行呢?
<!DOCTYPE html>
<html lang="en">
<head>
<title>css阻塞</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
console.log('before css')
var startDate = new Date()
</script>
<link href="https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<h1>这是红色的</h1>
<script>
var endDate = new Date()
console.log('after css')
console.log('经过了' + (endDate -startDate) + 'ms')
</script>
</body>
</html>
假设结果: css加载会阻塞后面的js运行 预期画面: 在link后面的js代码,应该要在css加载完成后才会运行 实际结果: 如下图
由上图我们可以看出,位于css加载语句前的那个js代码先执行了,但是位于css加载语句后面的代码迟迟没有执行,直到css加载完成后,它才执行。这也就说明了,css加载会阻塞后面的js语句的执行。 详细结果看下图(css加载用了5600+ms):
结论
由上所述,我们可以得出以下结论:
- css加载不会阻塞DOM树的解析
- css加载会阻塞DOM树的渲染
- css加载会阻塞后面js语句的执行
改变CSS阻塞的办法
- 尽快在适当的位置提供CSS
- 用媒体类型(media type)和媒体查询(media query)来解除对渲染的阻塞。
<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">
第一个资源会加载并阻塞。
第二个资源设置了媒体类型,会加载但不会阻塞,print 声明只在打印网页时使用。
第三个资源提供了媒体查询,会在符合条件时阻塞渲染。
JavaScript阻塞
模拟一个 JavaScript 会打断HTML解析的代码
<p>1</p>
<script>console.log("inline")</script>
<p>2</p>
<script src="app.js"></script>
<p>3</p>
<p>4</p>
<script src="app.js"></script>
<p>5</p>
<script>console.log("inline")</script>
<p>6</p>
script 标签一旦插入在页面头部或中间任意位置,容易阻塞 DOM解析(无论是不是 inline-script),P 标签会从上到下解析,这个过程会被两段 JavaScript 分别打断一次(加载并且执行的时间段内)。并且Script中JS放在header中,dom内容也会影响现代浏览器中执行的First Paint,导致First Paint延后。所以说我们会将JS放在后面,以减少First Paint的时间,但 不会减少 DOMContentLoaded 被触发的时间 。改变JS阻塞的办法
- 实际项目中,将脚本文件放到文档底部
- Script标签中插入 defer 和 asyncdefer和async都是script中可选属性,且只对外部脚本文件有效。
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>
有 async,加载和渲染后续文档元素的过程将和 script.js 的 加载与执行 并行进行(异步)。
<script defer src="myscript.js"></script>
有 defer,加载后续文档元素的过程将和 script.js 的 加载 并行进行(异步),但是 script.js 的 执行 要延迟到文档所有元素解析和显示后,DOMContentLoaded 事件触发之前完成。
接着,我们来看一张图: 蓝色线代表 网络请求,红色线代表 执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析。
此图告诉我们以下几个要点:
- defer 和 async 在网络读取(下载)这块儿是一样的,都是异步(相较于 HTML 解析)
- 它俩的差别在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的
- 关于 defer,此图未尽之处在于它是按照加载顺序执行脚本的,这一点要善加利用
- async 则是一个乱序执行的主,反正对它来说脚本的加载和执行是紧紧挨着的,所以不管你声明的顺序如何,只要它加载完了就会立刻执行
- 仔细想想,async 对于应用脚本的用处不大,因为它完全不考虑依赖(哪怕是最低级的顺序执行),不过它对于那些可以不依赖任何脚本或不被任何脚本依赖的脚本来说却是非常合适的。