前端性能优化原理与实践(一)

摘自前端性能优化原理与实践

网络层面的性能优化

我们从输入 URL到显示页面这个过程中,涉及到网络层面的,有三个主要过程:

  • DNS 解析
  • TCP 连接
  • HTTP 请求/响应

对于DNS解析和 TCP 连接两个步骤,我们前端可以做的努力非常有限。相比之下,HTTP 连接这一层面的优化才是我们网络优化的核心。

HTTP 优化有两个大的方向:

  • 减少请求次数
  • 减少单次请求所花费的时间

webpack 优化方案

不要让 loader 做太多事情——以 babel-loader 为例

最常见的优化方式是,用 includeexclude 来帮我们避免不必要的转译,比如webpack 官方在介绍 babel-loader时给出的示例:

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /(node_modules|bower_components)/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }
  ]
}

这段代码帮我们规避了对庞大的node_modules 文件夹或者 bower_components文件夹的处理。但通过限定文件范围带来的性能提升是有限的。除此之外,如果我们选择开启缓存将转译结果缓存至文件系统,则至少可以将babel-loader 的工作效率提升两倍。要做到这点,我们只需要为loader增加相应的参数设定:

loader: 'babel-loader?cacheDirectory=true'

Gzip 压缩原理

我们日常开发中,其实还有一个便宜又好用的压缩操作:开启 Gzip

具体的做法非常简单,只需要你在你的request headers中加上这么一句:

accept-encoding:gzip

webpack 的 Gzip 和服务端的 Gzip

一般来说,Gzip压缩是服务器的活儿:服务器了解到我们这边有一个Gzip 压缩的需求,它会启动自己的CPU去为我们完成这个任务。

而压缩文件这个过程本身是需要耗费时间的,大家可以理解为我们以服务器压缩的时间开销和CPU开销(以及浏览器解析压缩文件的开销)为代价,省下了一些传输过程中的时间开销。

既然存在着这样的交换,那么就要求我们学会权衡。服务器的 CPU性能不是无限的,如果存在大量的压缩需求,服务器也扛不住的。服务器一旦因此慢下来了,用户还是要等。WebpackGzip 压缩操作的存在,事实上就是为了在构建过程中去做一部分服务器的工作,为服务器分压。

因此,这两个地方的 Gzip 压缩,谁也不能替代谁。它们必须和平共处,好好合作。作为开发者,我们也应该结合业务压力的实际强度情况,去做好这其中的权衡。

图片优化

就图片这块来说,与其说我们是在做“优化”,不如说我们是在做“权衡”。因为我们要做的事情,就是去压缩图片的体积(或者一开始就选取体积较小的图片格式)。但这个优化操作,是以牺牲一部分成像质量为代价的。因此我们的主要任务,是尽可能地去寻求一个质量与性能之间的平衡点。

最经典的小图标解决方案——雪碧图(CSS Sprites)

图像精灵(sprite,意为精灵),被运用于众多使用大量小图标的网页应用之上。它可取图像的一部分来使用,使得使用一个图像文件替代多个小文件成为可能。相较于一个小图标一个图像文件,单独一张图片所需的 HTTP请求更少,对内存和带宽更加友好。

和雪碧图一样,Base64 图片的出现,也是为了减少加载网页图片时对服务器的请求次数,从而提升网页性能。Base64是作为雪碧图的补充而存在的。

Base64是一种用于传输 8Bit字节码的编码方式,通过对图片进行 Base64编码,我们可以直接将编码结果写入 HTML或者写入CSS,从而减少HTTP 请求的次数。

我们加载图片需要把图片链接写入img标签:


浏览器就会针对我们的图片链接去发起一个资源请求。 但是如果我们对这个图片进行 Base64 编码,我们会得到一个这样的字符串:

data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAMJGlDQ1BJQ0MgUHJvZmlsZQAASImVlwdUU8kagOeWJCQktEAEpITeBCnSpdfQpQo2QhJIKDEkBBU7uqjgWlARwYquitjWAshiw14Wwd4fiKgo62LBhsqbFNDV89477z9n7v3yzz9/mcydMwOAehxbJMpFNQDIExaI48MCmeNT05ikR4AECIAKRgEamyMRBcTFRQEoQ+9/yrubAJG9r9nLfP3c/19Fk8uTcABA4iBncCWcPMiHAMDdOCJxAQCEXqg3m1YggkyEWQJtMUwQsrmMsxTsIeMMBUfJbRLjgyCnA6BCZbPFWQCoyfJiFnKyoB+1pZAdhVyBEHIzZF8On82F/BnyqLy8qZDVrSFbZ3znJ+sfPjOGfbLZWcOsqEUuKsECiSiXPeP/nI7/LXm50qEYZrBR+eLweFnNsnnLmRopYyrk88KMmFjIWpCvC7hyexk/4UvDk5T2HziSIDhngAEASuWygyMhG0A2FebGRCn1vpmCUBZkOPdooqCAlagYi3LFU+OV/tHpPElIwhCzxfJYMptSaU5SgNLnRj6PNeSzqYifmKLIE20rFCTHQFaDfF+SkxCptHlexA+KGbIRS+NlOcP/HAOZ4tB4hQ1mnicZqgvz4gtYMUqO4rDl+ehCnlzATwxX+MEKeZLxUUN5cnnBIYq6sGKeMEmZP1YuKgiMV47dJsqNU9pjzbzcMJneFHKrpDBhaGxfAVxsinpxICqIS1TkhmtnsyPiFHFxWxAFgkAwYAIpbBlgKsgGgtbehl74S9E

浏览器缓存机制介绍与缓存策略剖析

大家倾向于将浏览器缓存简单地理解为“HTTP 缓存”。但事实上,浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  • Memory Cache
  • Service Worker Cache
  • HTTP Cache
  • Push Cache

HTTP 缓存机制探秘

HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

强缓存的特征

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

强缓存的实现:从 expires 到 cache-control

实现强缓存,过去我们一直用expires
当服务器返回响应时,在Response Headers 中将过期时间写入expires字段。像这样:

expires: Wed, 11 Sep 2019 16:12:18 GMT

可以看到,expires是一个时间戳,接下来如果我们试图再次向服务器请求资源,浏览器就会先对比本地时间和expires 的时间戳,如果本地时间小于 expires设定的过期时间,那么就直接去缓存中取这个资源。

从这样的描述中大家也不难猜测,expires 是有问题的,它最大的问题在于对“本地时间”的依赖。如果服务端和客户端的时间设置可能不同,或者我直接手动去把客户端的时间改掉,那么 expires 将无法达到我们的预期。

考虑到expires 的局限性,HTTP1.1新增了 Cache-Control字段来完成expires的任务。

cache-control: max-age=31536000

如大家所见,在 Cache-Control 中,我们通过 max-age来控制资源的有效期。max-age不是一个时间戳,而是一个时间长度。在本例中,max-age31536000秒,它意味着该资源在 31536000秒以内都是有效的,完美地规避了时间戳带来的潜在问题。

Cache-Control相对于expires更加准确,它的优先级也更高。当Cache-Controlexpires同时出现时,我们以 Cache-Control为准。

no-store与no-cache

no-cache绕开了浏览器:我们为资源设置了no-cache 后,每一次发起请求都不会再去询问浏览器的缓存情况,而是直接向服务端去确认该资源是否过期(即走我们下文即将讲解的协商缓存的路线)。

no-store比较绝情,顾名思义就是不使用任何缓存策略。在no-cache的基础上,它连服务端的缓存确认也绕开了,只允许你直接向服务端发送请求、并下载完整的响应。

协商缓存:浏览器与服务器合作之下的缓存策略

协商缓存依赖于服务端与浏览器之间的通信。

协商缓存机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求、下载完整的响应,还是从本地获取缓存的资源。

如果服务端提示缓存资源未改动(Not Modified),资源会被重定向到浏览器缓存,这种情况下网络请求对应的状态码是304

协商缓存的实现:从 Last-Modified 到 Etag

Last-Modified 是一个时间戳,如果我们启用了协商缓存,它会在首次请求时随着 Response Headers 返回:

Last-Modified: Fri, 27 Oct 2017 06:35:57 GMT

随后我们每次请求时,会带上一个叫If-Modified-Since的时间戳字段,它的值正是上一次response返回给它的last-modified值:

If-Modified-Since: Fri, 27 Oct 2017 06:35:57 GMT

服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified值;否则,返回如上图的 304响应,Response Headers不会再添加Last-Modified字段。

使用Last-Modified存在一些弊端,这其中最常见的就是这样两个场景:

我们编辑了文件,但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件,它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时,会被当做新资源,进而引发一次完整的响应——不该重新请求的时候,也会重新请求。

当我们修改文件的速度过快时(比如花了100ms完成了改动),由于 If-Modified-Since只能检查到以秒为最小计量单位的时间差,所以它是感知不到这个改动的——该重新请求的时候,反而没有重新请求了。

这两个场景其实指向了同一个bug——服务器并没有正确感知文件的变化。为了解决这样的问题,Etag作为Last-Modified的补充出现了。

Etag是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag就是不同的,反之亦然。因此Etag能够精准地感知文件的变化。

EtagLast-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串,它可以是这样的:

ETag: W/"2a3b-1602480f459"

那么下一次请求时,请求头里就会带上一个值相同的、名为if-None-Match 的字符串供服务端比对了:

If-None-Match: W/"2

你可能感兴趣的:(前端性能优化原理与实践(一))