浏览器缓存

1,058 阅读15分钟

1.load, DOMContentLoaded

window.addEventListener("load",function(){
    console.log('load')
},false)

window.addEventListener("DOMContentLoaded", function(){
    console.log('DOMContentLoaded')
},false)

load和DOMContentLoaded的作用就是当页面加载完成的时候自动执行,但是它们执行的时间点是不一样的

DOM文档加载的步骤

  1. 解析html结构
  2. 加载外部脚本和样式表文件
  3. 解析并执行脚本代码
  4. 构造HTML DOM模型    // DOMContentLoaded 执行时间点
  5. 加载图片等外部文件
  6. 页面加载完毕  // load执行时间点

浏览器解析HTML

  1. DOMTree CSSOM
  • 浏览器遍历文档节点生成DOMTree
  • 浏览器CSS parser将CSS解析成CSSOM(CSSOM也是树形结构)
  • HTML渲染和CSS渲染同步进行
  1. renderTree
  • 浏览器从DOMTree的根节点开始遍历每个可见节点,并找到其适配的CSS样式应用(中间包括样式计算,样式覆盖)
  1. layout
  • 计算出每个渲染对象的位置和尺寸并将其安置在浏览器中正确的位置
  1. painting
  • 浏览器遍历呈现树,并调用呈现器的paint方法,呈现在浏览器中

浏览器页面加载过程

2. preload 和 prefetch

使用建议:当前页面必要的资源使用preload,将来的页面中使用的资源使用prefetch

preload :

  • 是声明式的fetch,可以强制浏览器请求资源,同时不阻塞文档onload事件

  • 是对浏览器指示预先请求当前页面中需要的资源(关键的脚本,字体,主要图片,多种类型的资源),并且可以加载跨域资源

  • 加载的js脚本其加载和执行的过程是分离的,即preload会预加载相应的脚本待需要时自行调用

  • as可以取值style,script,image,font,fetch,document,audio,video等如果as属性被省略,那么该请求将会被当作异步请求处理 style 的优先级要大于 script

  • 在请求跨域资源时推荐加上crossorigin属性,否则可能会导致资源的二次加载(尤其时font资源)

    <link rel="preload" as="font" href="www.font.com" crossorigin="anonymous"> 
    // anonymous 请求头不会带上cookie以及其他的一些认证信息
    <link rel="preload" as="font" href="www.font.com" crossorigin="use-credentials">
    // use-credentials 会在请求中带上cookie和其他的一些认证信息
    

prefetch 

  • 是利用浏览器的空闲时间来加载页面将来可能用到的资源的机制
  • 通常用于加载非首页其他其他页面所需要的资源,以便加快后续页面的首屏速度
  • 当页面跳转时,未完成的prefetch请求不会中断

共同点:

  • Chrome有四种缓存:HTTP Catch,memory Catch,Service Worker Catch,Push Catch在preload或prefetch的资源加载时,两者都存储在HTTP Catch中,当资源加载完成后,如果资源可以被缓存(比如说存在有效的cache-control和max-age)那么它被缓存在HTTP缓存中可以被现在或将来的任务使用,如果资源不可被缓存,那么它们在被使用前都存储在 memory Catch中
  • 都没有同域名的限制(可跨域)
  • 不论资源是否可以缓存,prefetch会存储在net-stack Catch中至少5分钟
  • preload需要使用as属性指定特定的资源类型以便浏览器为其分配一定的优先级,并能够正确加载资源

3.浏览器缓存: HTTP缓存,内存缓存,Service Worker 缓存,Push缓存

a. HTTP Catch 其机制是根据HTTP报文的缓存标识进行的,而HTTP报文分为两种

    HTTP请求(Request)报文HTTP响应(Response)报文

浏览器发起HTTP请求-服务器响应该请求,浏览器第一次向服务器发起请求后拿到请求结果,会根据响应报文中的HTTP头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中。

1.每次浏览器发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

2.浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

根据浏览器的缓存又分为强制缓存协商缓存

**强制缓存:**客户端第二次访问同一个网站的时候,回去查看是否有缓存,或则缓存是否失效,如果没有缓存或则已经失效,就回去服务器端发送请求,请求新数据,新数据返回后然后在把数据根据某种规则缓存起来

**协商缓存:**客户端第二次访问同一个网站的时候,会堆区缓存中的缓存数据标识,然后向服务器端发送请求,请求服务器验证缓存标识所对应的数据是否有效,如果有效的,返回304状态码,通知客户端比较成功,可以使用缓存数据。如果是失效的,就直接返回新数据和缓存规则,客户端接收数据并根据缓存规则进行缓存数据。

强缓存    expire / catch-control

// 响应头中
expires: Sun,24 May 2030 23:00:27 GMT
catche-control:max-age=31536000

**expires:**客户端再次请求服务器之前会查一下expires的时间,比较当前时间是否在expires时间之内,在就直接读取缓存,否则就去服务器重新获得

catch-control:

  •  max-age设置最大的缓存时间
  • private 默认就是private(pravite 中间层,cdn不能缓存,浏览器能换存的。设置成public 。中间层能缓存的,cdn等)
  • public  客户端和代理服务器都可缓存
  • no-catch 不会被缓存
  • no-store 所有的内容都不会缓存,强缓存和对比缓存都不会触发

catch-control 的优先级要比expire高

强制缓存也存在问题,在这个期限时间内都用缓存数据了,服务器更新数据怎么办

协商缓存 (Last-Modified 和 If-Modified-Since)/(etag  和  if-none-match)

// response headers
last-modified:Wed,09 jul 2014 06:42:30 GMT
// request headers
if-modified-since:Sun,26 May 2019 11:03:06 GMT

last-Modified 有服务器产生,指数据的最后修改时间,服务器将Last-Modified返回给客户端,下一次浏览器再次请求会携带这个最后的修改时间放到If-Modified-Since里,服务器拿到if-Modified-Since后对比数据的最后修改时间:

对比成功,代表数据未被修改过,返回状态码 304

对比失败,代表距离上一次请求时,数据发生了修改,就要重新响应数据返回200

存在的问题:

1.资源被修改,内容没变的话但是last-modified却改变了,导致文件没办法使用缓存

2.last-modified是精确到1s的,本质上,1s内可以修改文件两次,同样也不能保证文件是最新的

3.有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等

Etag/if-None-Match 优先级高于 (last-modified/ if-modified-since)

//response headers
etag: "xxxxxx"
// request headers
if-none-match:xxxxx

etag:解决可能在同一秒内保存两次,也可以解决服务器修改时间不一致

服务器相应请求时,告诉浏览器当前资源在服务去的唯一标识

再次请求服务器时,通过此字段是服务器和客户端缓存数据的唯一标识

服务器收到请求后发现有if-none-match则与被请求资源的唯一标识进行比对,

不同,说明资源被改动过,则相应整个内容,返回200状态码

相同,说明资源没有新的变动,则返回304状态码,告诉浏览器继续使用保存的catch

b. Memory Catch(内存缓存)

内存缓存主要包含的是当前页面中已经获取到资源,例如已经下载的样式,脚本,图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存的持续性很短,会随着进程的释放而是释放。一旦我们关闭页面,内存中的缓存也就释放了。

既然内存缓存这么高效,我们是不是能让数据都放在内存中呢?当然是否定的。计算机的内存一定比硬盘的容量要小而且要小得多,操作系统需要精打细算内存的使用,所以让我们使用的内存必然不多。

当我们访问过页面后,再次刷新页面,发现很多数据来自于内存缓存

内存缓存中有一块重要的缓存资源是preloader相关指令()下载的资源。众所周知,preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件一边网络请求下一个资源。

需要注意的是,内存缓存在缓存资源时并不会关心返回资源的HTTP头的cache-control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

c. Disk Cache

硬盘缓存读取速度慢,但是什么都可以存储到磁盘中,比之Memory Cache 胜在容量和时效上

所有的浏览器的缓存中,Disk Cache覆盖面基本时最大的。他会根据HTTP Header中的字段判断哪些资源需要缓存,那些资源可以不请求直接使用,那些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自Disk Cache

大文件优先存在disk    当前内存使用率高的话也会存在disk中

d.Push Cache

推送缓存时http/2中的内容,当以上三种缓存都没有命中时,她才会使用。它只在会话(Session)中存在,一旦会话结束就会被释放,并且缓存时间短

e. service Worker Cache

缓存机制:

强制缓存优先于协商缓存,若强制缓存(Expires和Cache-Control)生效直接使用缓存,若不生效则进行协商缓存(Last-Modified/ if-modified-since 和 etag/if-none-match),协商缓存有服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中,生效则返回304,继续使用缓存。

4.hash模式和history模式

hash模式:

hash指的是url尾巴后的#号以及后面的字符。这里的#和css里的#是一个意思。hash也称作锚点,本身是用来做页面定位的,它可以使对应id的元素显示在可视区域内。由于hasn值变化不会导致浏览器向服务器发出请求,而且hash改变会触发hashchange事件,浏览器的前进后退也能对其进行控制,所以在html5的history出现前,基本使用的都是hash来实现前端路由的

window.location.hash = "jj" //设置url的hash,会在当前url后面加上‘#jj’
var hash = window.location.hash // ‘#jj’
window.addEventListener('hashchange',function (){
    //监听hash变化,点击浏览器的前进后退会触发
})

history模式:

HTML5规范提供了 history.pushState和history.replaceState来进行路由控制。通过这两个方法可以改变url而且不用向服务器发送请求。同时不会像hash有一个#,更加的美观,但是history路由需要服务器的支持,并且将所有的路由重定向到根页面

hash兼容到IE8history兼容到IE10

hash本来是用来页面定位的,如果拿来用作路由,原来锚点的功能就不能使用了,其次,hash的传参是基于url的,如果是要传递复杂的数据,会有体积的限制,而history模式不仅可以在url里放参数,还可以将数据存放在一个特定的对象中

vue-router模式hash模式,使用URL的hash来模拟一个完整的URL,于是当URL改变时,页面不会重新加载

window.history.pushState(state,title,url)
// state: 需要保存的数据,这个数据在触发popState事件时,可以在event.state里获取
// title: 标题,基本不用,一般传null
// url:设定新的历史记录的url,新的url的origin必须时一致的,否则抛出错误。url可以时绝对路径也可以时相对路径
// 当前是 https://www.baidu.com/a
// 执行history.pushState(null,null,'./qq/')
// 就会变成https://www.baidu.com/a/qq/
// 执行history.pushState(null,null,'/qq/')  https://www.baidu.com/qq/


window.history.replaceState(state,title,url)
// 与pushState基本相同,但它是修改当前的历史记录,而pushState是创建新的历史记录
// 这种方式没有保存历史记录,页面不可返回

window.addEventListener('popstate',function(){
     // 监听浏览器前进后退事件,pushState与replaceState都不会触发
})


window.history.back() //后退
window.history.forward() //前进
window.history.go(-1) // 前进异步,-2为后退两步
window.history.length // 可以查看当前历史堆栈中页面的数量

带来的问题: 单页面应用,返回的所有路径都是index.html,不会返回404错误页面,

在vue应用里面覆盖所有的路由情况,然后各一个404页面

const router = new VueRouter({  mode: 'history',  routes: [    { path: '*', component: NotFoundComponent }  ]})

5.回流和重绘

浏览器的渲染过程:

1.解析HTML,生成DOM树,解析CSS,生成CSSOM树

2.将DOM树和CSSOM树结合,生成渲染树(render tree)

3.Layout(回流):根据生成的渲染树,进行回流,得到节点的几何信息(位置,大小)

4.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素

5.Display:将像素发送给GPU,展示在页面上。(这一步中还有很多,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层)

渲染树只包含可见的节点

不可见的节点:

1.不会渲染输出的节点:script,meta,link。。。

2.一些通过css隐藏的节点,比如display:none,用visibility和opacity隐藏的节点还是会显示在渲染树中

回流主要是计算节点的位置和几何信息,只要页面布局和几何信息发生变化的时候,就需要回流:

  • 添加/删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距,内边框,边框大小,高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染时
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置的大小的)

回流一定会触发重绘,而重绘不一定会回流

浏览器的优化机制:

每次重排都会造成额外的计算消耗,因此,大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入队列里,直到过了一段时间或操作达到了一个阈值,才清空队列。当获取布局信息的操作时候,会强制队列刷新

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

最好避免使用上面的列出的属性,它们都会刷新渲染队列。如果要使用,最好将值缓存

批量修改Dom

  1. 使元素脱离文档流
  2. 使其进行多次修改
  3. 将元素带回到文档中

三种脱离文档流的方法

隐藏元素,应用属性修改内容,重新显示

使用文档片段(document fragment)在当前DOM之外建一个子树,在拷贝回文档流

将原始元素拷贝到一个脱离文档的节点中,修改节点后,在替换原始的元素

function appendDataToElement(appendToElement, data) {
    let li;
    for (let i = 0; i < data.length; i++) {
        li = document.createElement('li');
        li.textContent = 'text';
        appendToElement.appendChild(li);
    }
}
const ul = document.getElementById('list');
ul.style.display = 'none';
appendDataToElement(ul, data);
ul.style.display = 'block';

fragment

const ul = document.getElementById('list');
const fragment = document.createDocumentFragment();
appendDataToElement(fragment, data);
ul.appendChild(fragment);

脱离文档的节点中

const ul = document.getElementById('list');
const clone = ul.cloneNode(true);
appendDataToElement(clone, data);
ul.parentNode.replaceChild(clone, ul);

避免触发同步布局事件

多次调用offsetWidth为元素赋值。。

6.script标签的defer与async

defer:延迟脚本

  • 立即下载。不影响其他操作,如下载其他资源,HTML解析
  • 延迟执行。将延迟到整个页面都解析完毕后在运行,也就是到浏览器遇到标签后在执行
  • 理论按顺序执行,实际不是。HTML5规范要求脚本按照他们出现的先后顺序执行,并且会在DOMContentLoaded事件触发前执行。但是,在现实中,延迟脚本并一定会按照顺序执行,也不一定回在DOMContentLoaded事件触发前执行
  • 最好只包含一个延迟脚本
  • IE<=9时支持,但是存在bug

async:异步脚本

  • 立即下载。不影响其他操作,如下载其他资源,HTML解析
  • 立即执行。下载完即执行,暂停HTML解析
  • 不确定循序执行。一定会在页面的load事件前执行,但不确定在DOMContentLoaded事件触发前后执行
  • 建议异步脚本不要在加载期间修改DOM
  • IE<=9时不支持

src: 可选表示包含要执行代码的外部文件

  • 外部JS文件的.js扩展名不是必须的,因为浏览器不会检查包含JS文件的扩展名,如果不使用.js扩展名,请确保服务器能正确的返回MIME类型
  • 带有src属性的

特殊操作:

  • 同时使用defer和async浏览器会忽视defer属性,按照async执行

结论:

  • 对于完全独立的脚本才会使用async例如Google analytics

理解:

defer,遇到带有defer的script标签,立即下载js资源但是在下载的过程中不会中断htmlParse,执行的时候在整个HTMLParse完成后。

asnyc,遇到带有async的script标签,立即下载js资源但是在下载的过程中不会中断HTMLParse,下载完成后立即执行,但是在执行的过程中会中断HTMLParse

正常,遇到script标签,立即下载js资源,下载完成后,立即执行,但是在下载和执行期间会中断HTMLParse。