Jacleklm's Blog

浏览器

2019/10/29

浏览器渲染

渲染流程

  • 接收到 HTML 文件,转化为 DOM 树(字节数据=>字符串=>Token(最小单位代码)=>Node=>DOM)
    当然,在解析 HTML 文件的时候,浏览器还会遇到 CSS 和 JS 文件,这时候浏览器也会去下载并解析这些文件(即遇到 script 标签会暂停 DOM 的构建)
  • 将 CSS 文件转换为 CSSOM 树(字节数据=>字符串=>Token(最小单位代码)=>Node=>CSSOM)
    在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源
    eg. span 和 div > a > span,后者的解析成本会更高
    我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平
  • 将 DOM 树和 CSSOM 树组成成渲染树(Render Tree)
    渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是display: none的,那么就不会在渲染树中显示
  • 根据渲染树来进行布局(也可以叫做回流),然后调用 GPU 绘制,合成图层,显示在屏幕上

什么情况下会阻塞渲染

  • 首先渲染的前提是生成渲染树,所以 HTML 和 CSS 肯定会阻塞渲染
    想渲染的越快,越应该降低一开始需要渲染的文件大小,并且做到HTML 扁平层级,优化 CSS 选择器
  • 然后当浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始
    所以,如果想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因

重绘和回流

见 CSS 知识点

浏览器缓存机制

  • 缓存可以说是性能优化中简单高效的一种优化方式了,它可以显著减少网络传输所带来的损耗
  • 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

缓存位置

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。顺序是:

  1. Service Worker
  2. Memory Cache
  3. Disk Cache
  4. Push Cache
  5. 都找不到,就发起网络请求获取数据

Service Worker

  • service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
  • 当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

Memory Cache

  • Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
  • 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存

那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?

  • 先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JS、HTML、CSS、图片等等
  • 当然,我通过一些实践和猜测也得出了一些结论:
    对于大文件来说,大概率是不存储在内存中的,反之优先当前系统内存使用率高的话,文件优先存储进硬盘

Disk Cache

  • Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
  • 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据

Push Cache

Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放

网络请求

  • 如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
  • 那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来讨论缓存策略这部分的内容

缓存策略

通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的

强缓存

强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control 。强缓存表示在缓存期间不需要请求,state code 为 200
Expires

1
Expires: Wed, 22 Oct 2018 08:41:00 GMT

Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。

Cache-control

1
Cache-control: max-age=30

Cache-Control 出现于 HTTP/1.1,优先级高于 Expires 。该属性值表示资源会在 30 秒后过期,需要再次请求。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令

协商缓存

  • 如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 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 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

ETag 和 If-None-Match

ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。

实际场景应用缓存策略

频繁变动的资源

对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

代码文件

这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短

一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存

浏览器存储

浏览器的存储方式

cookie,localStorage,sessionStorage,indexDB

特性 cookie localStorage sessionStorage indexDB
数据生命周期 一般由服务器生成,可以设置过期时间 除非被清理,否则一直存在 页面关闭就清理 除非被清理,否则一直存在
数据存储大小 4K 5M 5M 无限
与服务端通信 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

结论:如果没有大量数据存储需求的话,可以使用 localStorage 和 sessionStorage 。对于不怎么改变的数据尽量使用 localStorage 存储,否则可以用 sessionStorage 存储

Service Worker

Service Worker 是运行在浏览器背后的独立线程(可以理解为是 Web Worker 的一种),一般用来实现缓存功能。
所以也和 Web Worker 一样不能访问 DOM,不能用 localStorage, XMLHttpRequest。
使用 Service Worker 的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全

Service Worker 实现缓存功能一般分为三个步骤:

  • 先注册 Service Worker
  • 监听到 install 事件以后就可以缓存需要的文件
  • 下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。以下是这个步骤的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//比如在index.js里注册一个Service Worker
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("xx.js")
.then(function(registration) {
console.log("注册成功");
})
.catch(function(e) {
console.log("注册失败");
});
}

//xx.js子线程文件
//监听install事件,注册后会缓存所指定的文件。
self.addEventListener("install", e => {
e.wiatUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});

//监听fetch事件进行请求拦截,如果缓存中已经有要请求的数据就直接用缓存,否则去请求数据
self.addEventListener("fetch", e => {
e.respondWith(
cache.match(e.request).then(function(response) {
//1. 如果请求的资源已被缓存,则直接返回
if (response) {
return response;
}
//2. 没有,则发起请求并缓存结果
let requestClone = e.request.clone();
return fetch(requestClone).then(netRes => {
if (!netRes || netRes.status !== 200) return netRes;
let responseClone = netRes.clone();
caches
.open(cacheName)
.then(cache => cache.put(requestClone, responseClone));
return netRes;
});
})
);
});

参考资料
掘金小册-前端面试之道-浏览器渲染原理
极客时间-浏览器工作原理与实践
浏览器缓存和 Service Worker

CATALOG
  1. 1. 浏览器渲染
    1. 1.1. 渲染流程
    2. 1.2. 什么情况下会阻塞渲染
    3. 1.3. 重绘和回流
  2. 2. 浏览器缓存机制
    1. 2.1. 缓存位置
      1. 2.1.1. Service Worker
      2. 2.1.2. Memory Cache
      3. 2.1.3. Disk Cache
      4. 2.1.4. Push Cache
      5. 2.1.5. 网络请求
    2. 2.2. 缓存策略
      1. 2.2.1. 强缓存
      2. 2.2.2. 协商缓存
    3. 2.3. 实际场景应用缓存策略
      1. 2.3.1. 频繁变动的资源
      2. 2.3.2. 代码文件
  3. 3. 浏览器存储
    1. 3.1. 浏览器的存储方式
    2. 3.2. Service Worker