这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
浏览器缓存机制
缓存是性能优化中最简单高效得到一种优化方式了。
最显著的效果就是减少了网络传输所带来的损耗
一个数据请求分为网络请求、后端处理、浏览器响应三个步骤。
浏览器缓存可以帮我们在第一、第三步骤中优化性能。
比如直接使用缓存不发起请求,或者是发起了请求,但是后端数据跟前端一样,就不需要把数据传回来,减少了响应。
可以通过下面几个部分来讨论浏览器缓存机制:
- 缓存位置
- 缓存策略
- 实际场景应用存储策略
缓存位置
缓存位置上来说四种,并且有各自的优先级,当依次查找缓存都没有命中时,才会去请求网络。
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
- 网络请求
Service Worker
这个是可以人为自由控制缓存哪些文件,如何匹配缓存、如何读取缓存,并且缓存是持续性的。
不管是在Memory Cache还是网络请求的数据,浏览器都会显示为从Service Worker获取的内容
Memory Cache
内存中的缓存,优点是读取高效,但是持续性短,会随着进程的释放而释放。
- 对于大文件来说,大概率是不存储在内存中的,反之优先
- 当前系统内存使用率高的话,文件优先存储进硬盘
Disk Cache
存储在硬盘的缓存,读取速度慢。
会根据HTTP Header的字段来判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。
在跨站点的情况下,相同地址的资源一旦缓存,就不会再去请求。
Push Cache
这是HTTP/2的内容,只有上面三种缓存都没有命中时才会使用,缓存时间很短暂,只在会话中存在,一旦会话结束就被释放。
- 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好
- 可以推送
no-cache
和no-store
的资源 - 一旦连接被关闭,Push Cache 就被释放
- 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
- Push Cache 中的缓存只能被使用一次
- 浏览器可以拒绝接受已经存在的资源推送
- 你可以给其他域名推送资源
缓存策略
分两种:强缓存和协商缓存,通过配置HTTP Header来实现的。
强缓存
可以配置两种Http Header实现:Expires
和Cache-Control
,强缓存表示缓存期间不需要请求,state code为200。
Expires
Expires: Wed, 22 Oct 2018 08:41:00 GMT
这个是HTTP/1的产物,表示资源会在上面表示的时间后过期。
受限于本地时间,如果修改了本地时间,可能会导致缓存失效。
Cache-control
Cache-control: max-age=30
出现于HTTP/1.1,优先级高于Expires,上面的意思是资源会在30秒后过期。
可以使用多个指令一起使用,可以在请求头或者响应头设置。
协商缓存
如果缓存过期了,就需要发起请求验证缓存是否有更新。
可以通过Http Header实现:Last-Modified
和ETag
当浏览器发起请求验证资源时,如果资源没有改变,那么服务器会返回304的状态码,并且更新浏览器缓存有限期。
Last-Modified和if-Modified-Since
Last-Modified
表示本地文件最后修改时间。
If-Modified-Since
会将Last-Modified
的值发送给服务器,询问服务器在日期后资源是否有更新,有更新的话会将新的资源发送回来, 没有就返回304。
但是也有一些弊端:
- 如果本地打开缓存文件,即使没有修改,但是还是会判断为修改,导致服务器返回相同的资源
- 因为
Last-Modified
只能以秒计时,如果在不可感知的时间修改完成文件,那么服务器会任务资源还是命中了,不会返回正确的资源。
因为上面的弊端,所以在HTTP/1.1出现了ETag
ETag和If-None-Match
Etag类似文件指纹,If-None-Match
会将当前ETag
发送给服务器,询问该资源Etag
是否改变,有变动就将新的资源返回,并且Etag优先级比Last-Modified高。
如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date
减去 Last-Modified
值的 10% 作为缓存时间。
如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date
减去 Last-Modified
值的 10% 作为缓存时间。
实际场景应用缓存策略
频繁变动的资源
对于频繁变动的资源,首先需要使用Cache-Control: no-cache
使浏览器每次都请求服务器,然后配合Etag和Last-Modified来验证资源是否有效。
这种做法虽然不能节省请求数量,但是能显著减少响应数据大小。
代码文件
HTML一般不缓存或者缓存时间很短,所以这里指的是html文件外的代码文件。
一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000
,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。
浏览器渲染原理
跟上一个章节一样,这个也是解决性能问题的。
执行JS有一个JS引擎,执行渲染童然也有一个渲染引擎。
渲染引擎在不同的浏览器也不是不同的,在Firefox叫做Gecko,在Chrome和Safari都是基于Webkit开发的。
浏览器接受到HTML文件并转换为DOM树
打开一个页面的时候,浏览器都会去请求对应的HTML文件。
虽然平时代码会分js、css、html文件,但实际上都是字符串,计算机硬件是不理解这些字符串的,所以在网络传输的内容都是0和1的这些字节数据。
当浏览器收到这些字节数据后,就将这些字节数据转换为字符串。
当数据转换为字符串后,就将这些字符串通过词法分析转换为标记(token),这一过程在词法分析中叫做标记化(tokenization)。
标记依旧是字符串,是构成代码的最小单位,这个过程会将代码分拆成一块块,并给这些内容打上标记。
当结束表计划后,这些标记就会转换为Node,然后所有的Node会将所有的联系构建为一颗Dom树。
将CSS文件转换为CSSOM树
转换CSS到CSSOM树的过程和HTML转换为DOM基本一致。
在这个过程中,浏览器会确认每一个节点的样式是什么,这个过程是很消耗资源的。
像是:
div > a > span{}
这种代码,会在html中寻找所有span,然后找到span上级有a,a上级有div,最后给符合这种条件的元素设置样式。
所以应该尽可能的避免写过于具体的CSS选择器,尽量保证层级扁平
生成渲染树
将生成好DOM树和CSSOM树后,将要将两棵树组合为渲染树。
渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display:none
,那么就不会在渲染树显示。
生成好渲染树后,就会根据渲染树来进行布局(也可以叫做回流),然后调用GPU绘制,合成图层,显示在屏幕上。
为什么操作DOM慢
因为DOM是渲染引擎中的东西,而JS是JS引擎的。当我们通过JS操作DOm时,是涉及到两个线程之间的通信的。操作DOM次数一多,也就等同于一直在进行线程之间的通信,并且操作DOM可能还会带来重绘回流的情况。
如果插入几万个DOM,怎么实现页面不卡顿
可以使用requestAnimationFrame
分批插入Dom。
还可以使用虚拟滚动,只渲染可视区域的内容,不可见就完全不渲染了,在用户滚动的时候实时去替换渲染的内容。
什么情况阻塞渲染
渲染的前提是生成渲染树,所以HTML和CSS肯定会阻塞渲染。
如果想渲染的快,应该降低一开始需要渲染的文件的大小,并且扁平层级,优化选择器。
当浏览器解析到script
标签,就会暂停构建DOM,完成后才会从暂停的地方重新开始,所以想首屏渲染的快,就不应该一开始就加载JS文件,这也是为什么建议将script
标签放在body底部的原因。
还可以给script
标签加上defer或者async属性,加上defer表示js文件会并行下载,但是会在HTML解析完成后顺序执行。
对于没有依赖的JS的文件可以加上async,表示js文件下载和解析不会阻塞渲染。
重绘(Repaint)和回流(Reflow)
重绘是当节点需要更改外观而不会影响布局。 回流是布局或者几何属性需要改变。
回流必定会发生重绘,重绘不一定会引发回流。
以下几个动作可能会导致性能问题:
- 改变window大小
- 改变字体
- 添加或删除样式
- 文字改变
- 定位或者浮动
- 盒模型
并且很多人不知道的是,重绘和回流其实也和 Eventloop 有关。
- 当 Eventloop 执行完 Microtasks 后,会判断
document
是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。 - 然后判断是否有
resize
或者scroll
事件,有的话会去触发事件,所以resize
和scroll
事件也是至少 16ms 才会触发一次,并且自带节流功能。 - 判断是否触发了 media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame
回调 - 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调。
减少重绘和回流
使用transform替代top。
使用visibility替换display:none。
不能把节点的属性值放在循环里当做变量。
不要使用table布局。
将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响到别的节点,对于video来说,浏览器会自动将其设置为图层。
通常可以使用will-change
来生成图层,还有video、iframe标签自动为图层。
如何加速渲染
- 从文件大小考虑
- 从
script
标签使用上来考虑 - 从 CSS、HTML 的代码书写上来考虑
- 从需要下载的内容是否需要在首屏使用上来考虑