进程和线程
程序(program)
尚未载入计算机内存的代码,比如java的class字节码文件。
进程(process)
- 程序执行的实例
- 已经执行或加载到内存中的程序
- 进程中,每一行程序代码都可能被
CPU执行 - 是CPU进行资源分配的基本单位
线程(thread)
CPU基本调度单位- 一个或多个线程组成了进程,也就是单线程和多线程
具像化例子(可以跳过)
参考阮一峰大佬的文章
我对原文的例子做了一些修改。
如果用“工厂”来比喻的话:一个工厂就是一个CPU,承担了所有任务。
- 一个个车间:就是一个个进程,工厂分配资源的基本单位
- 车间里有工人:这些工人就是线程,工厂进行调度的基本单位
- 工人在车间里干活:进程是线程的容器
- 这个工厂只能安排工人来干活,车间只是干活的一个环境:线程是CPU的基本调度单位
- 一个车间里可以有很多工人:一个进程由多个线程组成
- 在同一个车间里面,每个工人都是共享信息的,工人A可以问工人B我们车间名叫啥:同一进程里的线程间是数据共享的
- 增加车间比增加工人更费钱:采用多进程比多线程更耗资源
- 车间A的工人摔倒了不会影响车间B的工人继续干活:进程间线程不会互相影响
- 但是同一车间里,其中一个工人摔倒了,就会影响到这个车间的生产:线程挂掉会影响进程的运行
- 车间生产出来的最终产品要打包,如果车间里只有一个房间可以打包,那么对于同一个产品,如果两个人一起打包,就会出现冲突,所以打包的这一步需要一个一个来,只能等前一个人弄完了才轮到你:一个线程使用某些共享内存时,需要等上一个线程结束了才能使用
- 为了防止工人不按规则一个一个打包,一哄而上,就需要在车间打包的那个房间门上加把锁,一个工人进去后反锁,其他人在外面排队:这个就是“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域
- 车间有公共厕所,一个厕所只有n个马桶,所以这个厕所每次最多只能n个工人同时用,其他人要用必须等前一个用完:进程中的某些共享内存区域,只能给固定数量的线程使用
- 为了防止工人们一起上厕所,哈哈哈哈,就需要给厕所用钥匙上锁,用完厕所的人把钥匙送回原位,后面排队的人根据这个钥匙量就知道能不能进了:这个就是“信号量”(Semaphore),用来保证多个线程不会互相冲突。
理解到这一步就可以了,接下来是更深入的几个点,可以跳过:
- 车间、工人只是这个工厂在运行过程中的某个时间段的表现:进程和线程都是一个时间段的描述,是CPU工作时间段的描述,只是颗粒度不同。
- 在这个工厂比较奇葩,工人们不能主动干活,是根据工厂管理层来安排的,管理层说你可以干活了你才可以动手,管理层说你不能干活你就啥也干不了,即使你在上厕所你也得憋着,等管理层说你可以继续了,你才能继续:CPU工作是根据时间片来分的,这一时刻执行这个线程,下一时刻执行另一个线程,下一时刻可能又回去执行刚刚的线程,这么反复一个一个轮流执行
- 具体执行过程为:车间A的工人a在干活,管理层让他停下,他就得出去工厂外等着,管理层记下他属于哪个车间,进行到哪一步了。然后管理层让工人b来干活,直到管理层让他停下,并记下属于哪个车间,进行到哪一步。工人a一直在工厂外等着,直到管理层喊他进来干活,告诉他在哪个车间从之前没做完的那一步继续做。记下工人们属于哪个车间进行到哪一步称为:保存线程的上下文
- 如果想要执行另一个车间的任务,管理层需要把当前车间的一大堆信息记下处理好,才能处理另一个车间的任务:所以切换进程比切换线程开销大
- 切换进程的过程为:先加载进程A的上下文,然后开始执行A,保存进程A的上下文,调用下一个要执行的进程B的进程上下文,然后开始执行B,保存进程B的上下文……
- 进程就是上下文切换之间的程序执行的部分。是运行中的程序的描述,也是对应于该段CPU执行时间的描述。
浏览器是多进程
一、浏览器进程
负责浏览器的一些操作:
- 各个tab页签的创建、销毁
- 浏览器界面的显示
- 用户交互:前进、后退等
- 网络资源管理,如下载
二、GPU进程
- 负责3D绘制和硬件加速
三、第三方插件进程
- 负责第三方插件的使用
- 使用一个插件时就会创建一个插件进程
- 一个插件都不用就不会创建该进程
四、浏览器渲染进程(重点)
- 每个tab页签都有一个该进程
- tab页签之间互不影响
- 是多线程的
- 浏览器内核就是浏览器渲染进程
- 浏览器内核包括两部分:渲染引擎和JS引擎
- 渲染引擎负责对网页语法的解释(如HTML、XML等)并渲染网页(CSS),常见的有:
Trident内核引擎:IE、360浏览器的兼容模式等等Gecko内核引擎:FirefoxWebkit内核引擎:Safari [səˈfɑːri]、Opera [ˈɑːprə](2013之前)、Chrome [kroʊm](2013之前)Blink内核引擎:Chrome 基于 WebKit 做的修订和精简的渲染引擎,目前大部分国内浏览器最新版本的内核也都改为了Blinkchromium[ˈkroʊmiəm] 内核?:chromium是google建立在WebKit之上的浏览器开源项目,由于和苹果有分歧,改用了自己的内核Blink,相当于Chrome的工程版或称实验版,国产的所有 “双核浏览器”,都是基于Chromium开发的。Chrome也是基于它的稳定(Stable)版来发布。
GUI渲染线程
- 负责渲染浏览器界面,解析
HTML,CSS,构建DOM树和RenderObject树,布局和绘制等 - 当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行,反之GUI执行时JS引擎会被挂起
JS引擎线程
- 也称为
JS内核(例如V8、Chakra引擎),负责处理JavaScript脚本程序,处理JavaScript脚本的虚拟机。 JS引擎线程负责解析JavaScript脚本,运行代码。JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS线程在运行。- 因为
GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程
- 用来控制事件循环(可以理解成
JS引擎自己都忙不过来,需要浏览器另开线程协助)。 - 当
JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其它线程,如鼠标点击,AJAX异步请求等),会将对应任务添加到事件线程中。 - 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待
JS引擎的处理。 - 注意,由于
JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)。
定时器触发线程
- 传说中的
setTimeout和setInterval所在的线程 - 浏览器定时计数器并不是由
JavaScript引擎计数的,(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确) - 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待
JS引擎空闲后执行) - 注意,
W3C在HTML标准中规定,要求setTimeout中低于4ms的时间间隔算为4ms。
异步http请求线程
- 在
XMLHttpRequest在连接后是通过浏览器新型一个线程请求 - 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由
JavaScript引擎执行
详解渲染过程
各个引擎之间的实现大同小异,以下内容是基于
Webkit/Blink渲染引擎来讲
总体流程:
- [GUI线程] HTML解析器解析HTML文件,构建DOM树,同时浏览器主进程负责下载CSS文件
- [GUI线程] 渲染树结构:CSS文件下载完成,解析CSS文件成树形的数据结构,然后结合DOM树合并成RenderObject树(具有一定的视觉效果,并按照一定顺序排列在屏幕上)
- [GUI线程] 布局渲染树:负责RenderObject树中的元素的尺寸,位置等计算,为每个节点分配固定坐标(Layout/Reflow)
- [GUI线程] 绘制渲染树:渲染引擎会遍历所有的节点,绘制页面的像素信息
- [浏览器主进程] 将默认的图层和复合图层交给GPU进程,GPU进程再将各个图层合成(`composite [kəmˈpɑːzət]),最后显示出页面

为了提高用户体验,渲染引擎会试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
详细过程:
1、浏览器中输入url
回车后浏览器主进程接管,开一个下载线程
2、进行http请求(略去DNS查询,IP寻址、握手等等操作)
等待响应,获取内容。
中间过程所用时间可以查看w3c - Navigation Timing
3、获取内容转为字符串
网络传输内容其实都是0和1这些字节数据。浏览器接收后会将他们转换为字符串(代码)
4、HTML解析:标记化
引擎中的解析器:将文档转化为引擎可以使用的结构,解析的结果代表了文档结构的节点树,也称为解析树。
解析器分两个结构:词法分析和语法分析
词法分析负责将输入内容分解为有效标记符号
语法分析对语言应用语法规则,从而构建解析树。
渲染引擎获取到文档内容后,会将这些字符串通过词法分析转换为标记(token),这一过程称为标记化(tokenization)
html的符号包括开始标签、结束标签、属性名及属性值。解析器识别出符号后,将其传递给树构建器,并读取下一个字符,以识别下一个符号,这样直到处理完所有输入。
这一过程会将代码分成一个个小块,并给这些内容打上标记,比如:
<div>哈哈</div><div>:标记为开始的div标签- 哈哈:标记为标签内的文本
</div>:标记为结束的div标签
5、构建DOM树
结束标记化后,会被转换为Node节点,根据这些Node之间的联系又被构建成一颗DOM树。
6、构建CSSOM树
转换CSS到CSSOM树和上一步相似。将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。

7、生成渲染树(附加)
当生成完DOM树和CSSOM树后,会将两棵树组合成一颗完整的渲染树。
组合的过程也很复杂,渲染树只会包含需要渲染到浏览器界面上的节点,比如display:none就不会在渲染树中显示。
非可视化的 DOM 元素不会插入渲染树中,例如<head>元素。

Gecko将视觉化后的树称为“框架树”,webkit称为“渲染树”
将DOM树和样式信息构建渲染树的过程,webkit称为“附加”,gecko称为“框架结构”
Render树的每一个节点我们叫它渲染器renderer,有些DOM元素没有对应的renderer,而有些DOM元素却对应了好几个renderer。譬如下拉列表select元素,我们就需要三个renderer:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。

每个渲染器都有一个"paint()“、“layout()”或“reflow()”方法
8、布局(layout)
当渲染对象被创建并添加到渲染树中,它们并没有位置和大小,计算这些值的过程称为layout。
webkit称为“布局layout”,gecko称为“重排reflow”
由根渲染对象开始递归,为每个需要几何信息的渲染对象进行计算。
浏览器进行页面布局基本过程是以浏览器可见区域(viewport)为画布,左上角为 (0,0) 基础坐标,从左到右,从上到下从DOM的根节点开始画,首先确定显示元素的大小跟位置。
布局阶段输出的结果称为box盒模型(width,height,margin,padding,border,left,top,…),盒模型精确表示了每一个元素的位置和大小,并且所有相对度量单位此时都转化为了绝对单位。

所有的渲染对象都有一个layout或reflow方法,每个渲染对象调用需要布局的字节点(children)的`layout``方法。
9、绘制(paint)
渲染引擎会遍历Render树,并调用renderer的 paint() 方法,GPU进程会将最终内容显示在屏幕上。
10、回流(reflow)与重绘(repaint)
回流(reflow)
当浏览器发现某个部分发生了点变化影响了布局,需要回去重新渲染。reflow 会从 <html>开始递归往下,依次计算所有的结点几何尺寸和位置。
reflow 几乎是无法避免的。比如树状目录的折叠、展开,实质上是元素的显示与隐藏等,都将引起浏览器的
reflow。鼠标滑过、点击……只要这些行为引起了页面上某些元素的占位面积、定位方式、边距等属性的变化,都会引起它内部、周围甚至整个页面的重新渲染。通常我们都无法预估浏览器到底会 reflow 哪一部分的代码,它们都彼此相互影响着。
重绘(repaint)
改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,屏幕的一部分要重画,但是元素的几何尺寸没有变。
所以,回流必定会发生重绘,重绘不一定引发回流。
回流所需的成本比重绘高很多,改变父节点里某个字节点很可能会导致父节点一系列的回流。
reflow与repaint常见的时机:
display:none会触发 reflow,而visibility:hidden只会触发 repaint,因为没有发生位置变化。- 有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。
- resize 窗口,改变了页面默认的字体、添加/删除样式、文字改变、定位或浮动等等。对于这些操作,浏览器会马上进行 reflow。
其实,重绘和回流和event loop也有关,详见下面的章节。
事件循环(event loop)
执行栈
可以理解为一个存储函数调用的栈结构,遵循先进后出的原则。比如
function a(){
console.log('a')
}
function b(){
console.log('b')
a();
}
console.log(b())2
3
4
5
6
7
8
js引擎从上到下解析这几行代码,处理完变量提升后,开始把要执行的函数放入执行栈,顺序如下:
- console.log(b())
- b()
- console.log('b')
- a()
- console.log('a')
然后先进后出,从5开始:
- console.log('a'),控制台输出 a,移出队列
- a() 执行完毕,移出队列
- console.log('b'),控制台输出 b,移出队列
- b() 执行完毕,移出队列
- console.log(b()),控制台输出 undefined(b函数没有return),移出队列
JS执行过程
JS分为同步任务和异步任务- 同步任务都在主线程上执行,形成上面说的执行栈
- 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件
- 一旦执行栈中的所有同步任务执行完毕(此时
JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈,开始执行。
这个任务队列遵循的是:先进先出原则。
定时器
调用setTimeout后,是由定时器线程控制等到特定时间后添加到事件队列的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响计时准确,因此很有必要另开一个线程用来计时。
当使用setTimout或setInterval时,需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。
setTimeout(()=>{
console.log('hello')
},0)
console.log('begin')2
3
4
- 执行结果是:先
begin,后hello - 虽然代码的本意是
0毫秒就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms - 就算不等待
4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只能可执行栈内空了后才会主动读取事件队列)
宏任务/微任务
不同的任务源会被分配到不同的任务队列,任务源可以分为 宏任务和微任务,这个叫法来源于HTML规范。
es6规范中,宏任务称为task,微任务称为jobs
宏任务 macrotask [ˈmækroʊ tæsk]
- script
- setTimeout
- setInterval
- setImmediate(该方法可能不会被批准成为标准,目前只有最新版本的 Internet Explorer 和Node.js 0.10+实现了该方法。)
- I/O
- UI rendering
微任务 microtask [ˈmaɪkroʊ tæsk]
- process.nextTick(Nodejs)
- promise的then
- MutationObserver(监视对DOM树所做更改,vue中的nextTick的实现)
虽然微任务的优先级比宏任务高,但是因为script属于宏任务,所以首次加载js的时候是先执行宏任务,后续遇到异步任务才会遵守“先微任务再宏任务”这个规则。
注意,有一些浏览器执行结果不一样,因为它们可能把microtask当成macrotask来执行了,所以要知道有些浏览器可能并不标准。
console.log('1');
setTimeout(()=>{
console.log('2')
},0);
Promise.resolve()
.then(()=>console.log('3'))
.then(()=>console.log('4'))
console.log('5')
//执行结果:
'1'
'5'
'3'
'4'
'2'2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
总结
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务中的微任务都执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,
JS线程继续接管,开始下一个宏任务(从事件队列中获取)
在第4步中,具体步骤为:
- 判断document是否需要更新,需要的话就更新,频率为
16ms一次。(浏览器界面展示的刷新率是60Hz,也就是每秒钟会绘制60帧。1/60s=16ms) - 判断是否有
resize、scroll等事件,有就触发,并且自带节流 - 判断是否触发了
media query - 更新动画并且发送相应事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame回调 - 更新界面
以上步骤,都是在1帧中处理的事情,如果这1帧是空闲的,就会执行requestIdleCallback回调。
详情可以查看:event loop processing model
优化方向
Critical Rendering Path
浏览器将HTML,CSS和JavaScript转换为屏幕上的像素所经历的步骤的过程,也就是前面提到的渲染过程,称为:关键渲染路径(Critical Rendering Path),简称CRP。
下面说到的优化点,其实就是在优化关键渲染路径。
优化关键渲染路径可以缩短首次渲染的时间。了解和优化关键的渲染路径对于确保重排和重绘可以每秒60帧的速度进行,以确保高效的用户交互是很重要的。
一、开启硬件加速(GPU加速)
渲染步骤就提到了composite概念。浏览器渲染的图层一般包含两大类:普通图层以及复合图层。
普通图层
也称为默认图层:指的是普通文档流的元素
absolute布局(fixed也一样),虽然可以脱离文档流,但它仍然属于默认图层
复合图层
复合图层一般指的使用动画执行或者<video><iframe><canvas><webgl>等元素,也可以使用z-index将层级高的元素变成复合图层,
各个复合图层是GPU单独分配资源单独绘制的,脱离了普通文档流,不会影响其他图层的重绘和回流,所以:将一个普通图层转为复合图层就可以开启GPU加速。
常用转换到复合图层方式(开启硬件加速的方式):
元素加上
transform的translate3d属性(或translatez):css.vinsea{ transform: translate3d(0, 0, 0); /* transform: translatez(0); */ }1
2
3
4opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)<video><iframe><canvas><webgl>等元素元素加上
will-chang属性(这是一个实验中的功能):CSS 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。
csswill-change: transform; will-change: opacity;1
2但是要注意:
- 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与
will-change结合在一起的话,有可能会消耗很多机器资源,如果过度使用的话,可能导致页面响应缓慢或者消耗非常多的资源。 - **不要过早应用 will-change 优化:**如果你的页面在性能方面没什么问题,则不要添加
will-change属性来榨取一丁点的速度。will-change的设计初衷是作为最后的优化手段,用来尝试解决现有的性能问题。 - **给它足够的工作时间:**这个属性是用来让页面开发者告知浏览器哪些属性可能会变化的。使用时需要尝试去找到一些方法提前一定时间获知元素可能发生的变化,然后为它加上
will-change 属性。元素变化结束后,再将该属性设置为auto来关闭。
- 不要将 will-change 应用到太多元素上:浏览器已经尽力尝试去优化一切可以优化的东西了。有一些更强力的优化,如果与
注意
- 不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡。
- 硬件加速时请使用
index,防止浏览器默认给后续的元素创建复合层渲染。webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,那么层级比这个元素高的,或者相同的,并且relective或absolute属性相同的,会默认变为复合层渲染,如果处理不当会极大的影响性能。
其他
一、WebWorker
创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM) JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。
二、css加载是否会阻塞dom树渲染
- 头部引入
css的情况 css文件是由单独的下载线程异步下载的css加载不会阻塞DOM树解析,异步加载时dom照常构建- 会阻塞
render树渲染,渲染时需要等css加载完毕,因为render tree需要cssom
三、关于“布局”的补充
Dirty bit系统
为了不因为每个小变化都全部重新布局,浏览器使用一个dirty bit系统,一个渲染对象发生了变化或是被添加了,就标记它及它的children为dirty,表示需要layout。
存在两个标识:dirty及children are dirty,children are dirty说明即使这个渲染对象可能没问题,但它至少有一个child需要layout。
全局和增量布局
当layout在整棵渲染树触发时,称为全局layout,这可能在下面这些情况下发生:
- 一个全局的样式改变影响所有的渲染对象,比如字号的改变。
- 窗口resize。
layout也可以是增量的,这样只有标志为dirty的渲染对象会重新布局(也将导致一些额外的布局)。增量layout会在渲染对象dirty时异步触发,例如,当网络接收到新的内容并添加到Dom树后,新的渲染对象会添加到渲染树中。
异步和同步布局
增量layout的过程是异步的,Firefox为增量layout生成了reflow队列,以及一个调度执行这些批处理命令。WebKit也有一个计时器用来执行增量layout-遍历树,为dirty状态的渲染对象重新布局。
当脚本请求样式信息时,例如“offs etHeight”,会同步的触发增量布局。
全局的layout一般都是同步触发。
宝藏工具
- csstriggers - 查找某个css属性会触发什么渲染器的哪个事件
- loupe - 可视化
event loop工具
规范链接:
浏览器缓存机制
缓存位置
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
- 网络请求
当依次查找缓存且都没命中时,才会去请求网络
Memory Cache
内存中的缓存。读取内存虽然比读取磁盘高效,但是持续性短,会随着进程的释放而释放。也就是说页面关闭了,内存中的缓存就被释放了。
- 对于大文件来说,基本不会被存储到内存中
- 当前系统内存使用率高的话,文件优先存储到硬盘
Disk Cache
存储在硬盘中的缓存,读取比内存读取慢但是存储的时间久。
会根据HTTP Header中的字段判断哪些资源需要缓存,哪些需要设置过期时间。具体缓存策略可以看下一节。
即使跨站点,相同地址的资源一旦已经缓存到硬盘,就不会去网络重新请求。
Push Cache
只在会话(session)中存在,一旦会话结束就被释放。
现在不够普及,但是是未来的一个趋势。
网络请求
所有缓存都没命中就发起请求获取
缓存策略
强缓存
表示在缓存期间不需要发请求。
HTTP Header设置
expires[ɪkˈspaɪərz]Expires:1HTTP Header设置
Cache-control,优先级高于expires。Cache-control: max-age=601表示该资源60秒后过期。
更多可选值查看Cache-control MDN
协商缓存
如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
Last-Modified 和 If-Modified-Since
Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
但是 Last-Modified 存在一些弊端:
如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源
因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag 。
ETag 和 If-None-Match
ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。
如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
浏览器存储
cookie,localStorage,sessionStorage,indexDB
| 特性 | cookie | localStorage | sessionStorage | indexDB |
|---|---|---|---|---|
| 数据生命周期 | 一般由服务器生成,可以设置过期时间 | 除非被清理,否则一直存在 | 页面关闭就清理 | 除非被清理,否则一直存在 |
| 数据存储大小 | 4K | 5M | 5M | 无限 |
| 与服务端通信 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
- 不建议cookie 用于存储。
- 如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage
- 对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储。
需要注意cookie安全性。
| 属性 | 作用 |
|---|---|
| value | 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 |
| http-only | 不能通过 JS 访问 Cookie,减少 XSS 攻击 |
| secure | 只能在协议为 HTTPS 的请求中携带 |
| same-site | 规定浏览器不能在跨域请求中携带 Cookie,减少 CSRF 攻击 |
Service Worker
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用采取来适当的动作、更新来自服务器的的资源。
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。
Service Worker 实现缓存功能一般分为三个步骤:
- 注册 Service Worker
- 监听到 install 事件以后就可以缓存需要的文件
- 在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存
- 存在缓存的话就可以直接读取缓存文件,否则就去请求数据
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('sw.js')
.then(function(registration) {
console.log('service worker 注册成功')
})
.catch(function(err) {
console.log('servcie worker 注册失败')
})
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener('install', e => {
e.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll(['./index.html', './index.js'])
})
)
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 拦截所有请求事件
// 如果缓存中已经有请求的数据就直接用缓存,否则去请求数据
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response
}
console.log('fetch source')
})
)
})2
3
4
5
6
7
8
9
10
11
12
打开页面,可以在开发者工具中的 Application 看到 Service Worker 已经启动了
在 Cache 中也可以发现我们所需的文件已被缓存
当我们重新刷新页面可以发现我们缓存的数据是从 Service Worker 中读取的