阅读 287

细说HTTP增量更新

合理利用缓存是Web性能优化的必要手段。而增量更新是目前大部分团队采用的缓存更新方案,结合HTTP强制缓存策略,既能够保证用户在第一时间获取最新资源,又可以减少网络资源消耗,提高Web应用程序的执行速度。

覆盖更新和增量更新

覆盖更新3年前用得比较多,现在已经逐渐淘汰,我们先引用一个简单场景来说明一下两者的区别和增量更新的优势。

假设项目中存在一个css文件和一个js文件,由 index.html 引入:

<html>
  <head>
    <link rel="stylesheet" href="index.css" />
  </head>
  <body>
    <script src="index.js" />
  </body>
</html>
复制代码

为了提高页面加载性能,我们启动浏览器强制缓存策略,index.css 和 index.js 均被缓存到本地,如果在缓存有效期内发生了迭代,为了保证让用户第一时间获取最新内容,就必须让浏览器放弃之前的缓存文件而继续请求服务器并下载最新的资源。

覆盖更新方案

覆盖更新的方案和他的名字一样粗暴,在引用资源的URL后添加请求参数,比如添加一个版本号信息:

<html>
  <head>
    <link rel="stylesheet" href="index.css?v=1.0.0" />
  </head>
  <body>
    <script src="index.js?v=1.0.0" />
  </body>
</html>
复制代码

这样 inde.css 和 index.js 会被重新请求并下载,确实起到了更新缓存的作用。如果下一波迭代只改动了 index.js ,按照这个方案可以只增加 index.js 的版本号:

<html>
  <head>
    <link rel="stylesheet" href="index.css?v=1.0.0" />
  </head>
  <body>
    <script src="index.js?v=1.0.1" />
  </body>
</html>
复制代码

有针对性的参数修改工作对开发人员来说并不困难,因为参与开发的人员知道哪些文件改动了,但项目一旦大了或迭代次数多了就会越来越繁琐。工程化的思想指导我们使用工具替代人工,但工具没有记忆,想要让工具识别改动的文件并针对性地修改版本号参数有两个方案:

  • 通过人工记录改动的文件列表并让工具读取此配置(还是没有解放人力❌)
  • 让工具自动获取文件改动之前的内容后逐一对比(比较耗时❌)

哈希指纹

解决上面的差异更新问题,我们可以先从版本号上做文章。其实URL后面的v参数目的就是让浏览器重新请求该资源,那能否让这个参数和文件内容自动地一一对应呢?这就是哈希指纹,我们可以用md5算法计算出文件哈希值,然后用哈希指纹替换最初的版本号:

<html>
  <head>
    <link rel="stylesheet" href="index.css?v=858d5483" />
  </head>
  <body>
    <script src="index.js?v=bbcaaf73" />
  </body>
</html>
复制代码

看起来挺不错,以前确实见到过一些网站应用过这种方案。但仔细想想还是存在2个问题:

更新部署同步性问题

必须确保 html 文件与改动的静态资源文件同步更新,否则会出现资源不同步的情况。目前大多数团队的部署方式是将网站的入口HTML和静态资源JS/CSS/图片等分开部署(即常见的静态资源托管)。两种资源分开部署必然会有先后顺序,也就是资源上线的时间差,这个时间差可大可小,具体影响取决于这个网站的流量规模。这也是很多发布选在凌晨或半夜这种网站访问量较低的时间段进行的原因之一。

不利于版本回滚

由于覆盖更新每次都是迭代之后的资源覆盖服务器上原有的旧版本(也就是服务器上永远只存一份最新的内容),这对版本回滚就不友好了。虽然运用git的版本回退,然后快速覆盖部署能缓解一定的压力,但这远没有直接在服务器上使用老版本的构建产品来得快。

增量更新方案

增量更新策略完美地解决了上述缺陷,实现的方案很简单,将原本作为参数值的哈希指纹,作为资源文件名的一部分,并且删除用于更新的url参数:

<html>
  <head>
    <link rel="stylesheet" href="index.858d5483.css" />
  </head>
  <body>
    <script src="index.bbcaaf73.js" />
  </body>
</html>
复制代码

在静态资源使用增量更新策略的前提下,可以将静态资源先于动态HTML部署,此时静态资源没有引用入口,不会对线上缓解产生影响;动态HTML部署后即可在第一时间访问已存在的最新静态资源。这样并很好地解决了更新部署同步性的问题。另一方面,增量更新不会覆盖旧版本文件,运维回滚时只需回滚HTML即可,这样不仅优化了版本控制,而且还可以支持多版本共存的需求,perfect!

按需加载与多模块构建场景

多模块构建就是指存在多个互不干扰的模块体系,这些模块体系可能存在同一页面,也可能存在于两个独立的页面,需要按需加载。这类场景下需要考虑以下2个问题:

  • 第一,同步模块的修改对异步文件和主文件哈希指纹产生的影响
  • 第二,异步模块的修改对主文件哈希指纹产生的影响

先来说说第一点

假设一个单页面应用的模块结构如下:

模块构建示意图(1).png

  • 主模块 main.app.js
  • 同步模块 module.sync.js(构建后与主模块合并为 main.app.[hash].js,同步加载)
  • 异步模块 module.async.js (单独构建为异步文件 app.async.[hash].js,按需加载)

从上:构建输出的文件哈希值会在参与计算的模块内容改动后产生变化,同步模块 module.sync.js 的内容作为计算因子参与主文件的哈希指纹,并未参与异步文件hash指纹的计算。所以可以确定的是,同步模块的修改影响主模块的哈希指纹,对异步文件无影响。

异步模块的修改对主模块的影响

异步模块的内容只影响异步文件的哈希指纹,是这样的吗?在此之前我们先搞清楚异步文件的加载原理:

window.onload = function() {
  var script = document.createElement('script');
  script.src = 'https://static.app.com/async.js';
  document.head.append(script);
}
复制代码

异步文件的URL被固定写死在负责加载它的主文件中,如果应用了哈希指纹:

window.onload = function() {
  var script = document.createElement('script');
  script.src = 'https://static.app.com/async.2483feal.js';
  document.head.append(script);
}
复制代码

如果我们假设异步模块更新了只修改了异步模块的哈希指纹(从 async.2483feal.js 变成 async.3234afcb.js ),而主模块的哈希指纹保持不变,即主模块里加载的异步模块 依然是 async.2483feal.js

显然,这并不是我们想要的结果,所以虽然有点不情愿,异步模块修改不仅仅影响自己的哈希指纹,主模块的哈希指纹也要跟着改变才能起到更新的作用。

如何用webpack实现增量更新

用Webpack实现增量更新,顾名思义就是给构建产物(JS和CSS文件)添加哈希指纹

配置JS文件输出

核心就是Webpack编译生成js文件追加hashcode:

{
    entry: './app.js',
    output: {
        filename: 'js/[name].[chunkhash:7].js',
        chunkFilename: 'js/[name].[chunkhash:7].js',
    }
}
复制代码

配置CSS文件输出

给CSS文件也添加哈希指纹可以通过插件 mini-css-extract-plugin 来实现:

onst MiniCssExtractPlugin = require('mini-css-extract-plugin')

{
    ...
    plugins: [
        new MiniCssExtractPlugin({
          filename: 'css/[name].[contenthash:7].css'
        })
    ]
}
复制代码

值得注意的是JS哈希指纹使用 chunkhash 来生成,而CSS则使用 contenthash,两者的区别在于:

chunkhash是指Webpack在打包chunk块时,根据chunk块内容生成的哈希指纹,文件内容不改变则其哈希值不变。而 contenthash并不是Webpack自身的另外一种哈希值,而是代表被导出内容计算后的哈希值,其值是相对主文件(JS)完全独立的。

因为CSS是通过JS模块导入的,所以理论上CSS也属于JS的内容部分,CSS内容改变时JS的哈希指纹也会跟着变化,这显然不是我们想要的结果,而 contenthash就是解耦JS与CSS文件哈希指纹的关键,于是我们可以通过contenthash让JS文件改变时CSS文件哈希指纹保持不变。

参考文献



关注下面的标签,发现更多相似文章
评论