搜课云网 > 北京千禧艺海数字设计 > 资讯总汇 > 前端工程与性能优化

前端工程与性能优化

机构:北京千禧艺海数字设计 时间:2016-01-06 12:02:37 点击:577

  同样的,也可以使用图片压缩工具对图像进行压缩,实现“图像优化”原则,这两条原则是对单个资源的处理,因此不会引起任何工程方面的问题;

  很多团队也通过引入代码校验流程来确保实现“避免 css 表达式”和“避免重定向”原则;

  目前绝大多数互联网公司也已经开启了服务端的 Gzip 压缩,并使用 CDN 实现静态资源的缓存和快速访问;北京网页设计课程培训机构整理。

  一些技术实力雄厚的前端团队甚至研发出了自动 CSS Sprites 工具,解决了 CSS Sprites 在工程维护方面的难题。使用“查找 - 替换”思路,我们似乎也可以很好的实现“划分主域”原则。

  我们把以上这些已经成熟应用到实际生产中的优化手段去除掉,留下那些还没有很好实现的优化原则,再来回顾一下之前的性能优化分类:

优化方向 优化手段
请求数量 合并脚本和样式表,拆分初始化负载
请求带宽 移除重复脚本
缓存利用 添加 Expires 头,配置 ETag,使 Ajax 可缓存
页面结构 将样式表放在顶部,将脚本放在底部,尽早刷新文档的输出

  诚然,不可否认现在有很多顶尖的前端团队可以将上述还剩下的优化原则也都一一解决,但业界大多数团队都还没能很好的解决这些问题,因此接下来本文将就这些原则的解决方案做进一步的分析与讲解,从而为那些还没有进入前端工业化开发的团队提供一些基础技术建设意见,也借此机会与业界顶尖的前端团队在工业化工程化方向上交流一下彼此的心得。

  静态资源版本更新与缓存

  如表格 2 所示,在“缓存利用”分类中保留了“添加 Expires 头”和“配置 ETag ”两项,或许有些人会质疑,明明这两项只要配置了服务器的相关选项就可以实现,为什么说它们难以解决呢?确实,开启这两项很容易,但开启了缓存后,我们的项目就开始面临另一个挑战:如何更新这些缓存。

  相信大多数团队也找到了类似的答案,它和《高性能网站建设指南》关于“添加 Expires 头”所说的原则一样——修订文件名。即:

  思路没错,但要怎么改变链接呢?变成什么样的链接才能有效更新缓存,又能最大限度避免那些没有修改过的文件缓存不失效呢?

  先来看看现在一般前端团队的做法:

  或者

  大家会采用添加 query 的形式修改链接。这样做是比较直观的解决方案,但在访问量较大的网站,这么做可能将面临一些新的问题。

  通常一个大型的 web 应用几乎每天都会有迭代和更新,发布新版本也就是发布新的静态资源和页面的过程。以上述代码为例,假设现在线上运行着 index.html 文件,并且使用了线上的 a.js 资源。index.html 的内容为:

  这次我们更新了页面中的一些内容,得到一个 index.html 文件,并开发了新的与之匹配的 a.js 资源来完成页面交互,新的 index.html 文件的内容因此而变成了:

  好了,现在要开始将两份新的文件发布到线上去。可以看到,a.html 和 a.js 的资源实际上是要覆盖线上的同名文件的。不管怎样,在发布的过程中,index.html 和 a.js 总有一个先后的顺序,从而中间出现一段或大或小的时间间隔。对于一个大型互联网应用来说即使在一个很小的时间间隔内,都有可能出现新用户访问,而在这个时间间隔中访问了网站的用户会发生什么情况呢:

  如果先覆盖 index.html,后覆盖 a.js,用户在这个时间间隙访问,会得到新的 index.html 配合旧的 a.js 的情况,从而出现错误的页面。

  如果先覆盖 a.js,后覆盖 index.html,用户在这个间隙访问,会得到旧的 index.html 配合新的 a.js 的情况,从而也出现了错误的页面。

  这就是为什么大型 web 应用在版本上线的过程中经常会较集中的出现前端报错日志的原因,也是一些互联网公司选择加班到半夜等待访问低峰期再上线的原因之一。此外,由于静态资源文件版本更新是“覆盖式”的,而页面需要通过修改 query 来更新,对于使用 CDN 缓存的 web 产品来说,还可能面临 CDN 缓存攻击的问题。我们再来观察一下前面说的版本更新手段:

  我们不难预测,a.js 的下一个版本是“ 1.0.1 ”,那么就可以刻意构造一串这样的请求“ a.js?v=1.0.1 ”、“ a.js?v=1.0.2 ”、……让 CDN 将当前的资源缓存为“未来的版本”。这样当这个页面所用的资源有更新时,即使更改了链接地址,也会因为 CDN 的原因返回给用户旧版本的静态资源,从而造成页面错误。即便不是刻意制造的攻击,在上线间隙出现访问也可能导致区域性的 CDN 缓存错误。

  此外,当版本有更新时,修改所有引用链接也是一件与工程管理相悖的事,至少我们需要一个可以“查找 - 替换”的工具来自动化的解决版本号修改的问题。

  对付这个问题,目前来说最优方案就是基于文件内容的 hash 版本冗余机制 了。也就是说,我们希望工程师源码是这么写的:

  但是线上代码是这样的:

  其中”_82244e91 ”这串字符是根据 a.js 的文件内容进行 hash 运算得到的,只有文件内容发生变化了才会有更改。由于版本序列是与文件名写在一起的,而不是同名文件覆盖,因此不会出现上述说的那些问题。那么这么做都有哪些好处呢?

  线上的 a.js 不是同名文件覆盖,而是文件名 +h ash 的冗余,所以可以先上线静态资源,再上线 html 页面,不存在间隙问题;

  遇到问题回滚版本的时候,无需回滚 a.js,只须回滚页面即可;

  由于静态资源版本号是文件内容的 hash,因此所有静态资源可以开启永久强缓存,只有更新了内容的文件才会缓存失效,缓存利用率大增;

  修改静态资源后会在线上产生新的文件,一个文件对应一个版本,因此不会受到构造 CDN 缓存形式的攻击

  虽然这种方案是相比之下最完美的解决方案,但它无法通过手工的形式来维护,因为要依靠手工的形式来计算和替换 hash 值并生成相应的文件将是一项非常繁琐且容易出错的工作。因此,我们需要借助工具。有了这样的思路,我们下面就来了解一下 fis 是如何完成这项工作的。

  首先,之所以有这种工具需求,完全是因为 web 应用运行的根本机制决定的:web 应用所需的资源是以字面的形式通知浏览器下载而聚合在一起运行的。这种资源加载策略使得 web 应用从本质上区别于传统桌面应用的版本更新方式,也是大型 web 应用需要工具处理的最根本原因。为了实现资源定位的字面量替换操作,前端构建工具理论上需要识别所有资源定位的标记,其中包括:

  css 中的@import url(path)、background:url(path)、backgournd-image:url(path)、filter 中的 src

  js 中的自定义资源定位函数,在 fis 中我们将其规定为__uri(path)。

  html 中的

  不难看出,我们目前已经实现了“按需加载”,“将脚本放在底部”,“将样式表放在头部”三项优化原则。

  前面讲到静态资源在上线后需要添加 hash 戳作为版本标识,那么这种使用模板语言来收集的静态资源该如何实现这项功能呢?答案是:静态资源依赖关系表。假设前面讲到的模板源代码所对应的目录结构为下图所示:

image9

  那么我们可以使用工具扫描整个 project 目录,然后创建一张资源表,同时记录每个资源的部署路径,可以得到这样的一张表:

  {

  "res": {

  "A/A.css": {

  "uri": "/A/A_1688c82.css",

  "type": "css"

  },

  "B/B.css": {

  "uri": "/B/B_52923ed.css",

  "type": "css"

  },

  "C/C.css": {

  "uri": "/C/C_6dda653.css",

  "type": "css"

  },

  "bootstrap.css": {

  "uri": "bootstrap_08f2256.css",

  "type": "css"

  },

  "jquery.js": {

  "uri": "jquery_9155343.css",

  "type": "js"

  },

  },

  "pkg": {}

  }

  基于这张表,我们就很容易实现 {require name=” id ”} 这个模板接口了。只须查表即可。比如执行{require name=” jquery.js ”},查表得到它的 url 是“/jquery_9151577.js ”,声明一个数组收集起来就好了。这样,整个页面执行完毕之后,收集资源加载需求,并替换页面的占位符,即可实现资源的 hash 定位,得到:

  html of A

  html of B

  html of C

  接下来,我们讨论如何在基于表的设计思想上是如何实现静态资源合并的。或许有些团队使用过 combo 服务,也就是我们在最终拼接生成页面资源引用的时候,并不是生成多个独立的 link 标签,而是将资源地址拼接成一个 url 路径,请求一种线上的动态资源合并服务,从而实现减少 HTTP 请求的需求,比如:

  html of A

  html of B

  html of C

  这个“/combo?files=file1,file2,file3,…”的 url 请求响应就是动态 combo 服务提供的,它的原理很简单,就是根据 get 请求的 files 参数找到对应的多个文件,合并成一个文件来响应请求,并将其缓存,以加快访问速度。

  这种方法很巧妙,有些服务器甚至直接集成了这类模块来方便的开启此项服务,这种做法也是大多数大型 web 应用的资源合并做法。但它也存在一些缺陷:

  浏览器有 url 长度限制,因此不能无限制的合并资源。

  如果用户在网站内有公共资源的两个页面间跳转访问,由于两个页面的 combo 的 url 不一样导致用户不能利用浏览器缓存来加快对公共资源的访问速度。

  对于上述第二条缺陷,可以举个例子来看说明:

  假设网站有两个页面 A 和 B

  A 页面使用了 a,b,c,d 四个资源

  B 页面使用了 a,b,e,f 四个资源

  如果使用 combo 服务,我们会得:

  A 页面的资源引用为:/combo?files=a,b,c,d

  B 页面的资源引用为:/combo?files=a,b,e,f

  两个页面引用的资源是不同的 url,因此浏览器会请求两个合并后的资源文件,跨页面访问没能很好的利用 a、b 这两个资源的缓存。

  很明显,如果 combo 服务能聪明的知道 A 页面使用的资源引用为“/combo?files=a,b ”和“/combo?files=c,d ”,而 B 页面使用的资源引用为“/combo?files=a,b ”,“/combo?files=e,f ”就好了。这样当用户在访问 A 页面之后再访问 B 页面时,只需要下载 B 页面的第二个 combo 文件即可,第一个文件已经在访问 A 页面时缓存好了的。

  基于这样的思考,fis 在资源表上新增了一个字段,取名为“ pkg ”,就是资源合并生成的新资源,表的结构会变成:

  {

  "res": {

  "A/A.css": {

  "uri": "/A/A_1688c82.css",

  "type": "css"

  },

  "B/B.css": {

  "uri": "/B/B_52923ed.css",

  "type": "css"

  },

  "C/C.css": {

  "uri": "/C/C_6dda653.css",

  "type": "css"

  },

  "bootstrap.css": {

  "uri": "bootstrap_08f2256.css",

  "type": "css"

  },

  "jquery.js": {

  "uri": "jquery_9155343.css",

  "type": "js"

  },

  },

  "pkg": {

  "p0": {

  "uri": "/pkg/utils_b967346.css",

  "type": "css",

  "has": ["bootstrap.css", "A/A.css"]

  },

  "p1": {

  "uri": "/pkg/others_0d4552a.css",

  "type": "css",

  "has": ["B/B.css", "C/C.css"]

  }

  }

  }

  相比之前的表,可以看到新表中多了一个 pkg 字段,并且记录了打包后的文件所包含的独立资源。这样,我们重新设计一下{require name=” id ”}这个模板接口:在查表的时候,如果一个静态资源有 pkg 字段,那么就去加载 pkg 字段所指向的打包文件,否则加载资源本身。比如执行{require name=” bootstrap.css ”},查表得知 bootstrap.css 被打包在了“ p0 ”中,因此取出 p0 包的 url “/pkg/utils_b967346.css ”,并且记录页面已加载了“ bootstrap.css ”和“ A/A.css ”两个资源。这样一来,之前的模板代码执行之后得到的 html 就变成了:

  html of A

  html of B

  html of C

  css 资源请求数由原来的 4 个减少为 2 个。这样的打包结果是怎么来的呢?答案是配置得到的。我们来看一下带有打包结果的资源表的 fis 配置:

  fis.config.set('pack', {

  'pkg/util.css': [ 'bootstrap.css', 'A/A.css'],

  'pkg/other.css': [ '**.css' ]

  });

  我们将“ bootstrap.css ”、“ A/A.css ”打包在一起,其他 css 另外打包,从而生成两个打包文件,当页面需要打包文件中的资源时,模块框架就会收集并计算出最优的资源加载结果,从而解决静态资源合并的问题。

  这样做的原因是为了弥补 combo 在前面讲到的两点技术上的不足而设计的。但也不难发现这种打包策略是需要配置的,这就意味着维护成本的增加。但好在它有两个优势可以一定程度上弥补这个问题:

  打包的资源只是原来独立资源的备份。打包与否不会导致资源的丢失,最多是没有合并的很好而已。

  配置可以由工程师根据经验人工维护,也可以由统计日志生成,这为性能优化自适应网站设计提供了非常好的基础。

  关于第二点,fis 有这样辅助系统来支持自适应打包算法:

image10

  至此,我们通过基于表的静态资源管理系统和三个模板接口实现了几个重要的性能优化原则。