前端技术总结(二)

561 阅读10分钟

这是我参与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-cacheno-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

缓存策略

分两种:强缓存和协商缓存,通过配置HTTP Header来实现的。

强缓存

可以配置两种Http Header实现:ExpiresCache-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-ModifiedETag

当浏览器发起请求验证资源时,如果资源没有改变,那么服务器会返回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 有关。

  1. 当 Eventloop 执行完 Microtasks 后,会判断 document 是否需要更新,因为浏览器是 60Hz 的刷新率,每 16.6ms 才会更新一次。
  2. 然后判断是否有 resize 或者 scroll 事件,有的话会去触发事件,所以 resizescroll 事件也是至少 16ms 才会触发一次,并且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画并且发送事件
  5. 判断是否有全屏操作事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好
  8. 更新界面
  9. 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

减少重绘和回流

使用transform替代top。

使用visibility替换display:none。

不能把节点的属性值放在循环里当做变量。

不要使用table布局。

将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响到别的节点,对于video来说,浏览器会自动将其设置为图层。

通常可以使用will-change来生成图层,还有video、iframe标签自动为图层。

如何加速渲染

  1. 从文件大小考虑
  2. script 标签使用上来考虑
  3. 从 CSS、HTML 的代码书写上来考虑
  4. 从需要下载的内容是否需要在首屏使用上来考虑