Z从输入URL到页面展示,这中间发生了什么
首先来看一下“从输入URL到页面展示完整流程的示意图”
渲染进程渲染页面流程
再来看一下其中“页面渲染流程图”
输入URL到页面展示总结
接下来就大致描述下这个过程:
-
首先,用户输入URL
-
浏览器检查的是否为URL,如果是URL则根据规则,在这段内容加上协议,合为完整的URL
-
浏览器进程通过进程间通信(IPC)将URL发送给网络进程。
-
网络进程请求后检查本地缓存是否缓存了该请求的资源,如果有则返回该资源给浏览器进程。
-
如果没有就向服务器发送http请求,请求流程如下:
- 进行DNS解析,获得请求域名的IP地址
- 利用IP地址建立TCP连接,如果是
HTTPS
则还会建立TLS
连接 - 构建并发送请求头、请求体
- 接收响应头和响应报文并解析响应内容
-
网络进程解析响应内容的流程
- 检查响应内容中的状态码
- 如果是301/302 会重定向至其他URL,这时网络进程会读取
Location
字段里读区重定向地址,然后重新再来。例如:在当你使用HTTP
请求使用HTTPS
的网站时,会给你重定向至HTTPS
的网址 - 如果是2xx 的状态码,表示浏览器可以继续处理该请求,处理时会处理响应头会有一些字段标志响应体的内容的一些状态。不同的
Content-Type
响应头类型处理的流程不同。
- 如果是301/302 会重定向至其他URL,这时网络进程会读取
- 检查响应内容中的状态码
-
准备渲染进程
- 渲染进程会判断当前的页面是否与之前的已渲染的进程页面是否属于同一站点
- 如果不属于同一站点,则开启新的渲染进程
- 如果属于同一站点,则复用之前的渲染进程
- 此时渲染进程准备好等待网络进程传输数据
- 渲染进程会判断当前的页面是否与之前的已渲染的进程页面是否属于同一站点
-
提交文档
- 当浏览器进程接收到网络进程的响应头数据后,就向渲染进程发起
提交文档
- 渲染进程接收到提交文档信息后,会和网络进程建立传送数据的“管道”
- 等文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器的界面状态,包括安全状态,地址栏的URL,前进后退的历史状态,并更新Web页面
- 当浏览器进程接收到网络进程的响应头数据后,就向渲染进程发起
-
开始渲染
- 渲染进程会将
HTML
转化成浏览器能读懂的DOM树 - 渲染引擎转换
CSS
样式表- 通过格式化样式表转换成成浏览器可以理解的styleSheets
- 标准化样式属性将各种样式单位进行统一
- 最后计算每个节点的具体样式
- 创建布局树
- 遍历DOM节点,将DOM节点放到布局树中
- 计算布局树节点的坐标
- 对DOM节点布局完后,会对特定的节点进行分层,构建一颗图层树
- 对每个图层进行绘制列表,并提交给合成线程
- 合成线程会将图层分为图块,并在光栅化线程池中将图块转化为位图
- 合成线程发送绘制图块命令DrawQuad给浏览器进程
- 浏览器进程根据DrawQuad消息生成页面,并显示在显示器上
- 渲染进程会将
搬运文章
浏览器缓存
什么是浏览器缓存
MDN解释:
A browser cache holds all documents downloaded via HTTP by the user ... without requiring an additional trip to the server.
浏览器缓存者用户通过HTTP获取的所有资源,在下一次请求时可以避免重复向服务器发送出多余请求
缓存分类
一般浏览器缓存可以分为两类:
- 强缓存:不会向服务器发送请求,直接从缓存中读取资源
- 协商缓存:协商缓存是强制缓存失效后,浏览器携带缓存标识向服务器发出请求,由浏览器根据缓存标识决定是否使用缓存的过程
浏览器在加载资源时,会先判断是否命中强缓存再验证是否命中协商缓存。
强缓存
强制缓存是向浏览器缓存查找缓存,根据缓存规则决定是否使用缓存结果的过程。强制缓存有三种情况
- 未找到缓存结果和标识,强制缓存失效。直接向服务端发送请求。
- 存在缓存标识和结果,但是已经失效,强制缓存失效。携带资源标识,发起协商缓存。
- 存在缓存结果和标识,且结果未失效,强制缓存生效,返回结果。
那么缓存规则是如何的?
当浏览器向服务器发起请求时,服务器将缓存规则放入HTTP响应报文的HTTP和请求结果一起返回给浏览器,控制强制缓存的字段时Expires
、Cache-Control
-
Expires
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,
Expires=max-age + 请求时间
,需要和Last-modified
结合使用。Expires
是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
-
Cache-Control
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存,主要取值为:
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:所有内容只有客户端可以缓存,
Cache-Control
的默认取值 - no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定
- no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存
- max-age=xxx (xxx is numeric):缓存内容将在xxx秒后失效
需要注意的是,
no-cache
这个名字有一点误导。设置了no-cache
之后,并不是说浏览器就不再缓存数据,只是浏览器在使用缓存数据时,需要先确认一下数据是否还跟服务器保持一致,也就是协商缓存。而no-store
才表示不会被缓存,即不使用强制缓存,也不使用协商缓存
浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?这就是下面我们要讲到的from disk cache
和from memory cache
。
Chrome的网络请求的Size会出现三种情况from disk cache(磁盘缓存)
、from memory cache(内存缓存)
、以及资源大小数值。
状态 | 类型 | 说明 |
---|---|---|
200 | form memory cache | 不请求网络资源,资源在内存当中,一般脚本、字体、图片会存在内存当中 |
200 | form disk ceche | 不请求网络资源,在磁盘当中,一般非脚本会存在内存当中,如css等 |
200 | 资源大小数值 | 从服务器下载最新资源 |
304 | 报文大小 | 请求服务端发现资源没有更新,使用本地资源 |
简单的对比一下
协商缓存
协商缓存就是浏览器存在缓存但缓存已失效,于是浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存。主要有两种情况:
- 服务端返回
304
或者Not Modified
,协商缓存生效
- 服务端返回
200
以及请求结果
,协商缓存失效
那么服务端是如何验证资源是否更新呢
Last-Modified
和If-Modified-Since
验证流程:
- 浏览器第一次请求资源的时候,服务端返回Header中会带有一个
Last-Modified
字段,表示资源最后修改时间。 - 当浏览器再次请求该资源的时候,请求头中会带有
If-Modified-Since(保存Last-Modified的值)
字段。服务端收到这个请求后,将If-Modified-Since
与当前的最后修改时间进行对比。如果相等则可以协商缓存。不相等则将资源重新响应。
由于last-modified依赖的是保存的绝对时间,还是会出现误差的情况:
- 保存的时间是以秒为单位的,1秒内多次修改是无法捕捉到的;
- 各机器读取到的时间不一致,就有出现误差的可能性。为了改善这个问题,提出了使用
etag
。
ETag
和If-None-Match
etag
是http
协议提供的若干机制中的一种Web
缓存验证机制,并且允许客户端进行缓存协商。生成etag常用的方法包括对资源内容使用抗碰撞散列函数,使用最近修改的时间戳的哈希值,甚至只是一个版本号。 和last-modified
一样.
- 浏览器会先发送一个请求得到
etag
的值,然后再下一次请求在request header
中带上if-none-match
:[保存的etag的值]
。 - 通过发送的
etag
的值和服务端重新生成的etag
的值进行比对,如果一致代表资源没有改变,服务端返回正文为空的响应,告诉浏览器从缓存中读取资源。
etag能够解决last-modified的一些缺点,但是etag每次服务端生成都需要进行读写操作,而last-modified只需要读取操作,从这方面来看,etag的消耗是更大的。
二者对比
- 精确度上:
Etag
要优于Last-Modified
。 - 优先级上:服务器校验优先考虑
Etag
。 - 性能上:
Etag
要逊于Last-Modified
总结
当浏览器再次访问一个已经访问过的资源时,它会这样做:
- 看看是否命中强缓存,如果命中,就直接使用缓存了;
- 如果没有命中强缓存,就发请求到服务器检查是否命中协商缓存;
- 如果命中协商缓存,服务器会返回
304
告诉浏览器使用本地缓存; - 否则,返回最新的资源。
搬运文章
谈谈你对重绘和回流的理解
回流
当渲染树中的元素布局发生了改变,会产生回流。
具体一点:
- 添加或删除可见的DOM元素
- 元素的位置发生改变
- 元素的尺寸改变(包括:内外边距、边框厚度、宽度、高度等属性的改变)
- 读写
offset
族、scroll
族和client
族属性的时候,浏览器为了获取这些值,需要进行回流操作。
依照上面总结的渲染流程图,触发回流的时候,如果DOM结构发生改变,则重新渲染DOM树,然后将后面的流程全部走一遍
重绘
当渲染树中元素外观发生了改变,会发生重绘
由于没有发生DOM几何属性的变化,因此元素的位置信息不需要更新,从而省去了布局的流程
合成
那如果你更改⼀个既不要布局也不要绘制的属性,会发⽣什么变化呢?渲染引擎将跳过布局和绘制,只执⾏后续的合成操作,我们把这个过程叫做合成。具体流程参考下图:
利用CSS的transrom
、opacity
、filter
可以实现合成效果。直接跳过布局绘制,通过GPU
进行生成,不占用主线程。
如何最小化重绘和回流的影响呢?
- 使用
class
操作样式,而不是通过style - 避免使用
table
布局 - 对于 resize、scroll 等进行防抖/节流处理。
- 尽量使用CSS属性简写,比如:用 border 代替 border-width, border-style, border-color
能不能说一说浏览器的本地存储?各自优劣如何?
浏览器本地存储主要分为三种:
- Cookie
- WebStorage
- localStorage
- sessionStorage
- IndexedDB
Cookie
Cookie本质上是一个浏览器内部存储的文本文件,内部以键值对的方式存储。向同一个域名发送请求,都会向服务器发送Cookie,服务器拿到Cookie进行解析,解析完成就会拿到客户端的状态。
Cookie 是一个状态存储文件,但他也有很多缺陷:
- 容量缺陷:Cookie是一个只有4Kb大小的文件,只能存储少量信息
- 性能缺陷:Cookie紧跟域名,不管域名下的某个地址需不需要Cookie,都会发送,这就造成了性能缺陷
- 安全缺陷:由于 Cookie 以纯文本的形式在浏览器和服务器中传递,很容易被非法用户截获,然后进行一系列的篡改,在 Cookie 的有效期内重新发送给服务器,这是相当危险的。另外,在
HttpOnly
为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
Cookie 与 Session 的区别
Session 是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中; Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。
WebStorage
webStorage 又分为 localStorage
和 sessionStorage
localStorage
localStorage 与Cookie有些相同,localStorage也是针对一个域名,同一个域名下会存储一个localStorage。他有以下特性
- 容量:localStorage容量上限为5M,一个域名下可以有一个持续化存储的的localStorage
- 只存在客户端:默认不参与通信
- 接口封装:通过
localStorage
暴露在全局,并通过它的setItem
和getItem
等方法进行操作,非常方便。
sessionStorage
sessionStorage 有 localStorage的以上优点,但有一点不同的是:
sessionStorage
只是一个会话存储,如果回话结束,浏览器关闭那么就会消失
IndexedDB
IndexedDB
是一个运行在浏览器上的非关系型数据库,理论上存储是没有上限的。
IndexedDB的一些重要特性:
- 键值对存储。内部采用
对象仓库
存放数据,在这个对象仓库中数据采用键值对的方式来存储。 - 异步操作。数据库的读写属于 I/O 操作, 浏览器中对异步 I/O 提供了支持。
- 受同源策略限制,即无法访问跨域的数据库。
搬运文章
V8引擎机制
要深⼊理解V8的⼯作原理,你需要搞清楚⼀些概念和原理,⽐如接下来我们要详细讲解的编译器(compiler)、解释器(interpreter)、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT)等。
V8是如何执行一段JavaScript代码的
先看整体流程图
-
生成抽象语法树(AST)和执行上下文:
将源代码进行结构化表示成解释器和编译器能够读懂的AST,有了AST后就会生成代码的执行上下文。生成AST又会有两个阶段:
- 分词:又称为词法分析,将源码分为一个个不能再分字符串,就是所谓的
token
- 解析:又称为语法分析,将上一步生成的
token
根据语法规则转化为AST
- 分词:又称为词法分析,将源码分为一个个不能再分字符串,就是所谓的
-
生成字节码:
为了让机器更快速的运行程序但却不像机器码一样占用过大的内存,通过解释器(Ignition)将AST转化成介于AST和机器码中间的字节码
-
执行代码
在执行字节码的过程中,如果发现一段代码被多次执行,那么后台编译器(TurboFan)会将字节码编译为更高效的机器码。那其实解释器(Lgnition)和编译器(TurboFan)这样相结合就是即时编译(JIT)
V8的垃圾回收
JS引擎中对变量的存储主要有两种位置,栈内存和堆内存,栈内存存储基本类型数据以及引用类型数据的内存地址,堆内存储存引用类型的数据。
栈内存
栈内存的垃圾回收相对来说较简单,栈内存调用栈上下文切换后就会被回收。
堆内存
堆内存会更加复杂,V8将堆内存分为新生代内存和老生代内存,新生代内存是临时分配的内存,存在时间短,老生代内存存在时间长
新生代内存回收机制:
- 新生代内存容量小,64位系统下仅有32M。新生代内存分为From、To两部分,进行垃圾回收时,先扫描From,将非存活对象回收,将存活对象顺序复制到To中,之后调换From/To,等待下一次回收
老生代内存回收机制
- 晋升:如果新生代的变量经过多次回收依然存在,那么就会被放入老生代内存中
- 标记清除:老生代内存会先遍历所有对象并打上标记,然后对正在使用或被强引用的对象取消标记,回收被标记的对象
- 整理内存碎片:把对象挪到内存的一端
内存泄漏
由于我们代码的写法不当,会造成内存泄漏问题
1. 意外的全局变量
function foo(arg) {
bar = "this is a hidden global variable";
}
bar没被声明,会变成一个全局变量,在页面关闭之前不会被释放。
另一种意外的全局变量可能由 this
创建:
function foo() {
this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();
在 JavaScript 文件头部加上 'use strict',可以避免此类错误发生。启用严格模式解析 JavaScript ,避免意外的全局变量。
2.被遗忘的计时器或回调函数
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);
//使用完毕的计时器需要及时clear
这样的代码很常见,如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放。
3.闭包
function bindEvent(){
var obj=document.createElement('xxx')
obj.onclick=function(){
// Even if it is a empty function
}
}
闭包可以维持函数内局部变量,使其得不到释放。上例定义事件回调时,由于是函数内定义函数,并且内部函数--事件回调引用外部函数,形成了闭包。
// 将事件处理函数定义在外面
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = onclickHandler
}
// 或者在定义事件处理函数的外部函数中,删除对dom的引用
function bindEvent() {
var obj = document.createElement('xxx')
obj.onclick = function() {
// Even if it is a empty function
}
obj = null
}
解决之道,将事件处理函数定义在外部,解除闭包,或者在定义事件处理函数的外部函数中,删除对dom的引用。
CSS 与 JS 阻塞DOM解析和渲染
我们常说CSS
放在头部JS
放在底部,这样可以提高性能,但具体为什么这样做却并不清楚。这里我们只说结论,如果想仔细看其中原理,可参考文末搬运文档
。
CSS
并不会阻塞DOM解析,但会DOM渲染JS
会阻塞DOM解析- 浏览器遇到
<script>
且没有defer
或async
属性的 标签时,会触发页面渲染,因而如果js
前面有CSS
资源尚未加载完毕时,浏览器会等待它加载完毕再执行脚本。
前端性能优化方式
图片性能优化
- 图标类型的图片,尽量使用 字体图标,减少网络传输资源的浪费。
- 响应式图片,通过不同的屏幕大小,自动加载不同的图片。
- 图片进行延迟加载。刚开始不加载图片,直到图片到达浏览器可视区域再加载图片。
使用DNS预解析
DNS解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的
IP
<link rel="dns-prefetch" href="//blog.poetries.top">
节流防抖
通过节流/防抖来减少HTTP的请求次数
减少重绘和回流
- 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
- 压缩DOM的深度,一个渲染层内不要有过深的子元素,少用DOM完成页面样式,多使用伪元素或者box-shadow取代。
- 将没用的元素设为不可见:
visibility: hidden
,这样可以减小重绘的压力,必要的时候再将元素显示。
善用缓存,不重复加载相同资源
为了避免用户每次访问网站都得请求文件,我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间,只要在这个时间之前,浏览器都不会请求文件,而是直接使用缓存。
静态资源使用CDN
让用户离服务器更近,从而缩短请求时间。
如果用户访问的网站部署了 CDN,过程是这样的:
- 浏览器要将域名解析为 IP 地址,所以需要向本地 DNS 发出请求。
- 本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求,得到全局负载均衡系统(GSLB)的 IP 地址。
- 本地 DNS 再向 GSLB 发出请求,GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置,筛选出距离用户较近的本地负载均衡系统(SLB),并将该 SLB 的 IP 地址作为结果返回给本地 DNS。
- 本地 DNS 将 SLB 的 IP 地址发回给浏览器,浏览器向 SLB 发出请求。
- SLB 根据浏览器请求的资源和地址,选出最优的缓存服务器发回给浏览器。
- 浏览器再根据 SLB 发回的地址重定向到缓存服务器。
- 如果缓存服务器有浏览器需要的资源,就将资源发回给浏览器。如果没有,就向源服务器请求资源,再发给浏览器并缓存在本地。