从输入url到看到页面经历了些什么(二)——浏览器渲染

浏览器架构

浏览器一般由以下几部分构成:

  • 用户界面:除了主窗口渲染界面之外的其他显示部分
  • 浏览器引擎:主控用户界面和渲染引擎,为它们之间传送指令
  • 渲染引擎:负责显示请求的内容(解析html和css等)
  • 网络模块:处理网络请求,同时还提供获得的文档的缓存,为所有的平台提供底层实现
  • js解释器:解释和执行js代码(著名的v8引擎)
  • 用户界面后端:绘制基本的窗口小部件,比如组合框和窗口,在底层使用的是操作系统的用户界面方法,并且公开通用的接口
  • 数据存储:管理用户数据,比如书签,偏好设置等

上面的组成部分是之前看博客的时候看到的,最近看了一下浏览器原理方面的文章,接下来从进程与线程的角度对浏览器架构进行进一步分析。参考文章如下:
浏览器内部揭秘(1-4)
有大大们翻译了中文版本:
现代浏览器内部揭秘(一)
现代浏览器内部揭秘(二)
现代浏览器内部揭秘(三)
现代浏览器内部揭秘(四)

浏览器一直都在致力于如何提供更好的用户体验,明白浏览器如何处理开发者写的代码,能够反过来为用户提供更多友好体验。

在此简单介绍一下,首先是进程和线程的概念:进程可以被描述为是一个应用的执行程序,线程存在于进程并执行程序任意部分。如果两个进程需要进行对话,可以通过进程间通信(IPC)来进行。

通常会有疑问,浏览器是单进程还是多进程?

A:浏览器可能是一个拥有很多线程的进程,也可能是一些通过IPC通信的不同线程的进程,Chrome浏览器属于多渲染进程架构。Chrome进程主要包括浏览器进程,渲染进程,插件进程和GPU进程四个部分:

  1. 浏览器进程:控制应用中的“Chrome”部分,包括地址栏,书签,回退和前进按钮等,还处理一些不可见的操作例如网络请求与文件访问等。
  2. 渲染进程:控制标签页内的网站展示,简单来说就是每个标签都有自己的渲染进程,这样就算其中一个标签失去响应,也不会影响其他标签的正常使用
  3. 插件进程:控制站点使用的任意插件
  4. GPU进程:处理独立于其他进程的GPU任务。因为GPU会被分成不同进程,处理来自不同应用的请求并绘制在相同表面
  5. 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里,最近独立出来成为一个单独的进程

多进程的好处:

  • 安全性和沙箱化:由于操作系统提供了限制进程权限的方法,浏览器就可以用沙箱保护某些特定功能的进程
  • 沙箱看成是操作系统给进程上了一把锁,沙箱里面的程序可以运行,但是不能在你的硬盘上写入任何数据,也不能在敏感位置读取任何数据

多进程的缺点:

  • 进程有自己的私有内存空间,所以一些公共部分只能被拷贝不能像线程之间一样共享,所以就会消耗更多内存

从浏览器内部处理逻辑的角度再来看看从输入url到看到页面浏览器是怎么工作的;

  1. 处理输入:UI线程会询问输入的是url地址还是一个查询条件(chrome地址栏也可以作为搜索栏操作),此时UI线程需要解析和决定把请求发送到搜索引擎或者要请求的网站中去。
  2. 开始导航:UI线程通知网络进程调取去获取站点内容,网络进程开始工作,比如建立连接,下载资源等
  3. 读取响应:数据返回时网络进程需要判断响应报文,如果响应是个HTML文件,下一步就会将数据传给渲染进程,如果是一个压缩文件或者其他文件,就需要将数据传递给下载管理器,这个时候也会进行安全检查和CORB(cross origin read blocking)检查,以保证敏感的跨域数据不被传给渲染进程
  4. 查找渲染进程:一旦检查执行完毕而且网络线程确信浏览器能够导航到请求的站点,网络线程会告诉UI线程所有的数据准备完成,UI线程会寻找渲染进程,开始渲染web页面,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程
  5. 提交导航:数据和渲染进程都准备就绪时,浏览器进程会发送一个IPC到渲染进程去提交导航。它也会传递数据流,所以渲染进程可以保持接收HTML数据,一旦浏览器进程收到渲染进程已经提交的确认信息,导航完毕并且文档加载解析开始
  6. 额外的步骤:初始加载完毕:一旦导航被提交,渲染进程开始加载资源和渲染页面,一旦渲染进程渲染完毕,会发送一个IPC返回给浏览器进程,此时UI线程停止标签页上的加载动画,但是客户端js可以在此时仍然加载额外资源并重新渲染视图

了解浏览器组成之后,接下来就到了浏览器渲染过程了。

1.概述

在这里插入图片描述

如图所示是webkit的渲染过程,

  1. 渲染引擎解析HTML文档以及CSS文档,生成对应的DOM和CSSOM
  2. 两棵树将合成为一棵渲染树,树中有很多的组成部分,这些组成部分被称为节点
  3. 渲染树构建完毕后,为每个节点分配一个应该出现在屏幕上的确切坐标,这个阶段称为布局
  4. 找到每个节点的位置后,渲染引擎会遍历渲染树,将每个节点的细节绘制出来
  5. 最后将画面显示出来
1.1 解析阶段

渲染进程的核心是将HTML,CSS和Javascript转换为用户可以与之交互的网页,渲染进程内部包括主线程,工作线程,合成线程和光栅线程。

在实际应用中,解析阶段主要是主线程在工作。渲染引擎会先解析HTML头部代码,下载样式表,下载的同时HTML会继续解析,等到解析完成之后,构造生成DOM树,开始解析下载好的CSS,构造CSSOM。
这个时候主线程解析CSS并确定每个DOM节点计算后的样式(没下载完不能开始构建)。样式计算的目的是为了计算DOM节点中的每个元素的具体样式,大体分为三个步骤:

  1. 把CSS转换为浏览器能够理解的结构(渲染引擎接收到CSS文本时,会执行转换操作,将CSS文本转换为styleSheets)
  2. 转换样式表中的属性值,使其标准化(将不容易被渲染引擎理解的属性标准化,2em->32px这种)
  3. 计算出DOM树中每个节点的具体样式
    等两棵树都解析完了(两棵树是可以并行解析的),就开始进行渲染树的构造。为了加快速度,预加载扫描器(preload scanner)会在主线程处理外部资源请求时同时运行,当HTML文档中有<img>和<link>之类的内容,预加载扫描器会查看由HTML解析器生成的标记,并在浏览器进程中向网络线程发送请求。

如果当中遇到JS代码,DOM的解析就会被迫停止,因为浏览器认为js代码可能会修改DOM结构,所以需要等到js执行完毕之后,DOM才会接着解析

还会出现一种特殊情况,就是当CSSOM还没下载完成的时候,HTML解析时遇到js代码,这个时候浏览器会暂停脚本执行,直到CSS下载完,并完成CSSOM构建再继续执行

关于HTML和CSS是怎么解析的可以看看这篇文章

1.2 布局阶段

布局是个递归计算元素几何形状的过程,它会从根节点开始,遍历部分或者所有的框架层次结构,界面首次渲染的时候肯定会全部遍历,但是当发生一些细小变更时,全部进行重新排布就会消耗性能,因此浏览器采取了一种“dirty位”系统,如果某个节点发生了变更,就把它以及它的子孙们都标记成"dirty",将标记的这部分进行重新布局(重排),还会进行重新的绘制,标记有两种:“dirty”和“children are dirty”。“children are dirty”表示尽管呈现器自身没有变化,但它至少有一个子代需要布局。

重排一般是异步触发,但是请求样式信息(例如“offsetHeight”)或者操作DOM的脚本会导致重排的同步触发,因为如果操作了DOM,后面的JS又不能获取最新的DOM,就会出现bug。但是这个特性也会因此带来一些阻塞,因为DOM没渲染完成之前JS就不能继续。(例子是React的diff,解决方法是fiber,这个下回分解)

重排的具体触发条件:

  • DOM树结构的变化(DOM的添加,删除等)
  • 请求样式信息:比如offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle()等,可以在获取时做适当的缓存来减少重排
  • 未脱离文档流的情况下改变了元素的位置
  • DOM元素的几何属性变化(宽高,margin之类的)
  • 元素内容的改变
  • 元素的缩放,旋转,或者给元素添加动画

总结起来就是元素位置会变的时候,和原来布局不一样了,所以要重新布局

1.3 绘制阶段

绘制阶段,主线程会遍历呈现树,并调用渲染引擎的“paint”方法,将内容显示在屏幕上,绘制时元素会进入堆栈样式上下文,然后从后往前绘制,绘制的顺序如下(进入堆栈的顺序和这个刚好相反):

  1. 轮廓
  2. 子代
  3. 边框
  4. 背景图片
  5. 背景颜色

一般来说,当DOM修改只是导致样式变化,并没有影响几何属性,节点位置不会发生改变的时候,渲染引擎会跳过布局阶段,直接进入绘制阶段,对元素进行重新绘制(重绘),也就是说一些简单的样式变化,就会触发重绘,包括但不仅限于以下几种情况:

  1. 背景颜色,字体颜色的改变
  2. visibility和opacity这种不会改变宽高,大小的样式
1.4 页面合成

浏览器已经知道文档结构,每个元素的样式,页面的几何形状和绘制顺序后,下一步就是将这些信息转换为屏幕上的像素,这个过程称之为光栅化。最原始处理这种情况的方法是,先在光栅化视窗内的画面,如果用户滚动页面,则移动光栅框,并光栅化填充缺少的部分。但是,现代浏览器会运行一种被称之为“合成”的方式对信息进行光栅化。

合成是一种将页面各部分分层,分别光栅化,并在合成线程中合成为页面的技术

如果此时发生滚动,由于图层已经光栅化,要做的只是合成一个新帧,动画也可以用相同的方式(移动图层和合成新帧)实现。

主线程为了区分哪些元素位于哪些图层,会遍历布局树创建图层树,当图层树被创建并且确定绘制顺序之后主线程就会将该信息提交给合成线程,进行每个图层的光栅化。当图层过大时,合成线程还会将它们分块后发送给光栅线程,由光栅线程光栅化每个小块后存储在显存中。

合成线程会给不同的光栅线程设置优先级,同时还具有多个不同分辨率的快,可以处理放大操作等动作,一旦块被光栅化,合成线程就会收集这些块的信息(这个过程称为绘制四边形)创建合成帧(一个绘制四边形的集合,代表一个页面的一帧)

接着,合成帧通过IPC提交给浏览器进程,同时可通过 UI 线程或其他插件的渲染进程中添加另一个合成帧。将这些合成器帧都发送到发送到 GPU ,显示到屏幕上。如果接收到滚动事件,合成线程会创建另一个合成帧发送到 GPU

合成的好处就是它可以在不涉及主线程的情况下完成,这也就是为什么css动画会比js性能好的原因

2.如何提高网站性能

本节基于《高性能网站建设指南》一书,对里面列举的十四个性能优化点进行阐述

性能黄金法则

黄金法则:只有10%20%的最终用户响应时间花在了下载HTML文档上,其余的80%90%时间花在了下载页面中的所有组件上
一共有14条提升性能的规则

规则1:减少HTTP请求——用户第一次访问网站时能更有效地减少HTTP请求的数量

主要包括图片地图,CSS Spirites,内联图片和脚本,样式表的合并

图片地图(Image Map)

允许在一个图片上关联多个url,目标url的选择取决于用户点击了图片的哪个位置
图片地图有两种类型:
1.服务器端图片地图(Server-side Image Map):将所有点击提交到同一个目标url,向其传递用户点的x,y坐标,web应用程序将该x,y坐标映射为适当的操作
2.客户端图片地图(Client-side Image Map):将用户的点击映射到一个操作,无需向后端应用程序发送请求,映射通过HTML的MAP标签实现

 <img usemap="#map1" border="0" src="/">
 <map name="map1">
  <area shape="rect" coords="" href="" title="">
 <area shape="rect" coords="" href="" title="">
 </map>

图片地图的缺点:
1.定位图片地图上的区域坐标时,如果采取手工方式则很难完成且容易出错,而且除了矩形外几乎无法定义其他形状
2.通过DHTML创建的图片地图则在IE中无法工作

CSS Sprites

将多幅图片合并为一幅单独的图片,使用css的background-position属性,可以将HTML元素放置到背景图片中期望的位置上
CSS Sprites的优点:
1.通过合并图片减少HTTP请求,并且比图片地图更灵活
2.降低了下载量,合并后的图片会比分离的图片的总和要小,因为降低了图片自身的开销(颜色表,格式信息等)

合并脚本和样式表

内联图片

使用data:URL模式可以在Web页面中包含图片但无需任何额外的HTTP请求
缺陷:1.ie7之前不受ie支持,2.可能会存在数据大小上的限制
** iconfont**
采用字体的方式来做图标,将图标作为一个透明的矢量图,这样可以通过font-size和color进行图标的改变,文件的体积也会变小

规则2:使用内容发布网络(content delivery network)

内容发布网络(CDN):是一组分布在多个不同地理位置的web服务器,用于更加有效地向用户发布内容,在优化性能时,向特定用户发布内容的服务器的选择基于对网络可用度的测量
优点:缩短响应时间,还可备份,扩展存储能力,进行缓存
缺点:响应时间可能会受到其他网站的影响
CDN用于发布静态内容

规则3:添加Expires头

浏览器(和代理)使用缓存来减少HTTP请求的数量,并减少HTTP响应的大小,使web页面加载得更快
web服务器使用Expires头来告诉web客户端它可以使用一个组件的当前副本,直到指定的时间为止

另一种缓存的选择,HTTP1.1的Cache-Control头可以克服Expires头的限制,因为Expires头使用一个特定的时间,要求服务器和客户端的时钟严格同步,而且过期日期需要经常检查,Cache-Control头可以使用max-age指令指定组件被缓存多久,以秒为单位定义了一个更新窗,两者同时出现的话Expires会被覆盖

Cache-Control: max-age=31536000

不支持Cache-Control的浏览器还是要用Expires头
跨浏览器改善缓存的最佳解决方案就是使用由ExpiresDefault设置的Expries头,
用户为了获取最新版本的组件,最有效的解决方案就是修改其所有链接

规则4:压缩组件

通过减少HTTP响应的大小来减少响应时间,最简单的方式是利用gzip编码来压缩HTTP响应包,gzip是GNU项目开发的一种免费的格式,并被标准化为RFC1952
方式:

Accept-Encoding: gzip, default

压缩的内容:HTML文档,脚本以及样式表,图片和PDF不应该被压缩,因为本身就被压缩过,再压缩反而耗费CPU,还可能增加文件大小
压缩的成本:1.服务器端会花费额外的CPU周期来完成压缩,2.客户端要对压缩文件进行解压缩
当浏览器通过代理发送请求时,当有来源于既有支持gzip又有不支持gzip的同个请求时,代理缓存就可能会混乱,解决方法:在服务器的Vary响应头中包含Accept-Encoding,即:

Vary:Accept-Encoding

当并不清楚浏览器是否支持缓存时,使用浏览器白名单:只为已经证实过支持压缩的浏览器提供压缩内容,在有代理的前提下,可以将User-Agent加到Vary头中,即:

Vary:Accept-Encoding,User-Agent

根据使用场景选择适合的方式

  1. 网站用户少,且浏览器环境差别不大,这时边缘情况可以不考虑,可以通过压缩组件改善用户体验
  2. 如果更加注重的是带宽开销,也可以通过压缩组件的方式降低开销,提高代理处理的请求数量
  3. 如果有大量用户,且本身能应付大的带宽开销,压缩内容时使用Cache-Control:Private,这禁用代理缓存但避免了边缘情形缺陷
规则5:将样式表放在顶部

将样式放在文档底部会导致浏览器中阻止用户内容逐步呈现,为避免样式变化时重绘页面,浏览器会阻止内容的逐步呈现,这样在等待位于底部的样式表时,内容不能被呈现,就会造成白屏

使用@import进行样式加载会导致组件下载时的无序性,所以尽可能使用<link >

如果样式表不是呈现页面必须的,可以想办法在文档加载完毕之后动态加载进去

规则6:将脚本放在底部

对响应时间影响最大的事页面中组件的数量,当缓存为空时,每个组件都会产生一个HTTP请求,HTTP1.1规范建议浏览器从每个主机名并行地下载两个组件

脚本是会阻塞下载的,具体的分析可以看看上文的浏览器渲染原理中的解析阶段

规则7:避免CSS表达式

当页面发生改变时,CSS表达式会重新求值,对CSS表达式的频繁求值也会导致CSS表达式的低下性能

尽可能使用一次性表达式或者利用事件处理器来操作CSS属性

规则8:使用外部的js和CSS
规则9:减少DNS查找
规则10:精简javascript

可以通过混淆的方式进行代码的精简(比如Uglify)

规则11:避免重定向

重定向在实际应用中都不会有缓存,除非有附加的Expires或者Cache-control等头

规则12:删除重复脚本
规则13:配置ETag

ETag(实体标签):Web服务器和浏览器用于确认缓存组件有效性的一种机制,用于检测浏览器缓存中的组件与原始服务器上的组件是否匹配

规则14:使用Ajax可缓存

Ajax的目的是为了突破web本质的开始-停止交互方式,它在UI和服务器之间插入了一层,Ajax层位于客户端,与服务器进行交互以获取请求的信息,并与表现层交互,仅更新必要的组件。它将Web体验从“浏览页面”转化成了“与应用程序进行交互”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值