从浏览器发起请求到页面渲染出来的五个主要阶段
- DNS 查询
- TCP 连接
- HTTP 请求即响应
- 服务器响应
- 客户端渲染
DNS解析
浏览器端开始请求
输入提示
从浏览器地址框输入一个 g ,浏览器会根据你的历史访问,书签等,给出输入建议,假如说我以前打开过 google.com,浏览器就会根据它的算法匹配并给出几条建议地址。
url 解析
当协议或主机名不合法时,也就是不符合 URL 格式,比如输入几个单词,中文等。浏览器会将地址栏中输入的文字传给默认的搜索引擎。
合法的 URL 格式:
<scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
什么是DNS解析
简单来说,就是域名转换成ip地址的过程
为什么要DNS解析
因为 http 是基于 tcp 连接的,而 tcp 则是通过 ip 地址去识别访问的。所以一定要找到服务器的ip地址。
DNS域名解析过程
查找浏览器缓存
浏览器会检查缓存中有没有这个域名对应的解析过的IP地址,如果缓存中有,这个解析过程就将结束,没有则下一步。浏览器缓存域名也是有限制的,不仅浏览器缓存大小有限制,而且缓存的时间也有限制,通常情况下为几分钟到几小时不等。这个缓存时间太长和太短都不好,如果缓存时间太长,一旦域名被解析到的IP有变化,会导致被客户端缓存的域名无法解析到变化后的IP地址,以致该域名不能正常解析,这段时间内有可能会有一部分用户无法访问网站。如果时间设置太短,会导致用户每次访问网站都要重新解析一次域名。
打开
chrome://net-internals/#dns
即可查看本机浏览器的 dns 缓存。查找系统缓存
如果用户的浏览器缓存中没有,浏览器会查找操作系统缓存中是否有这个域名对应的DNS解析结果,浏览器会调用一个类似 gethostbyname 的库函数,此函数会先去检测本地 hosts 文件,查看是否有对应 ip。例如,
localhost
默认 ip 是172.0.0.1
。路由器缓存、ISP 缓存
如果浏览器和系统缓存都没有,系统的 gethostname 函数就会像 DNS 服务器发送请求。而网络服务一般都会先经过路由器以及网络服务商(电信),所以会先查询路由器缓存,然后再查询 ISP 的 DNS 缓存。也就是本地区的域名服务器,通常是提供给你接入互联网的应用提供商。这个专门的域名解析服务器性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。
递归搜索
最无奈的情况发生了, 在前面都没有办法命中的DNS缓存的情况下,(1)本地 DNS服务器即将该请求转发到互联网上的根域(.),通常省略不写。(2)根域将所要查询域名中的顶级域(.com, .org)的服务器IP地址返回到本地DNS。(3) 本地DNS根据返回的IP地址,再向顶级域(就是com域)发送请求。(4) com域服务器再将域名中的二级域(google.com)的IP地址返回给本地DNS。(5) 本地DNS再向二级域发送请求进行查询。(6) 之后不断重复这样的过程,直到本地DNS服务器得到最终的查询结果,并返回到主机。这时候主机才能通过域名访问该网站。
如果域名正常,应该就会返回 IP 地址,如果没有浏览器就会提示找不到服务器地址。
DNS有关的性能优化
DNS 查询的过程经历了很多的步骤,如果每次都如此,是不是会耗费太多的时间,资源。所以我们应该尽早的返回真实的 IP 地址,减少查询过程,也就是 DNS 缓存。浏览器获取到 IP 地址后,一般都会加到浏览器的缓存中,本地的 DNS 缓存服务器,也可以去记录。
减少DNS查找,避免重定向
- 服务器可以设置TTL值表示DNS记录的存活时间。本机DNS缓存将根据这个TTL值判断DNS记录什么时候被抛弃,这个TTL值一般都不会设置很大,主要是考虑到快速故障转移的问题。
- 浏览器DNS缓存也有自己的过期时间,这个时间是独立于本机DNS缓存的,相对也比较短,例如chrome只有1分钟左右。
- 浏览器DNS记录的数量也有限制,如果短时间内访问了大量不同域名的网站,则较早的DNS记录将被抛弃,必须重新查找。不过即使浏览器丢弃了DNS记录,操作系统的DNS缓存也有很大机率保留着该记录,这样可以避免通过网络查询而带来的延迟。
DNS的预解析
可以通过用meta信息来告知浏览器, 我这页面要做DNS预解析
1<meta http-equiv="x-dns-prefetch-control" content="on" />使用link标签来强制对DNS做预解析:
1<link rel="dns-prefetch" href="http://ke.qq.com/" />
TCP连接
待总结~~
HTTP请求
待总结~~
浏览器的渲染
渲染引擎
浏览器的主要组件
- 用户界面:除了浏览器主窗口显示的你请求的页面之外,其他的各个部分都属于用户界面。
- 渲染引擎:负责显示请求的内容。如果请求的内容是HTML,它就负责解析HTML和CSS内容,并将解析后的内容显示在屏幕上。
- 浏览器引擎:在用户界面与渲染引擎之间传送指令。
- 网络:用于网络调用,比如HTTP请求,其接口与平台无关,并为所有平台提供底层实现。
- 用户界面后端:用于绘制基本的窗口小部件,比如组合框和窗口。
- JavaScript解释器:用于解析和执行JavaScript代码。
- 数据存储
两种渲染引擎
Firefox 使用的是 Gecko,这是 Mozilla 公司“自制”的渲染引擎。而 Safari 和 Chrome 浏览器使用的都是 WebKit。
WebKit 是一种开放源代码渲染引擎,起初用于 Linux 平台,随后由 Apple 公司进行修改,从而支持苹果机和 Windows。
关键渲染路径
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并,计算可见节点形成
render tree
- 根据渲染树来布局,计算每个节点的位置
- 调用GPU绘制,合成图层,将每个节点转化为实际像素绘制到视口上(栅格化)
WebKit主流程
Firefox的Gecko呈现引擎的主流程
可以看出,虽然 WebKit 和 Gecko 使用的术语略有不同,但整体流程是基本相同的。
优化关键渲染路径就是指最大限度缩短执行上述第 1 步至第 5 步耗费的总时间。 这样一来,就能尽快将内容渲染到屏幕上,此外还能缩短首次渲染后屏幕刷新的时间,即为交互式内容实现更高的刷新率。
构建对象模型(Constructing the Object Model)
浏览器渲染页面前需要先构建 DOM 和 CSSOM 树。因此,我们需要确保尽快将 HTML 和 CSS 都提供给浏览器。
文档对象模型 (DOM)
1. **字节转换字符:** 浏览器从磁盘或网络读取 HTML 的原始字节,并根据文件的制定编码(如UTF-8)将它们转换成各个字符。
2. **令牌化:** 浏览器将字符串转换成 W3C HTML5标准规定的各种令牌,例如,``<html>``、``<body>``,以及其他尖括号内的字符串。每个令牌都有特殊的规则。
3. **词法分析:** 将令牌转换成定义其属性和规则的“对象”。
4. **DOM树构建:** 最后,由于HTML标记定义不同标记之间的关系,创建的对象连接在一个树数据结构内,此结构也会捕捉原始标记中定义的父-子关系。
整个流程最终输出的是我们的文档对象模型(DOM),浏览器对页面进行的所有进一步处理都会用到它。
CSS 对象模型 (CSSOM)
在浏览器构建我们的
DOM
时,在文档的 head 部分遇到了一个 link 标记,该标记引用一个外部 CSS 样式表。由于预见到需要利用该资源来渲染页面,它会立即发出对该资源的请求。与处理 HTML 时一样,我们需要将收到的 CSS 规则转换成某种浏览器能够理解和处理的东西,因此我们会重复构建对象模型过程。
CSS 字节转换为字符串,接着转换成令牌和节点,最后转换为一个 CSSOM 树结构内:
CSSOM 为何具有树结构?为页面上的任何对象计算最后一组样式时,浏览器都会先从适用于该节点的最通用规则开始(例如,如果该节点是 body 元素的子项,则应用所有 body 样式),然后通过应用更具体的规则(即规则“向下级联”)以递归方式优化计算的样式。
以上面的 CSSOM 树为例进行更具体的阐述。span 标记内包含的任何置于 body 元素内的文本都将具有 16 像素字号,并且颜色为红色 — font-size 指令从 body 向下级联至 span。不过,如果某个 span 标记是某个段落 (p) 标记的子项,则其内容将不会显示。
还请注意,以上树并非完整的 CSSOM 树,它只显示了我们决定在样式表中替换的样式。每个浏览器都提供一组默认样式(也称为“User Agent 样式”)。
渲染树(Render-Tree)构建、布局及绘制
CSSOM 树和 DOM 树合并成渲染树,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上。优化上述每一个步骤对实现最佳渲染性能至关重要。
第一步:将 DOM 和 CSSOM 合并成一个渲染树,网罗网页上所有可见的 DOM 内容,以及每个节点的所有 CSSOM 样式信息。
为构建渲染树,浏览器大体上完成了下列工作:
- 从DOM树的根节点开始遍历每个可见节点(脚本标记、元标记等,“display: none”属性)。
- 对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们(找的方式见上面的CSSOM讲解)。
- 连同可见节点内容和计算的样式发射该可见节点。
第二步:布局
有了渲染树,我们进入“布局阶段”,即计算节点们在设备视口内的确切位置和大小,也称为“自动重拍”。
为了弄清楚每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。渲染引擎会精确地捕获每个元素在视口内的确切位置和尺寸(所有相对测量值都会转换为屏幕上的绝对元素。
第三步:绘制(栅格化):将渲染树中的每个节点转换成屏幕上的实际像素
- 捕获渲染树以及元素位置和尺寸计算。
- 布局完成后,浏览器会立即调用 GPU 发出“Paint Setup”和“Paint”事件,将渲染树转换成屏幕上的像素。
执行渲染树构建、布局和绘制所需的时间将取决于文档大小、应用的样式,以及运行文档的设备:文档越大,浏览器需要完成的工作就越多;样式越复杂,绘制需要的时间就越长(例如,单色的绘制开销“较小”,而阴影的计算和渲染开销则要“大得多”)。
如果 DOM 或 CSSOM 被修改,您只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。
阻塞渲染的CSS(Render-Blocking CSS)
默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕(CSSOM 构建完毕才会开始渲染)。请务必精简您的 CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞。
在渲染树构建中,我们看到关键渲染路径要求我们同时具有 DOM 和 CSSOM 才能构建渲染树。这会给性能造成严重影响:HTML 和 CSS 都是阻塞渲染的资源。 HTML 显然是必需的,因为如果没有 DOM,我们就没有可渲染的内容,但 CSS 的必要性可能就不太明显,因为我们没有 CSS 也会渲染成一个网页(尽管样子很丑。。
CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。
例如:如果我们有一些 CSS 样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,又该如何?如果这些资源不阻塞渲染,该有多好。
我们可以通过 CSS“媒体类型”和“媒体查询”来解决这类用例:
12345<link href="style.css" rel="stylesheet"><link href="style.css" rel="stylesheet" media="all"><link href="print.css" rel="stylesheet" media="print"><link href="portrait.css" rel="stylesheet" media="orientation:portrait"><link href="other.css" rel="stylesheet" media="(min-width: 40em)">上面的第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况,也就是说,它始终会阻塞渲染。第二个声明同样阻塞渲染:“all”是默认类型,如果您不指定任何类型,则隐式设置为“all”。因此,第一个声明和第二个声明实际上是等效的。第三个样式表则不然,它只在打印内容时适用—或许您想重新安排布局、更改字体等等,因此在网页首次加载时,该样式表不需要阻塞渲染。第四个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。最后一个样式表声明提供由浏览器执行的“媒体查询”:符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。
最后,请注意“阻塞渲染”仅是指该CSS资源准备就绪了才开始网页的首次渲染。无论哪一种情况,浏览器仍会下载该 CSS,只不过不阻塞渲染的资源优先级较低罢了。
使用JavaScript添加交互
JavaScript可以查询和修改 DOM 和 CSSOM ,会阻止 DOM 构建和延缓网页渲染。
我们的脚本在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JavaScript 引擎;等 JavaScript 引擎运行完毕,浏览器会从中断的地方恢复 DOM 构建。也就是说:执行内联脚本会阻止 DOM 构建,也就延缓了首次渲染。同时,浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。
|
|
这样的 script 标签会阻塞 HTML 解析,无论是不是 inline-script。上面的 P 标签会从上到下解析,这个过程会被两段 JavaScript 分别打算一次(加载、执行)。
所以实际工程中,我们常常将 JavaScript 放到文档底部。
另外,因为 JavaScript 可以查询任意对象的样式,所以意味着在 CSS 解析完成,也就是 CSSOM 生成之后,JavaScript 才可以被执行。所以我们常常将 style 放到文档头部。
异步脚本
如果我们想让页面尽快显示,那我们可以使用异步脚本。HTML5 中定义了两个定义异步脚本的方法:defer 和 async。我们来看一看他们的区别。
- 同步脚本
|
|
当 HTML 文档被解析时如果遇见(同步)脚本,则停止解析,先去加载脚本,然后执行,执行结束后继续解析 HTML 文档。加载的脚本是外部 JavaScript 文件,浏览器必须停下来,等待从磁盘、缓存或远程服务器获取脚本,这就可能给关键渲染路径增加数十至数千毫秒的延迟。
- defer
|
|
defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。整个 document 解析完毕且 defer-script 也加载完成之后(这两件事情的顺序无关),会执行所有由 defer-script 加载的 JavaScript 代码,然后触发 DOMContentLoaded 事件。
defer 不会改变 script 中代码的执行顺序,示例代码会按照 1、2、3 的顺序执行。所以,defer 与相比普通 script,有两点区别:载入 JavaScript 文件时不阻塞 HTML 的解析,执行阶段被放到 HTML 标签解析完成之后。
- async
|
|
async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行。需要注意的是,这种方式加载的 JavaScript 依然会阻塞 load 事件。换句话说,async-script 可能在 DOMContentLoaded 触发之前或之后执行,但一定在 load 触发之前执行。
评估关键渲染路径
使用 Navigation Timing API与页面加载时触发的浏览器事件
- domInteractive 表示 DOM 准备就绪的时间点。
- domContentLoaded 一般表示 DOM 和 CSSOM 均准备就绪的时间点。
如果没有阻塞解析器的 JavaScript,则 DOMContentLoaded 将在 domInteractive 后立即触发。 - domComplete 表示网页及其所有子资源都准备就绪的时间点。
分析关键渲染路径
DevTools 会在底部报告 DOMContentLoaded 和 onload 事件的时间,DOMContentLoaded一般表示 DOM 和 CSSOM 均准备就绪的时间点(不包括图像的渲染),onload 事件标记的点是网页所需的所有资源均已下载并经过处理的点。
关键资源: 可能阻止网页首次渲染的资源
关键路径长度: 获取所有关键资源所需的往返次数或总时间。
关键字节: 实现网页首次渲染所需的总字节数,它是所有关键资源传送文件大小的总和。我们包含单个 HTML 页面的第一个示例包含一项关键资源(HTML 文档);关键路径长度也与 1 次网络往返相等(假设文件较小),而总关键字节数正好是 HTML 文档本身的传送大小。
- 最简单的网页(只包括 HTML 标记)
T0 与 T1 之间的时间捕获的是网络和服务器处理时间。
关键资源:html(1项)
关键最短路径:最少1次往返
关键字节:5kb
- 添加 CSS
关键资源:html + css(2项)
关键最短路径:最少2次往返
关键字节:9kb
- 添加 JavaScript (外联)
关键资源:html + css + JavaScript(3项)
关键最短路径:最少2次往返 (在获取html文档后才会获取css和js的地址)
关键字节:11kb(增加了JavaScript)
- 异步脚本(向 script 标记添加“async”属性来解除对解析器的阻止)
这种情况下,T2时就已经完成构建了所有的 DOM 和 CSSOM ,domContentLoaded 触发的时间变得更早
关键资源:html + css(2项,JavaScript没有必要阻止网页的首次渲染)
关键最短路径:最少2次往返 (在获取html文档后才会获取css和js的地址)
关键字节:9kb(减少了JavaScript)
优化关键路径
为尽快完成首次渲染,我们需要最大限度减小一下三种可变因素:
关键资源的数量
关键资源越少,浏览器的工作量就越小,对CPU以及其他资源的占用也就越少。
关键路径长度
关键路径长度越短,加载所需的往返次数就越少。
关键字节的数量
关键字节数越少,需要处理的内容就越少并让其出现在屏幕的速度就越快。
优化关键渲染路径的常规步骤如下
- 对关键路径进行分析和特性描述
- 最大限度减少关键资源的数量:删除、延迟下载、标记为异步等
- 优化关键字节数以缩短下载时间
- 优化其余关键资源的加载顺序:尽早下载所有关键资产,以缩短关键路径长度。
PageSpeed 规则和建议
消除阻塞渲染的 JavaScript 和 CSS
优化 JavaScript 的使用
- 首选使用异步 JavaScript 资源(使用async)
- 避免同步服务器调用(使用异步的
fetch
) - 延迟解析任何非必需的脚本 JavaScript(即对构建首次渲染的可见内容无关紧要的脚本)(使用defer)
- 避免运行时间长的 JavaScript
优化 CSS 的使用
- 将 CSS 置于文档 head 标签内
- 避免使用 CSS import(因为它们会在关键路径中增加往返次数)
- 内联阻塞渲染的 CSS(可实现“一次往返”关键路径长度)
静态文件优化
图片优化
使用base64编码代替图片
将图片转换为base64编码字符串inline到页面或css中,适用于图片大小小于2KB,页面上引用图片总数不多的情况。
合并图片sprite
将多个页面上用到的背景图片合并成一个大的图片在页面中引用 ,可以有效的减少请求个数,兼容性很好,但是增加开发时间、增加维护成本。
图片延迟加载(懒加载)
对应图片比较多的页面,可以考虑通过js来实现图片的延迟加载,先让一部分图片优先请求下来,当用户滚动页面的时候进一步加载图片。
页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,只有通过javascript设置了图片路径,浏览器才会发送请求。
懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,把正真的路径存在元素的“data-url”(这个名字起个自己认识好记的就行)属性里,要用的时候就取出来,再设置。实现步骤:
使用css、svg、canvas或iconfont代替图片
css方式可以用来绘制相对简单的图案来代替图片,一般使用before或者after伪元素来丰富图案的复杂度。使用css,svg,iconfont减少图片尺寸,请求数据少。
响应式图片
通过服务器图片资源配置命名规则来获取图片
<img src="bgimg-320.jpg" />或<img src="bgimg-480.jpg" />
通过css定义来加载不同的背景bg图片
123456@media only screen and (max-width : 480px) {.img {background-image: url(bg-480.jpg);}}@media only screen and (max-width : 360px) {.img {background-image: url(bg-360.jpg);}}Img的srcset和sizes的方法
这两个img属性是html5的属性,有浏览器的兼容问题
123456<imgclass="img"src="imgbg-320.jpg"srcset="imgbg-320.jpg 320w, imgbg-360.jpg 360w, imgbg-480px.jpg 480w"sizes="(max-width: 480px) 480px, 320px"/>src
:当设备不支持srcset,sizes属性时,使用这个图片
srcset指定图片的地址和对应的图片质量。sizes用来设置图片的尺寸零界点
[响应式图片srcset](https://www.zhangxinxu.com/wordpress/2014/10/responsive-images-srcset-size-w-descriptor/)
- picture标签实现
通过媒体查询的方式,根据页面宽度(当然也可以添加其他参考项)加载不同图片
12345
<picture> <source srcset="3.jpg" media="(min-width: 320px)"> <source srcset="2.jpg" media="(min-width: 480px)"> <img srcset="1.jpg"> </picture>
预加载
和图片的懒加载相反,图片预加载说白了就是将所有所需的图片提前请求加载到本地,这样后面在需要用到时就直接从缓存取图片。图片预加载的JavaScript实现原理很简单:new Image(),然后使用onload方法回调预加载完成事件,当浏览器把图片下载到本地后,之后同样的src就直接使用缓存。
提升静态文件的加载速度
减少请求数——文件合并
- 合并js脚本文件
- 合并css样式文件
- 合并css引用的图片(使用sprite雪碧图)
减少静态文件大小——代码压缩
我们在平时开发的时候,JS脚本文件和CSS样式文件中的代码,都会依据一定的代码规范(比如javascript-standard-style)来提高项目的可维护性,以及团队之间合作的效率。
但是在项目发布后, 这些代码是给客户端(浏览器)识别的,此时代码的命名规范、空格缩进都已没有必要,我们可以使用工具将这些代码进行混淆和压缩,减少静态文件的大小。
比如用
webpack
打包。gzip
CDN和缓存
CDN 是一个全球(或者只有国内,具体看供应商)分布式网络,它把网站内容更快地传递给服务范围内的一个具体位置,而往往这个具体的位置离实际的内容服务器距离很远。举个极端点的例子,你的网站主机在爱尔兰(海南),而你的用户则在澳大利亚(漠河)访问。这时当你的用户访问你的网站的时候,延迟会很大,把你的(静态)数据用 CDN 放到澳大利亚(漠河)则会很大程度上提高用户访问网站的体验。
静态资源尽量使用 CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个 CDN 域名。对于 CDN 加载静态资源需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie。