You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance
<html>
<head>
<title>Web page parsing</title>
</head>
<body>
<div>
<h1>Web page parsing</h1>
<p>This is an example Web page.</p>
</div>
</body>
</html>
DOM Tree解析如下:
CSS解析
Webkit使用Flext and Bison 解析器生成器,通过CSS语法文件自动创建解析器。Bison会创建自下而上的移位归约解析器。Firefox使用的是人工编写的自上而下的解析器。
浏览器是多进程的工作的,“从URL输入到渲染”会主要涉及到的,是浏览器进程、网络进程和渲染进程这三个:
浏览器进程负责处理、响应用户交互,比如点击、滚动;
网络进程负责处理数据的请求,提供下载功能;
渲染进程负责将获取到的HTML、CSS、JS处理成可以看见、可以交互的页面;
“从URL输入到页面渲染”整个过程可以分成网络请求和浏览器渲染两个部分,分别由网络进程和渲染进程去处理。
网络请求
URL 解析
encodeURI和 encodeURIComponent正是起这个作用的,它们的规则基本一样,只是
= ? & ; /
这类URI组成符号,这些在encodeURI中不会被编码,但在encodeURIComponent中统统会检查缓存
DNS 解析
建立 TCP 连接
三次握手
三次握手是为了保证客户端存活,防止服务端在收到失效的超时请求造成资源浪费。
TLS 握手
TLS握手时是非对称加密来通信,正式传输数据时,使用对称加密的方式。
发送请求 && 接收响应
关闭 TCP 连接
四次挥手
TCP 是可以双向传输数据的,每个方向都需要一个请求和一个确认。所以双方都需要确认关闭。
主动方会多等待 2MSL,这是一种兜底机制。
浏览器渲染
构建 DOM 树
HTML文件的结构没法直接被浏览器使用,所以需要先把HTML标签变成可以给js使用的结构
样式计算
问题1:为什么 css 放在头部,js 放在 body 尾部?
css 资源异步下载,下载和解析不会阻塞构建 dom 树
js 资源同步下载,下载和执行都会阻塞构建 dom 树
问题2:js脚本引入时async和defer有什么差别
问题3:preload、prefetch有什么区别
为了保证资源的正确的预加载
布局定位 Layout
布局树只取渲染树中的可见元素,意味着head标签,display:none的元素不会被添加。
图层分层
图层绘制 Paint
浏览器内有哪些进程,这些进程都有些什么作用
浏览器地址输入URL后,内部的进程、线程都做了哪些事
我们与浏览器交互时,内部进程是怎么处理这些交互事件的
浏览器架构
在讲浏览器架构之前,先理解两个概念,进程和线程。
进程(process)是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,线程(thread)是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
简单的说呢,进程可以理解成正在执行的应用程序,而线程呢,可以理解成我们应用程序中的代码的执行器。而他们的关系可想而知,线程是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程,只能隶属于一个进程。
大家都知道,浏览器属于一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程,进程启动后,CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程进行资源调度,进而完成我们应用程序的功能。
而在应用程序中,为了满足功能的需要,启动的进程会创建另外的新的进程来处理其他任务,这些创建出来的新的进程拥有全新的独立的内存空间,不能与原来的进程内向内存,如果这些进程之间需要通信,可以通过IPC机制(Inter Process Communication)来进行。
很多应用程序都会采取这种多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响,也就是说,当其中一个进程挂掉了之后,不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。
浏览器的多进程架构
假如我们去开发一个浏览器,它的架构可以是一个单进程多线程的应用程序,也可以是一个使用IPC通信的多进程应用程序。
不同的浏览器使用不同的架构,下面主要以Chrome为例,介绍浏览器的多进程架构。
在Chrome中,主要的进程有4个:
浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
插件进程 (Plugin Process):负责控制网页使用到的插件
GPU进程 (GPU Process):负责处理整个应用程序的GPU任务
这4个进程之间的关系是什么呢?
首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入URL,这个时候Browser Process会向这个URL发送请求,获取这个URL的HTML内容,然后将HTML交给Renderer Process,Renderer Process解析HTML内容,解析遇到需要请求网络的资源又返回来交给Browser Process进行加载,同时通知Browser Process,需要Plugin Process加载插件资源,执行插件代码。解析完成后,Renderer Process计算得到图像帧,并将这些图像帧交给GPU Process,GPU Process将其转化为图像显示屏幕。
多进程架构的好处
Chrome为什么要使用多进程架构呢?
第一,更高的容错性。当今WEB应用中,HTML,JavaScript和CSS日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG,而有些BUG会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。
第二,更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠
第三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。
多进程架构优化
之前的我们说到,Renderer Process的作用是负责一个Tab内的显示相关的工作,这就意味着,一个Tab,就会有一个Renderer Process,这些进程之间的内存无法进行共享,而不同进程的内存常常需要包含相同的内容。
浏览器的进程模式
为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。
Process-per-site-instance (default) - 同一个 site-instance 使用一个进程
Process-per-site - 同一个 site 使用一个进程
Process-per-tab - 每个 tab 使用一个进程
Single process - 所有 tab 共用一个进程
这里需要给出 site 和 site-instance 的定义
site 指的是相同的 registered domain name(如: google.com ,bbc.co.uk)和scheme (如:https://)。比如a.baidu.com和b.baidu.com就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。
site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance
用户通过
<a target="_blank">
这种方式点击打开的新页面JS代码打开的新页面(比如 window.open)
理解了概念之后,下面解释四个进程模式
首先是Single process,顾名思义,单进程模式,所有tab都会使用同一个进程。接下来是Process-per-tab ,也是顾名思义,每打开一个tab,会新建一个进程。而对于Process-per-site,当你打开 a.baidu.com 页面,在打开 b.baidu.com 的页面,这两个页面的tab使用的是共一个进程,因为这两个页面的site相同,而如此一来,如果其中一个tab崩溃了,而另一个tab也会崩溃。
Process-per-site-instance 是最重要的,因为这个是 Chrome 默认使用的模式,也就是几乎所有的用户都在用的模式。当你打开一个 tab 访问 a.baidu.com ,然后再打开一个 tab 访问 b.baidu.com,这两个 tab 会使用两个进程。而如果你在 a.baidu.com 中,通过JS代码打开了 b.baidu.com 页面,这两个 tab 会使用同一个进程。
默认模式选择
那么为什么浏览器使用Process-per-site-instance作为默认的进程模式呢?
Process-per-site-instance兼容了性能与易用性,是一个比较中庸通用的模式。
相较于 Process-per-tab,能够少开很多进程,就意味着更少的内存占用
相较于 Process-per-site,能够更好的隔离相同域名下毫无关联的 tab,更加安全
导航过程都发生了什么
前面我们讲了浏览器的多进程架构,讲了多进程架构的各种好处,和Chrome是怎么优化多进程架构的,下面从用户浏览网页这一简单的场景,来深入了解进程和线程是如何呈现我们的网站页面的。
网页加载过程
之前我们我们提到,tab以外的大部分工作由浏览器进程Browser Process负责,针对工作的不同,Browser Process 划分出不同的工作线程:
UI thread:控制浏览器上的按钮及输入框;
network thread:处理网络请求,从网上获取数据;
storage thread: 控制文件等的访问;
第一步:处理输入
当我们在浏览器的地址栏输入内容按下回车时,UI thread会判断输入的内容是搜索关键词(search query)还是URL,如果是搜索关键词,跳转至默认搜索引擎对应都搜索URL,如果输入的内容是URL,则开始请求URL。
第二步:开始导航
回车按下后,UI thread将关键词搜索对应的URL或输入的URL交给网络线程Network thread,此时UI线程使Tab前的图标展示为加载中状态,然后网络进程进行一系列诸如DNS寻址,建立TLS连接等操作进行资源请求,如果收到服务器的301重定向响应,它就会告知UI线程进行重定向然后它会再次发起一个新的网络请求。
第三步:读取响应
network thread接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
与此同时,浏览器会进行 Safe Browsing 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。
第四步:查找渲染进程
各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。
第五步:提交导航
到了这一步,数据和渲染进程都准备好了,Browser Process 会向 Renderer Process 发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。
这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。
第六步:初始化加载完成
当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。
网页渲染原理
导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用户可进行交互的web页面。那么渲染进程是如何工作的呢?
渲染进程中,包含线程分别是:
一个主线程(main thread)
多个工作线程(work thread)
一个合成器线程(compositor thread)
多个光栅化线程(raster thread)
不同的线程,有着不同的工作职责。
构建DOM
当渲染进程接受到导航的确认信息后,开始接受来自浏览器进程的数据,这个时候,主线程会解析数据转化为DOM(Document Object Model)对象。
DOM为WEB开发人员通过JavaScript与网页进行交互的数据结构及API。
子资源加载
在构建DOM的过程中,会解析到图片、CSS、JavaScript脚本等资源,这些资源是需要从网络或者缓存中获取的,主线程在构建DOM过程中如果遇到了这些资源,逐一发起请求去获取,而为了提升效率,浏览器也会运行预加载扫描(preload scanner)程序,如果HTML中存在img、link等标签,预加载扫描程序会把这些请求传递给Browser Process的network thread进行资源下载。
JavaScript的下载与执行
构建DOM过程中,如果遇到<script>标签,渲染引擎会停止对HTML的解析,而去加载执行JS代码,原因在于JS代码可能会改变DOM的结构(比如执行document.write()等API)
不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script> 标签上添加了 async 或 defer 等属性,浏览器会异步的加载和执行JS代码,而不会阻塞渲染。
样式计算 - Style calculation
DOM树只是我们页面的结构,我们要知道页面长什么样子,我们还需要知道DOM的每一个节点的样式。主线程在解析页面时,遇到<style>标签或者标签的CSS资源,会加载CSS代码,根据CSS代码确定每个DOM节点的计算样式(computed style)。
计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认的样式。
布局 - Layout
DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。
主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中,会跳过隐藏的元素(display: none),另外,伪元素虽然在DOM上不可见,但是在布局树上是可见的。
绘制 - Paint
布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。
合成 - Compositing
文档结构、元素的样式、元素的几何关系、绘画顺序,这些信息我们都有了,这个时候如果要绘制一个页面,我们需要做的是把这些信息转化为显示器中的像素,这个转化的过程,叫做光栅化(rasterizing)。
那我们要绘制一个页面,最简单的做法是只光栅化视口内(viewport)的网页内容,如果用户进行了页面滚动,就移动光栅帧(rastered frame)并且光栅化更多的内容以补上页面缺失的部分,如下:
Chrome第一个版本就是采用这种简单的绘制方式,这一方式唯一的缺点就是每当页面滚动,光栅线程都需要对新移进视图的内容进行光栅化,这是一定的性能损耗,为了优化这种情况,Chrome采取一种更加复杂的叫做合成(compositing)的做法。
那么,什么是合成?合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧来展示滚动后的效果罢了。页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧即可。
为了实现合成技术,我们需要对元素进行分层,确定哪些元素需要放置在哪一层,主线程需要遍历渲染树来创建一棵层次树(Layer Tree),对于添加了 will-change CSS 属性的元素,会被看做单独的一层,没有 will-change CSS属性的元素,浏览器会根据情况决定是否要把该元素放在单独的层。
你可能会想要给页面上所有的元素一个单独的层,然而当页面的层超过一定的数量后,层的合成操作要比在每个帧中光栅化页面的一小部分还要慢,因此衡量你应用的渲染性能是十分重要的一件事情。
一旦Layer Tree被创建,渲染顺序被确定,主线程会把这些信息通知给合成器线程,合成器线程开始对层次数的每一层进行光栅化。有的层的可以达到整个页面的大小,所以合成线程需要将它们切分为一块又一块的小图块(tiles),之后将这些小图块分别进行发送给一系列光栅线程(raster threads)进行光栅化,结束后光栅线程会将每个图块的光栅结果存在GPU Process的内存中。
为了优化显示体验,合成线程可以给不同的光栅线程赋予不同的优先级,将那些在视口中的或者视口附近的层先被光栅化。
当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)。
绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息。
合成帧:代表页面一个帧的内容的绘制四边形集合。
以上所有步骤完成后,合成线程就会通过IPC向浏览器进程(browser process)提交(commit)一个渲染帧。这个时候可能有另外一个合成帧被浏览器进程的UI线程(UI thread)提交以改变浏览器的UI。这些合成帧都会被发送给GPU从而展示在屏幕上。如果合成线程收到页面滚动的事件,合成线程会构建另外一个合成帧发送给GPU来更新页面。
合成的好处在于这个过程没有涉及到主线程,所以合成线程不需要等待样式的计算以及JavaScript完成执行。这就是为什么合成器相关的动画最流畅,如果某个动画涉及到布局或者绘制的调整,就会涉及到主线程的重新计算,自然会慢很多。
浏览器对事件的处理
当页面渲染完毕以后,TAB内已经显示出了可交互的WEB页面,用户可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?
以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process,但是Browser Process只知道事件发生的类型和发生的位置,具体怎么对这个点击事件进行处理,还是由Tab内的Renderer Process进行的。Browser Process接受到事件后,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)。
渲染进程中合成器线程接收事件
前面我们说到,合成器线程可以独立于主线程之外通过已光栅化的层创建组合帧,例如页面滚动,如果没有对页面滚动绑定相关的事件,组合器线程可以独立于主线程创建组合帧,如果页面绑定了页面滚动事件,合成器线程会等待主线程进行事件处理后才会创建组合帧。那么,合成器线程是如何判断出这个事件是否需要路由给主线程处理的呢?
由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。
而对于非快速滚动区域的标记,开发者需要注意全局事件的绑定,比如我们使用事件委托,将目标元素的事件交给根元素body进行处理,代码如下:
在开发者角度看,这一段代码没什么问题,但是从浏览器角度看,这一段代码给body元素绑定了事件监听器,也就意味着整个页面都被编辑为一个非快速滚动区域,这会使得即使你的页面的某些区域没有绑定任何事件,每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。
其实这种情况也很好处理,只需要在事件监听时传递passtive参数为 true,passtive会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。
查找事件的目标对象(event target)
当合成器线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象。具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象。
浏览器对事件的优化
一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗。
出于优化的目的,浏览器会合并这些连续的事件,延迟到下一帧渲染是执行,也就是requestAnimationFrame之前。
而对于非连续性的事件,如keydown,keyup,mousedown,mouseup,touchstart,touchend等,会直接派发给主线程去执行。
[浏览器渲染的那些事]
浏览器的主要概念
浏览器主要分为这及部分。
User Interface
用户接口。浏览器中的地址栏、前进后退、书签菜单等。除了网页显示区域以外的都是。
Brower engine
浏览器引擎。查询与操作渲染引擎的接口。
Rendering engine
渲染引擎。今天的内容主角就是它~负责显示请求的内容
Networking
网络。用于网络请求,例如HTTP请求。
JavaScript Interpreter
用于解析执行JavaScript代码
UI Backend
绘制基础原件,比如组合框、窗口。
Data Persistence
持久层。HTML5规定了完整的浏览器中的数据库:web database
主要流程
Webkit主要流程如下:
Mozilla的Gecko渲染引擎主要流程
由图可以发现,两个引擎过程基本相同。主要有三个步骤:
解析。浏览器会解析HTML/SVG/XHTML,事实上,webkit有三个C++的类对应这三类文档。浏览器解析这三种文件会产生一个DOM Tree;解析CSS,产生style rules;解析Javacript,主要通过DOM API和CSSOM API来操作DOM Tree和CSS Rule Tree
解析完成后,浏览器引擎会通过DOM Tree和CSS Rule Tree来构造Rendering Tree。
调用操作系统Native GUI的API绘制。
两个引擎的差别在于它们起的名字不一样,即语义差别:
webkit把可视化好的可视元素成为Render Tree,用Layout来表示元素的布局
Gecko把可视化好的可视元素成为Frame Tree,每个元素就是一个frame,元素的布局成为Reflow
当然,也不是只有语义差别。还有一个细小的差别差别在于:
Gecko在HTML与DOM树之间还多一个层content Sink,这是创建DOM对象的工厂。
DOM解析
解析由两部分组成:分词+构建树。
HTML5规范中,HTML解析流程如下图:
分词是词法分析,把输入解析成符号序列。
构建树的过程就是在不断处理分词器完成的节点。除了把元素添加到DOM树上,还会将其添加到一个开放元素堆栈,用于纠正嵌套错误和标签未关闭错误。
但其实浏览器比我们想象的更强大更包容。浏览器的错误处理相当统一,虽然这不是当前HTML规范的一部分。当很多格式不良的HTML文档出现在很多网站,浏览器会尝试用和其他浏览器一样的方式修复错误。
DOM Tree解析如下:
CSS解析
Webkit使用Flext and Bison 解析器生成器,通过CSS语法文件自动创建解析器。Bison会创建自下而上的移位归约解析器。Firefox使用的是人工编写的自上而下的解析器。
浏览器对URL为什么要解析?URL参数用的是什么字符编码?那encodeURI和encodeURIComponent有什么区别?
浏览器缓存的disk cache和memory cache是什么?
预加载prefetch、preload有什么差别?
JS脚本的async和defer有什么区别?
TCP握手为什么要三次,挥手为什么要四次?
HTTPS的握手有了解过吗?
先简单了解一下浏览器的架构作为前置知识。浏览器是多进程的工作的,“从URL输入到渲染”会主要涉及到的,是浏览器进程、网络进程和渲染进程这三个:
浏览器进程负责处理、响应用户交互,比如点击、滚动;
网络进程负责处理数据的请求,提供下载功能;
渲染进程负责将获取到的HTML、CSS、JS处理成可以看见、可以交互的页面;
“从URL输入到页面渲染”整个过程可以分成 网络请求 和 浏览器渲染 两个部分,分别由 网络进程 和 渲染进程 去处理。
网络请求
网络请求部分进行了这几项工作:
URL的解析
检查资源缓存
DNS解析
建立TCP连接
TLS协商密钥
发送请求&接收响应
关闭TCP连接
URL解析
浏览器首先会判断输入的内容是一个URL还是搜索关键字。
如果是URL,会把不完整的URL合成完整的URL。一个完整的URL应该是:协议+主机+端口+路径[+参数][+锚点]。比如我们在地址栏输入www.baidu.com,浏览器最终会将其拼接成https://www.baidu.com/,默认使用443端口。
如果是搜索关键字,会将其拼接到默认搜索引擎的参数部分去搜索。这个流程需要对输入的不安全字符编码进行转义(安全字符指的是数字、英文和少数符号)。因为URL的参数是不能有中文的,也不能有一些特殊字符,比如= ? &,否则当我搜索1+1=2,假如不加以转义,url会是/search?q=1+1=2&source=chrome,和URL本身的分隔符=产生了歧义。
URL对非安全字符转义时,使用的编码叫百分号编码,因为它使用百分号加上两位的16进制数表示。这两位16进制数来自UTF-8编码,将每一个中文转换成3个字节,比如我在google地址栏输入“中文”,url会变成/search?q=%E4%B8%AD%E6%96%87,一共6个字节。
我们在写代码时经常会用的encodeURI和 encodeURIComponent正是起这个作用的,它们的规则基本一样,只是= ? & ; /这类URI组成符号,这些在encodeURI中不会被编码,但在encodeURIComponent中统统会。因为encodeURI是编码整个URL,而encodeURIComponent编码的是参数部分,需要更加严格把关。
检查缓存
检查缓存一定是在发起真正的请求之前进行的,只有这样缓存的机制才会生效。如果发现有对应的缓存资源,则去检查缓存的有效期。
在有效期内的缓存资源直接使用,称之为强缓存,从chrome网络面板看到这类请求直接返回200,size是memory cache或者disk cache。memory cache是指从资源从内存中被取出,disk cache是指从磁盘中被取出;从内存中读取比从磁盘中快很多,但资源能不能分配到内存要取决于当下的系统状态。通常来说,刷新页面会使用内存缓存,关闭后重新打开会使用磁盘缓存。
超过有效期的,则携带缓存的资源标识向服务端发起请求,校验是否能继续使用,如果服务端告诉我们,可以继续使用本地存储,则返回304,并且不携带数据;如果服务端告诉我们需要用更新的资源,则返回200,并且携带更新后的资源和资源标识缓存到本地,方便下一次使用。
DNS解析
如果没有成功使用本地缓存,则需要发起网络请求了。首先要做的是DNS解析。
会依次搜索:
浏览器的DNS缓存;
操作系统的DNS缓存;
路由器的DNS缓存;
向服务商的DNS服务器查询;
向全球13台根域名服务器查询;
为了节省时间,可以在HTML头部去做DNS的预解析:
为了保证响应的及时,DNS解析使用的是UDP协议
建立TCP连接
我们发送的请求是基于TCP协议的,所以要先进行连接建立。建立连接的通信是打电话,双方都在线;无连接的通信是发短信,发送方不管接收方,自己说自己的。
这个确认接收方在线的过程就是通过TCP的三次握手完成的。
客户端发送建立连接请求;
服务端发送建立连接确认,此时服务端为该TCP连接分配资源;
客户端发送建立连接确认的确认,此时客户端为该TCP连接分配资源;
但假如这时服务端收到一个失效的建立连接请求,我们会发现服务端的资源被浪费了——此时客户端并没有想给它传送数据,但它却准备好了内存等资源一直等待着。
所以说,三次握手是为了保证客户端存活,防止服务端在收到失效的超时请求造成资源浪费。
协商加密密钥——TLS握手
为了保障通信的安全,我们使用的是HTTPS协议,其中的S指的就是TLS。TLS使用的是一种非对称+对称的方式进行加密。
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。这种加密方式速度很快,但是问题在于如何让双方知道秘钥。因为传输数据都是走的网络,如果将秘钥通过网络的方式传递的话,秘钥被截获,就失去了加密的意义。
非对称加密,每个人都有一把公钥和私钥,公钥所有人都可以知道,私钥只有自己知道,将数据用公钥加密,解密必须使用私钥。这种加密方式就可以完美解决对称加密存在的问题,缺点是速度很慢。
我们采取非对称加密的方式协商出一个对称密钥,这个密钥只有发送方和接收方知道的密钥,流程如下:
客户端发送一个随机值以及需要的协议和加密方式;
服务端收到客户端的随机值,发送自己的数字证书,附加上自己产生一个随机值,并根据客户端需求的协议和加密方式使用对应的方式;
客户端收到服务端的证书并验证是否有效,验证通过会再生成一个随机值,通过服务端证书的公钥去加密这个随机值并发送给服务端;
服务端收到加密过的随机值并使用私钥解密获得第三个随机值,这时候两端都拥有了三个随机值,可以通过这三个随机值按照之前约定的加密方式生成密钥,接下来的通信就可以通过该对称密钥来加密解密;
通过以上步骤可知,在TLS握手阶段,两端使用非对称加密的方式来通信,但是因为非对称加密损耗的性能比对称加密大,所以在正式传输数据时,两端使用对称加密的方式。
发送请求&接收响应
HTTP的默认端口是80,HTTPS的默认端口是443。
请求的基本组成是请求行+请求头+请求体
响应的基本组成是响应行+响应头+响应体
关闭TCP连接
等数据传输完毕,就要关闭TCP连接了。关闭连接的主动方可以是客户端,也可以是服务端,这里以客户端为例,整个过程有四次握手:
客户端请求释放连接,仅表示客户端不再发送数据了;
服务端确认连接释放,但这时可能还有数据需要处理和发送;
服务端请求释放连接,服务端这时不再需要发送数据时;
客户端确认连接释放;
为什么要有四次挥手
TCP 是可以双向传输数据的,每个方向都需要一个请求和一个确认。因为在第二次握手结束后,服务端还有数据传输,所以没有办法把第二次确认和第三次合并。
主动方为什么会等待2MSL
客户端在发送完第四次的确认报文段后会等待2MSL才正真关闭连接,MSL是指数据包在网络中最大的生存时间。目的是确保服务端收到了这个确认报文段,
假设服务端没有收到第四次握手的报文,试想一下会发生什么?在客户端发送第四次握手的数据包后,服务端首先会等待,在1个MSL后,它发现超过了网络中数据包的最大生存时间,但是自己还没有收到数据包,于是服务端认为这个数据包已经丢失了,它决定把第三次握手的数据包重新给客户端发送一次,这个数据包最多花费一个MSL会到达客户端。
一来一去,一共是2MSL,所以客户端在发送完第四次握手数据包后,等待2MSL是一种兜底机制,如果在2MSL内没有收到其他报文段,客户端则认为服务端已经成功接受到第四次挥手,连接正式关闭。
浏览器渲染
上面讲完了网络请求部分,现在浏览器拿到了数据,剩下需要渲染进程工作了。浏览器渲染主要完成了一下几个工作:
构建DOM树;
样式计算;
布局定位;
图层分层;
图层绘制;
显示;
构建DOM树
HTML文件的结构没法被浏览器理解,所以先要把HTML中的标签变成一个可以给JS使用的结构。
在控制台可以尝试打印document,这就是解析出来的DOM树。
样式计算
CSS文件一样没法被浏览器直接理解,所以首先把CSS解析成样式表。
这三类样式都会被解析:
通过 link 引用的外部 CSS 文件
<style>标签内的样式 元素的 style 属性内嵌的 CSS 在控制台打印document.styleSheets,这就是解析出的样式表。 利用这份样式表,我们可以计算出DOM树中每个节点的样式。之所以叫计算,是因为每个元素要继承其父元素的属性。 DOM树中的节点有了样式,现在被叫做渲染树。 为什么要把CSS放在头部,js放在body的尾部 在解析HTML的过程中,遇到需要加载的资源特点如下: CSS资源异步下载,下载和解析都不会阻塞构建dom树 JS资源同步下载,下载和执行都会阻塞构建dom树<script src='./index.js'/> 因为这样的特性,往往推荐将CSS样式表放在head头部,js文件放在body尾部,使得渲染能尽早开始。 CSS会阻塞HTML解析吗 上文提到页面渲染是渲染进程的任务,这个渲染进程中又细分为GUI渲染线程和JS线程。 解析HTML生成DOM树,解析CSS生成样式表以及后面去生成布局树、图层树都是由GUI渲染线程去完成的,这个线程可以一边解析HTML,一边解析CSS,这两个是不会冲突的,所以也提倡把CSS在头部引入。 但是在JS线程执行时,GUI渲染线程没有办法去解析HTML,这是因为JS可以操作DOM,如果两者同时进行可能引起冲突。如果这时JS去修改了样式,那此时CSS的解析和JS的执行也没法同时进行了,会先等CSS解析完成,再去执行JS,最后再去解析HTML。 ![image](https://user-images.githubusercontent.com/59645426/220215630-ad8eccc3-8e5a-4caa-a8b5-6c40f7033c64.png) ``` 将script标签放在最后 构建CSSOM树 --- 布局绘制 构建DOM树 --- 布局绘制 没有将script标签放在最后 构建CSSOM树 ------------- --- 布局绘制 构建DOM树 下载,执行JS 构建DOM树 --- 布局绘制 没有将script标签放在最后,并且JS操作了CSSOM 构建CSSOM树。 --- 构建DOM树 下载,执行JS --- JS操作了CSSOM 构建DOM树 布局绘制 ``` CSS有可能阻塞HTML的解析。 预加载扫描器是什么 上面提到的外链资源,不论是同步加载JS还是异步加载CSS、图片等,都要到HTML解析到这个标签才能开始,这似乎不是一种很好的方式。实际上,从2008年开始,浏览器开始逐步实现了预加载扫描器:在拿到HTML文档的时候,先扫描整个文档,把CSS、JS、图片和web字体等提前下载。 js脚本引入时async和defer有什么差别 预加载扫描器解决了JS同步加载阻塞HTML解析的问题,但是我们还没有解决JS执行阻塞HTML解析的问题。所有有了async和defer属性。 没有 defer 或 async,浏览器会立即加载并执行指定的脚本 async 属性表示异步执行引入的 JavaScript,经加载好,就会开始执行 defer 属性表示延迟到DOM解析完成,再执行引入的 JS ![image](https://user-images.githubusercontent.com/59645426/220216030-3aae9bd5-854e-4d56-a306-d0a9b4a1428a.png) ``` script 构建CSSOM树 ----------- --- 布局绘制 构建DOM树 下载JS,执行JS 构建DOM树 --- script + async 构建CSSOM树 ------------- 构建DOM树 执行JS 构建DOM树 ------- 布局绘制 下载JS script + defer 构建CSSOM树 ----------------- 布局绘制 构建DOM树 ----------------- 执行JS 下载JS(引入script) ``` 在加载多个JS脚本的时候,async是无顺序的执行,而defer是有顺序的执行 preload、prefetch有什么区别 之前提到过预加载扫描器,它能提前加载页面需要的资源,但这一功能只对特定写法的外链生效,并且我们没有办法按照自己的想法给重要的资源一个更高的优先级,所以有了preload和prefetch。 preload:以高优先级为当前页面加载资源; prefetch:以低优先级为后面的页面加载未来需要的资源,只会在空闲时才去加载; 无论是preload还是prefetch,都只会加载,不会执行,如果预加载的资源被服务器设置了可以缓存cache-control那么会进入磁盘,反之只会被保存在内存中。 ```hello world!
<script src="main.js" defer></script>此外,这两种预加载资源不仅可以通过HTML标签设置,还可以通过js设置
以及 HTTP 响应头:
布局定位
上面详细的讲述了HTML和CSS加载、解析过程,现在我们的渲染树中的节点有了样式,但是不知道要画在哪个位置。所以还需要另外一颗布局树确定元素的几何定位。
布局树只取渲染树中的可见元素,意味着head标签,display:none的元素不会被添加。
图层分层
现在我们有了布局树,但依旧不能直接开始绘制,在此之前需要分层,生成一棵对应的图层树。浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面。
因为页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-index 做 z 轴排序等,我们希望能更加方便地实现这些效果。
并不是布局树的每个节点都能生成一个图层,如果一个节点没有自己的层,那么这个节点就从属于父节点的图层
通常满足下面两点中任意一点的元素就可以被提升为单独的一个图层。
1、拥有层叠上下文属性的元素会被提升为单独的一层:明确定位属性position的元素、定义透明属性opacity的元素、使用 CSS 滤镜filter的元素等,都拥有层叠上下文属性。
2、需要剪裁(clip)的地方也会被创建为图层overflow
在chrome的开发者工具:更多选项-更多工具-Layers可以看到图层的分层情况。
图层绘制
在完成图层树的构建之后,接下来终于到对每个图层进行绘制。
首先会把图层拆解成一个一个的绘制指令,排布成一个绘制列表,在上文提到的开发者工具的Layers面板中,点击detail中的profiler可以看到绘制列表。
至此,渲染进程中的主线程——GUI渲染线程已经完成了它所有任务,接下来交给渲染进程中的合成现成。
合成线程接下来会把视口拆分成图快,把图块转换成位图。
至此,渲染进程的工作全部完成,接下来会把生成的位图还给浏览器进程,最后在页面上显示。
预解析、预渲染
除了上文提到的使用preload、prefetch去提前加载,还可以使用DNS Prefetch、Prerender、Preconnect
DNS Prefetch:DNS 预解析;
preconnect:在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手;
减少回流和重绘
回流是指浏览器需要重新计算样式、布局定位、分层和绘制,回流又被叫重排;
触发回流的操作:
添加或删除可见的DOM元素
元素的位置发生变化
元素的尺寸发生变化
浏览器的窗口尺寸变化
重绘是只重新像素绘制,当元素样式的改变不影响布局时触发。
回流=计算样式+布局+分层+绘制;重绘=绘制。故回流对性能的影响更大
所以应该尽量避免回流和重绘。比如利用GPU加速来实现样式修改,transform/opacity/filters这些属性的修改都不是在主线程完成的,不会重绘,更不会回流。
结语
把“URL输入到渲染”整个过程讲完,回到开头几个比较刁钻的问题,在文中都不难找到答案:
浏览器将输入内容解析后,拼接成完整的URL,其中的参数使用的是UTF-8编码,也就是我们开发时会常用的encodeURI和encodeURIComponent两个函数,其中encodeURI是对完整URL编码,encodeURIComponent是对URL参数部分编码,要求会更严格;
浏览器缓存的disk cache和memory cache分别是从磁盘读取和从内存中读取,通常刷新页面会直接从内存读,而关闭tab后重新打开是从磁盘读;
预加载prefetch是在空闲时间,以低优先级加载后续页面用到的资源;而preload是以高优先级提前加载当前页面需要的资源;
脚本的async是指异步加载,完成加载立刻执行,defer是异步加载,完成HTML解析后再执行;
TCP握手需要三次的三次是为了保证客户端的存活,防止服务端资源的浪费,挥手要四次是因为TCP是双工通信,每一个方向的连接释放、应答各需要一次;
HTTPS的握手是为了协商出一个对称密钥,双方一共发送三个随机数,利用这三个随机数计算出只有双方知道的密钥,正式通信的内容都是用这个密钥进行加密的;
The text was updated successfully, but these errors were encountered: