Web前端性能优化(三)

2024-03-19 20:05:52 浏览数 (4)

UnsplashUnsplash

浏览器存储

因为HTTP请求无状态,当这一次 HTTP 请求结束之后,这个链接就关闭了,而下一次需要发起这个请求时,服务端不会知道这个请求是和之前某一个请求,来自同一个客户端的,不能跟踪 HTTP 请求的会话和发生情况,在这种场景下,就很难去处理登录信息、用户信息的维护问题,所以需要 Cookie 去维持客户端状态,需要注意的是,Cookie 是存在过期时间 expire 的

Cookie 的生成方式有两种,一种是服务端通过 HTTP Response Header 中的 set-cookie 生成 Cookie,客户端对 Cookie 里的信息进行存储和维护,另一种是通过 JS 中的 document.cookie 方式对 Cookie 进行读写

两种不同的生成方式,对应着两种应用场景,一种是用于浏览器端和服务器端的交互,另一种是客户端自身数据的存储,Cookie 若仅仅作为浏览器存储,其存储为 4KB 左右,并且需要设置过期时间,所以我们不推荐使用 Cookie 用作浏览器存储,而是使用功能更强大的 LocalStorage 进行代替

因为 Cookie 能够被 JS 进行读写,我们客户端存储的 Cookie 信息很容易被黑客获取,所以我们一般会对 Cookie 设置 HttpOnly 参数,让其只能进行 HTTP 请求使用

在大型网站中,所有相关域名请求都会带上 Cookie,但有的请求不需要 Cookie,如 JS, CSS, 图片等静态资源,这会造成 CDN 的流量损耗,所以我们需要将 CDN 域名和主站域名独立开来

LocalStorage 是 HTML5 设计出来专门用于浏览器存储的;存储大小为 5M 左右;仅在客户端使用,不和服务端进行通信;接口封装较好;浏览器本地缓存方案

代码语言:javascript复制
if(window.localStorage) {
    localStorage.setItem('name', 'Niangao')
}
localStoragelocalStorage
代码语言:javascript复制
// 利用 LocalStorage 缓存 JS 文件

var testContent = localStorage.getItem('test')
if(testContent) {
    eval(testContent)
}else {
    var xmlhttprequest = new XMLHttpRequest();
    xmlhttprequest.onreadystatechange = callback;
    xmlhttprequest.onprogress = progressCallback;
    xmlhttprequest.open("GET", "./test.js", true);
    xmlhttprequest.send();

    function callback() {
        if(xmlhttprequest.readyState == 4 && xmlhttprequest.status == 200) {
            var responseText = xmlhttprequest.responseText;
            eval(responseText)
            localStorage.setItem('test', responseText)
        }else {
            console.log("Request was unsuccessful: "   xmlhttprequest.status)
        }
    }

    function progressCallback(e) {
        e = e || event;
        if(e.lengthComputable) {
            console.log("Received "   e.loaded   " of "   e.total)
        }
    }
}

SessionStorage 会话级别的浏览器存储;存储大小为 5M 左右;仅在客户端使用,不和服务端进行通信;接口封装较好;对于表单信息的维护

虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用,而 IndexDB 是一种低级 API,用于客户端存储大量结构化数据,该 API 使用索引来实现对该数据的高性能搜索,在网络状态不好,无法获取数据库数据时,为应用创建离线版本

代码语言:javascript复制
function openDB(name, callback) {
    // 建立打开indexdb
    var request = window.indexedDB.open(name)
    // 请求失败
    request.onerror = function(e) {
        console.log('open indexdb error')
    }
    // 请求成功
    request.onsuccess = function(e) {
        myDB.db = e.target.result
        callback && callback()
    }
    // 请求数据库版本变化
    request.onupgradeneeded = function() {
        var store = request.result.createObjectStore('books', {
            keyPath: 'isbn'
        })
        var titleIndex = store.createIndex('by_title', 'title', {
            unique: true
        })
        var authorIndex = store.createIndex('by_author', 'author')
        
        store.put({
            title: 'Quarry Memories',
            author: 'Fred',
            isbn: 123456
        })
        store.put({
            title: 'Water Buffaloes',
            author: 'Fred',
            isbn: 234567
        })
        store.put({
            title: 'Bedrock Nights',
            author: 'Barney',
            isbn: 345678
        })
    }
}
var myDB = {
    name: 'testDB',
    version: '1',
    db: null
}

function addDate(db, storeName) {
    var transaction = db.transaction('books', 'readwrite')
    var store = transaction.objectStore('books')
    // 获取当前indexdb中的数据
    // var request = store.get(234567)
    // request.onsuccess = function(e) {
    //     console.log(e.target.result)
    // }

    // 添加信息到indexdb中
    // store.add({
    //     title: 'Flowers',
    //     author: 'Quan',
    //     isbn: 222
    // })

    // 删除
    // store.delete(222)

    // 获取&更新
    store.get(222).onsuccess = function(e) {
        books = e.target.result
        console.log(books)
        books.author = 'James'
        var request = store.put(books)
        request.onsuccess = function(e) {
            console.log('update success')
        }
    }
}

openDB(myDB.name, function() {
    // 关闭indexdb
    // myDB.db.close()
    // 删除indexdb
    // window.indexedDB.deleteDatabase(myDB.db)
})

setTimeout(function() {
    addDate(myDB.db)
}, 2000)
运行结果运行结果

PWA

PWA (Progressive Web Apps) 是一种 Web App 新模型,并不是具体指某一种前沿的技术或者某一个单一的知识点,我们从英文缩写来看就能看出来,这是一个渐进式的 Web App,是通过一系列新的 Web 特性,配合优秀的 UI 交互设计,逐步的增强 Web App 的用户体验

PWA 的主要特点包括下面三点:可靠 - 即使在不稳定的网络环境下,也能瞬间加载并展现;体验 - 快速响应,并且有平滑的动画响应用户的操作;粘性 - 像设备上的原生应用,具有沉浸式的用户体验,用户可以添加到桌面

通过性能检测工具 Lighthouse,可以检测网站是否符合 PWA,还能查看网站的可靠性、速度等性能优化指标,安装该插件需翻墙

手淘性能检测手淘性能检测

Service Worker 是一个脚本,浏览器独立于当前网页,将其在后台运行,为实现一些不依赖页面或者用户交互的特性打开了一扇大门。在未来这些特性将包括推送消息,背景后台同步,geofencing(地理围栏定位),但它将推出的第一个首要特性,就是拦截和处理网络请求的能力,包括以编程方式来管理被缓存的响应,查看当前浏览器上 运行的 Service Worker ,查看 已注册的Service Worker,仅在 Chrome 下有效

Service Worker 作为 PWA 的一个关键角色,其运用方式有以下两个:使用拦截和处理网络请求的能力,去实现一个离线应用;使用 Service Worker 在后台运行同时能和页面通信的能力,去实现大规模后台数据的处理

代码语言:javascript复制
// service-worker.js

self.addEventListener('install', function(e) {
    e.waitUntil(
        caches.open('app-v1')
            .then(function(cache) {
                console.log('open cache')
                return cache.addAll([
                    './app.js',
                    './main.css',
                    './serviceWorker.html'
                ])
            })
    )
})

self.addEventListener('fetch', function(event) {
    event.respondCith(
        caches.match(event.request).then(function(res) {
            if(res) {
                return res
            }else {
                // 通过fetch方法向网络发起请求
                fetch(url).then(function(res) {
                    if(res) {
                        // 对于新请求到资源存储到我们的cachestorage中
                        caches
                    }else {
                        // 用户提示
                    }
                })
            }
        })
    )
})
代码语言:javascript复制
// app.js

if(navigator.serviceWorker) {
    navigator.serviceWorker.register('./service-worker.js', {scope: './'})
            .then(function(reg) {
                console.log(reg)
            })
            .catch(function(e) {
                console.log(e)
            })
}else {
    alert('Service Worker is not supported')
}

Service Worker 是在后台启动的一条服务 Worker 线程,该线程的工作是把一些资源缓存起来,然后拦截页面的请求,先看下缓存库里有没有,如果有的话就从缓存里取,响应 200,反之没有的话就走正常的请求

Service Worker 可以帮助我们做一些大规模的逻辑运算,运算完成之后,通过一些和主页面通信的机制,把相关的运算结果传递给主页面,主页面监听相关的事件,就能获得运算结果,从而进行页面后续的逻辑执行,不阻碍我们主线程的执行

Service Worker 的生命周期Service Worker 的生命周期

缓存

浏览器缓存就是把一个已经请求过的 Web 资源拷贝一份副本储存在浏览器中,当下一个请求来到的时候,如果是相同的URL,浏览器会根据缓存机制决定直接使用副本响应访问请求还是再次向服务器发送请求

手淘读取缓存手淘读取缓存

通过指令 max-age=<seconds> 来设置资源能够被缓存的最大时间, 例如,max-age=60 表示在请求发起的接下来 60 秒可被缓存和重用响应,尽管我们在客户端可以设置足够长的缓存过期时间,但在代理服务器中我们会使用不同的缓存策略,指令 s-maxage=<seconds> 就是用于设置共享缓存,仅应用于 public 缓存设备上(如 CDN),而不应用于针对单用户的本地缓存,s-maxage 指令优先级高于 max-age 指令

缓存时间缓存时间

Cache-Control 其他取值为:

  • public 所有内容都将被缓存(客户端和代理服务器都可缓存)
  • private 内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
  • no-cache 必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌(ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载
  • no-store 所有内容都不会被缓存或 Internet 临时文件中

这里需要注意的是,no-cache 的作用是指跳过文档过期的验证而直接进行服务器再验证,而 no-store 是指资源禁止被缓存

在浏览器缓存中,根据 Expires 和 Cache-Control 的值来验证文档(资源副本)是否过期的过程,称为 HTTP 的文档过期验证机制,若是文档没有过期,则浏览器会直接使用缓存中的文档作为返回结果,若是文档已经过期了,则需要进行服务器再验证

Expires 的一个缺点就是返回的到期时间是服务器端的时间,这样存在一个问题,如果客户端的时间与服务器的时间相差很大(比如时钟不同步,或者跨时区),那么误差就很大,所以在 HTTP 1.1 中,使用 Cache-Control: max-age=<seconds> 替代 Expires,当 Expires 与 Cache-Control 同时存在时,Cache-Control 的优先级要高于 Expires

Expires 与 Cache-ControlExpires 与 Cache-Control
Cache-Control 过程Cache-Control 过程

在 HTTP 中 Last-Modified 与 If-Modified-Since 都是用于记录页面最后修改时间的 HTTP 头信息,需要注意的是,Last-Modified 是由服务器往客户端发送的 HTTP 头,If-Modified-Since 是由客户端往服务器发送的头

所以再次请求本地存在的 cache 页面时,客户端会通过 If-Modified-Since 头将先前服务器端发过来的 Last-Modified 最后修改时间戳发送回去,这是为了让服务器端进行验证,通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则说明资源被修改过,则响应 HTTP 200,并且返回最新的资源;如果是最新的,则相应 HTTP 304 ,浏览器会继续使用原先保存的该资源的副本,这样在网络上传输的数据就会大大减少,同时也减轻了服务器的负担

Last-ModifiedLast-Modified
If-Modified-SinceIf-Modified-Since

Etag 是服务器端在响应请求时用来说明资源在服务器端的唯一标识,与之对应的是 If-None-Match 字段,在服务器再验证过程中,浏览器发送的 HTTP 请求的请求头中会带上 If-Modified-Since 字段,值为该资源 Etag 属性的值

当服务器端接收到带有 If-None-Match 属性的请求时,则会将 If-None-Match 属性的值与被请求资源的唯一标识做对比,如果相同,说明资源没有新的修改,则响应 HTTP 304,浏览器会继续使用原先保存的该资源的副本;如果不同,则说明资源被修改过,则响应 HTTP 200,并且返回最新的资源

EtagEtag
If-None-MatchIf-None-Match

当 Last-Modified / If-Modified-Since 和 Etag / If-None-Match 同时存在时,Etag / If-None-Match 的优先级要高于 Last-Modified / If-Modified-Since,HTTP 1.1 中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:

  • Last-Modified 标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内被修改多次的话,它将不能准确标注文件的修改时间
  • 如果某些文件会被定期生成,但有时内容并没有任何变化,但 Last-Modified 却改变了,导致文件没法使用缓存
  • 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
当浏览器发起 HTTP 请求时缓存机制的过程当浏览器发起 HTTP 请求时缓存机制的过程

参考资料 浏览器缓存机制介绍

1 人点赞