原稿:语雀 · 性能优化 · 原目录:Web & 浏览器 › 性能优化

资源压缩

资源压缩主要包括html压缩、css压缩、js压缩和混乱、文件压缩

  • html压缩:在 html 中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如注释
  • css压缩:css 代码压缩简单来说就是无效代码删除和 css 语义合并
  • js的压缩和混乱
  1. 无效字符的删除
  2. 剔除注释
  3. 代码语义的缩减和优化
  4. 代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)
  • 文件压缩:各种媒体文件等

gzip 是一种常见的前端压缩格式,需要前后端都支持,请求头在 Accept-Encoding 中标识对压缩支持,如果支持gzip,响应时内容会压缩返回给客户端,在响应头的 content-encoding:gzip

image.png

异步加载

浏览器解析 html 时会遇到外部资源链接:CSS 样式资源、JS 脚本、媒体类资源。对于外链资源,浏览器会单独开启下载线程下载资源

  • CSS下载时异步,不会阻塞html解析,不过 render 过程必须等待 CSS 下载下载解析完毕
  • JS则会直接阻塞 html 解析,直到当前脚本被下载执行完毕

因此对于 JS 加载有优化的必要,h5中的 defer 和 async 可以修改脚本加载方式


浏览器缓存

使用浏览器缓存技术可以大幅减少请求响应的时间,提高性能,缓存可简单的划分为两类:强缓存与协商缓存

  • 强缓存(200 from cache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求
  • 协商缓存(304)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存

优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存

image.png

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信

强缓存:Expires

expires: Sat, 29 Aug 2020 08:43:05 GMT

当服务端返回响应时,在 Response Header 中将过期时间写入 expires 字段中。当浏览器再次加载对应资源文件时,如果在这个过期时间内,则命中强缓存。反之重新获取。但是expires依赖本地时间,如果服务端和客户端时间不同,expires可能无法准确控制

强缓存:Cache-Control

cache-control: max-age=600, s-maxage=3600

cache-control 通过 max-age 来控制资源的有效期,value 值为时间长度,秒为单位,用数值表示。则代表在这个请求正确返回时间(浏览器也会记录下来)的 600 秒内再次加载资源,就会命中强缓存。完美地规避了时间戳带来的潜在问题

expires 与 cache-control 两者区别就在于 expires 是 http1.0 的产物,cache-control 是 http1.1 的产物。expires 其实是过时的产物,现阶段它的存在是一种向下兼容。两者共同出现时,cache-control优先级更高

cache-control有几个重要的值,其中no-store和no-cache很容易被错用,前者表示不缓存,后者表示缓存,但使每次缓存都立即失效(即强制进行协商缓存)

public

表时响应可以被客户端和代理服务器缓存

private

表示响应只可以被客户端缓存

max-age=xxx

缓存xx秒后就过期,需要重新请求

s-maxage=xxx

覆盖max-age,作用同上,只在代理服务器生效

no-store

不缓存

no-cache

资源被缓存,但是立即失效,下次会发起验证资源是否过期

协商缓存:Last-Modified & If-Modified-Since

协商缓存需要配合Cache-Control使用,算作前者的优化

  • 第一次请求资源:服务器将资源传递给客户端时,会将资源最后更改的时间以 Last-Modified 的形式加在实体首部上一起返回给客户端
  • 随后接下每次请求时,都会带上 If-Modified-Since 字段的时间戳字段,值为上一次 response 返回给它的 Last-Modified 值
  • 服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。

但 Last-Modified 存在一些缺点:

  • 某些服务端不能获取精确的修改时间:小于 1s 的差别无法区分
  • 可能文件修改时间改了,但文件内容却没有变

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略

协商缓存:ETag & If-None-Match

Etag 是上一次加载资源时服务器返回的 response header,是对该资源的一种唯一标识,只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致。如果服务器发现 ETag 匹配不上,那么将新的资源发给客户端;如果一致,则直接返回 304 ,告知客户端直接使用本地缓存

拓展:服务端如何生成 header

  • 服务端设置 Last-Modified

在 koa2 框架下,通过 fs.statSync(‘xxx’).mtimeMs 获取资源自上次修改后的时间戳,转为标准时间后设置为 Last-Modified,接收到请求后截取其中的

  let since=ctx.request.get('If-Modify-Since');
    if(since){
      let sincetime=new Date(since).getTime();
      var info=fs.statSync('./index.js');
      if(sincetime<info.mtimeMs){
        //说明更新过
        ctx.response.set('Last-Modified',new Date(info.mtimeMs).toGMTString());
        ctx.body ={
          message:'hello koa-body'
        }
      }
      else{
        //说明没更新
        ctx.status = 304;
        return;
      }
    }
  • 服务端设置 ETag

参考链接:轻松理解浏览器缓存(Koa缓存源码解析)

koa-etag 中间件使用了“文件修改时间戳+文件大小”转16进制之后加盐(crypto 库的 sha1 算法)生成 etag

function stattag (stat) {
  var mtime = stat.mtime.getTime().toString(16)
  var size = stat.size.toString(16)

  return '"' + size + '-' + mtime + '"'
}

小结

  • 强制缓存优先于协商缓存进行
  • 若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,返回200 (from cache)
  • 若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match)

协商缓存由服务器决定是否使用缓存

  • 若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中
  • 生效则返回 304,继续使用缓存
cache.png

CDN

CDN 加速的原理

CDN 是将源服务器上的静态资源缓存在多个 CDN server 上,当用户请求资源时,会根据就近性和服务器负载为用户确定一个最能够提供快速稳定访问的服务器进行资源获取

运行模式: 2. 主站 index.html 可以不上 cdn,其中的模块都使用编译哈希组成文件名,当模块未更新,直接获取 CDN server 资源即可,否则去源站拉取最新模块,类似将 index.html 当作 manifest 4. 对于 CDN 与源站点同步问题,可以设置刷新时间策略,按照一定频率由向源站点拉取;CDN server 还可手动清理缓存,强制拉取最新内容

按类型存放资源

HTTP1.x 下,浏览器会对于向同一域名并发请求数限制在 4~8 个,可以按静态资源类型设置不同域的 CDN server,绕过最大并发限制,例如 JS 文件放在 js.cdn.com 下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过 dns-prefetch 来解决  <link rel=‘dns-prefetch’ href=’//js.cdn.com’> ,形如 //xx.com 的 URL 省略了协议,浏览器在访问资源时会自动根据当前 URL 采用的模式来决定使用 HTTP 还是 HTTPS 协议

综上,使用 CDN 构建需要满足以下几点:

  • 静态资源导入的 URL 要变成指向 CDN 服务的绝对路径的 URL
  • 静态资源的文件名需要带上根据内容计算出的 Hash 值
  • 不同类型资源放在不同域名的CDN上,配合 dns prefetch 抵消域名解析耗时

懒加载

图片懒加载:如 vue-lazyload 组件,在图片进入可视区后再请求

组件懒加载:vue-router 中使用动态 import() 加载组件,示例可见webpack-vue_3:优化


服务端推送

(1)Ajax 轮询,利用 XHR,通过 setInterval 定时向后端发送请求

优点:实现简单,客户端实现即可,不需要服务端配合

缺点:数据同步不及时,且大多数情况下无用请求,增加后端处理压力

实现方式:客户端每隔一段时间调用接口,无论有没有数据,接口立即返回

使用场景:不想折腾的开发者,消息及时性要求没那么高,服务器资源资源足

(2)Ajax 长轮询

优点:消息及时,命中率高,消耗服务端资源少

缺点:服务端和客户端需要同时改造,消息会有部分延迟(发生在请求交替之时)

实现方式:客户端在上次请求返回后,在发送下次请求,服务端当有数据或者超时后返回,没有数据时保持链接(超时时间需要综合考虑服务器性能和及时性做出平衡,有代理的话需要考虑代理对于链接的超时机制)。

使用场景:扫码登录,微信网页端获取消息等。

(3)websocket,允许服务端主动向客户端发送数据,浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并进行双向数据传输

优点:通信及时,通信模式采用双工,类似于打电话

缺点:服务端和客户端需要同时改造,当连接过多时,消耗服务端资源比较大。

实现方式:客户端和服务端需要建立长连接,通常基于 http1.1-keepalive、websocket、iframe 等

使用场景:实时性要求很高,银行系统,股票系统等

(4)http2.0:http2 支持服务器主动向推送,比如浏览器请求一个页面时,服务器会将 html 中的静态资源如图片和 css、js 等随带主动返回给浏览器,而不必等到浏览器解析 html 时再次请求

优点:协议原生、可利用 http2.0 的优化

缺点:协议普及问题,尚需升级