关于大型网站技术演进的思考(二十一)--网站静态化处理—web前端优化—下【终篇】(13)
本篇继续web前端优化的讨论,开始我先讲个我所知道的一个故事,有家大型的企业顺应时代发展的潮流开始投身于互联网行业了,它们为此专门设立了一个事业部,不过该企业把这个事业部里的人事成本,系统运维成本特别是硬件采购的成本都由总公司来承担,当然互联网业务上的市场营销成本这块还是由该事业部自己承担,可是网站一年运维下来,该公司发现该事业部里最大的成本居然不是市场营销的开销,而是短信业务和宽带使用上的开销,是不是有点让人感到意外呢?下面我来分析下这个场景吧。
短信这块是和通讯运营商有关,很难从根本上解决,当然该企业可以考虑使用像微信这样的工具来分摊下短信的成本,但是宽带流量消耗这个问题却很难有第二选择了,可能有人会感到诧异,一家做互联网的企业,用户都是使用自己掏钱的宽带来上网的,为啥企业会有宽带流量的成本呢?其实互联网公司的后台服务都是会放置在IDC即数据中心里的,除非你的企业是真正的高富帅,或者你本身的核心业务就是互联网业务,这样的企业才有可能会自建数据中心,绝大部分企业都会租用第三方的数据中心,而且有些企业为了容灾还会在不同地域建立不同的数据中心,不同数据中心之间是通过专线来通讯的,而专线的成本是很高的,我们想让自己开发的网站让更多人用,可以通过改造服务端并发处理能力来达到这个目的,但是这里还有一个制约因素,那就是服务端使用的带宽,一般而言,企业选择多大带宽是可以估算出来,最终采用一个合理的带宽,但是,如果这家公司是电商类型网站,就很有可能碰到像双十一啊,或者自身做大促销的情况,这个时候服务端的负载压力就会成倍增加,远远超出平时的网络流量,如是企业会提前扩充带宽,而扩充的带宽流量是昂贵的,这样就会无形增加网站运营成本。如果我们不去思考成本问题,当今社会讲求环保,例如淘宝就说它们网站没完成一次交易使用的电量可以煮熟两个鸡蛋,它们网站一天下来消耗的电量相当于中国一个三线城市一天消耗的电量,那么如果我们能节约每次请求消耗的宽带流量其实也就是在节约能源,所以不管是从成本角度还是从环保角度提高宽带的利用率都是有很大的现实意义的。
Web前端优化里有一个技巧就是压缩http请求的数据量,这个技巧很多人都是简单认为http请求的数据越小,那么http处理速度就更快些,不过我认为这结论其实是一个相对的结论,现在的网速是越来越快,很多人家里接入的宽带已经使用上了光纤,50兆,百兆的宽带已经飞进了寻常百姓家了,那么这时候其实网络传输100kb数据和传输300kb数据的效率差异基本可以忽略不计了,当然并非每个人网络访问速度都这么快,例如我们使用手机的2G网络上网,那么100kb和300kb的传输效率还是会有很大差异的,所以压缩http请求大小这个手段在客户端这块是一种解决短板的技巧,这个短板就是照顾那些上网速度太慢的人了,而非对人人平等的技术手段,但是这个问题换到服务端就不同了,减少http报文的数据大小可以提升企业对宽带的利用率,是一种节约网站运营成本的一个重要手段,因此压缩http传输数据的大小是一个很有价值的技术手段。
用来压缩http请求数据大小的手段很多,例如使用Gzip压缩http请求,压缩图片等等,不过我这里要特别说明一个手段那就是减少cookie存储数据的大小,这是一个常常被忽视的压缩http请求大小的技术手段。不过cookie技术对很多初学者常常会感到差异,cookie是客户端的数据,为什么服务端和客户端都能操作它,难道服务端也会存储一份cookie的备份吗?之所以初学者会对cookie使用有疑问,这主要是初学者不太清楚cookie信息除了保存在浏览器端,它还会包含在http报文头里的,每个http请求响应都会带着cookie信息进行传递,所以cookie既可以被客户端操作也能被服务端操作,如果我们忽视cookie这个特点,再加上我们滥用cookie,最后cookie被撑满了,这也就意味每次请求响应的数据量会增加,而这些信息可能大部分都不会被使用,纯粹多余。而网站在开发和维护时候很容易不自觉的让cookie变得越来越多,越来越大,如果我们一开始就明确cookie这个特点,提前设计cookie使用规范,那么就可以一定程度上规避cookie不合理使用导致的http数据量变大的问题。如果网站使用了单独的静态资源服务器,并且把静态资源放置在单独的域名下面,这个时候我们还要避免给静态资源域名下使用cookie技术,因为静态资源基本都不会有状态信息,使用cookie只会无谓的增加请求的数据大小。
网络是存储设备里效率最差的,如果页面加载时候还有些请求是一个坏请求,例如页面访问的某些静态资源突然丢了,浏览器这个时候会有一个容错的做法,这个做法具体是:浏览器不能确定有问题的请求到底是因为网速慢了还是找不到,所以浏览器会多次请求这个url,直到浏览器认为这个url的确是有问题无法访问了,浏览器才不去继续请求了,如果碰到的资源正好是外部javascript文件,那就很有可能阻塞整个页面的加载,所以剔除页面里的坏请求也是要经常留心的事情。
我们如果再进一步分析下web前端优化的一些手段,就会发现很多优化手段其实都是基于静态资源来处理的,静态资源的特点就是在一定时间范围内不会发生变化的,而且当用户请求静态资源时候,服务端不需要任何计算操作即消耗CPU资源就能把结果返回给客户端,静态资源这种不参与计算的特点就可以让静态资源和业务应用服务器解耦,因此我们可以把静态资源单独抽取出来放置在CDN或者是请求效率处理更佳的静态资源服务器上。和静态资源相对的动态资源就很难做到这点,我们仔细回味下网站后台整个应用架构,就会发现所有网站都会使用存储系统即基本都会用数据库,而且应用服务器和数据库又是一种紧耦合的关系,因为我们想消除存储系统的状态问题基本是不可能完成的任务,这就让应用服务器没法做成CDN的形式,因此动态资源处理想使用CDN这种减少距离对网络通讯影响的手段基本是很麻烦的。我觉得网站静态化处理其实是根据web前端优化技术产生的技术,它让网站静态化资源和动态资源分离做的更好,所以我说网站静态化技术是充分发挥web前端优化手段的重要保证,这也就是我为什么会把web前端优化的内容也要放在网站静态化处理系列里的原因了。
静态资源因为在一定时间里不会发生变化,容易被缓存,而且浏览器本身也有缓存机制,那么如果我们把静态资源缓存在浏览器端,用户请求网站就不需要再去请求网络资源,这个效率不就更高了吗?现实情况的确是如此,但是我们想让浏览器端充分发挥这个缓存作用其实并非那么简单,因为我们会碰到如下的问题,具体如下:
问题一:网站对浏览器的控制是一种被动控制,用户才是控制浏览器的主动方,用户的很多行为都会导致网站对浏览器的缓存设计策略失效,如果缓存失效,那么用户再去访问网站时候就得重新请求资源,所以为了弥补浏览器缓存的不可靠性,CDN技术以及静态资源服务器的使用就派上用场了。
问题二:浏览器缓存网页部分资源可以让网页加载的更快,但是要做到这一点之前,我们首先要明确何时采用,同时采用何种方式让客户端缓存这些可以被缓存的资源?那么我们在知道某个用户要访问网站了,我们提前把需要缓存的资源发送个用户,让用户先缓存下这些资源,这个做法肯定是开国际玩笑了,一般我们都是在用户第一次访问网站时候将可以缓存的资源缓存起来,这个时候问题又来了,那就是用户第一次访问网站时候因为需要缓存的资源都没有被缓存,所以全部的资源都要通过网络请求下载,这个时候就会导致用户第一次访问网站页面的效率很差,有人可能认为网站又不是设计为访问一次的产品,只要资源被缓存了网页就会更快的,要是用户觉得第一次访问慢了,就先忍忍吧,以后会快的,这个想法又是再开国际玩笑了。就算用户忍受了第一次访问慢的情形,但是如果用户使用这个网站的时间间隔是很长的,例如某些专业性的网站,它的用户可能会很长一段时间后再访问该网站,而过了这段时间后,浏览器缓存的资源很有可能失效了,这个时候用户再去访问又等于是第一次访问了,那么我们这个缓存设计方案基本就是无效了。
问题二所反映的问题也就表明我们在如何合理使用浏览器缓存这块上是需要考虑用户的使用场景的,需要加入一些业务性的策略了,只有这样浏览器缓存方案才能充分发挥其优势。下面我就来谈论下浏览器端缓存策略设计的问题了。
首先我们来看一个场景,用户第一次访问网站,访问的是网站的首页,我们按照web前端优化原则设计了网站首页,特别是使用了一个优化原则就是把css合并成一个外部css文件,把javascript代码也合并成一个外部文件,首页都引入了这两个外部文件,这种情况首页访问至少会产生三个http请求,可是网站首页其实没有那么复杂,也就是说首页使用的css代码和javascript代码其实并不太多,如果我们把这些代码就放置到页面内部,那么首页加载就只有一个请求,虽然这会导致这个请求的数据量变大,不过按照我前面说到压缩http请求数据大小,其实在提升网络传输速度上这个角度是值得商榷的,但是多个http请求就会导致浏览器打开更多连接,而每个连接的建立和销毁却是十分消耗计算资源的,那么如果我们能把三个请求合并成一个请求完成就一定会让请求处理的更快,可是这个做法就会导致css和javascript文件没法被缓存,那么以后想复用它们就麻烦了。碰到这样的问题我们又该如何来抉择了?最理想的结果就是二者兼顾,但是要兼顾二者,那么页面就一定要处理这三个http请求了,我们到底能不能做到二者兼顾了?答案是肯定的,我们可以做到的。我们仔细的分析下这个场景,就会发现,快速加载页面和缓存静态资源在页面首次访问这个背景下其实是两个不同的业务操作,用户第一次访问首页用户只会关心页面是否快速被加载,至于加载静态资源的行为以及缓存静态资源的行为,用户是不用关心,因此我们就可以拆分这两个操作,首先是让页面快速被加载,等页面加载完毕后,我们在通过异步手段来加载外部的静态资源,这样就可以做到二者兼顾了,至于如何异步加载静态资源,我在以前的文章里讲述过,这篇文章就是《探真无阻塞加载javascript脚本技术,我们会发现很多意想不到的秘密》,不了解这个技术的朋友可以看看本篇文章。
不过要让上面的方案发挥作用是有一个大的前置条件的,那就是我们要判断出用户到底是不是第一次访问,而且因为外部的css文件和外部的javascript文件都被我们合并成了一个文件,这也就是说首页里内嵌的css代码和javascript代码和外部文件是有一个冗余的,如果用户第二次访问时候不需要这些操作了,那么让首页保持这个冗余是不是就没有这个必要了?特别是javascript代码,重复的javascript代码总是让人觉得不放心。这两个问题的核心还是在于如何判断用户是否第一次访问,判断用户的行为那就是属于判断用户状态的问题了,用户的状态标记在服务端使用的是session技术,浏览器端使用的是cookie技术,而session技术是一个临时会话存储技术,因此使用session是没法判断用户以前是否访问过该网站,所以这里只能使用cookie技术(如果浏览器支持html5,客户端保存用户状态的信息手段就更加多了,不一定非要使用cookie了),也就是当用户第一次访问网站时候,我们将一些可以标记用户是否访问过网站的状态信息存储在cookie里,那么用户再次访问这个网站时候,http请求就会把cookie信息传送给服务端,服务端通过cookie信息判定用户是否第一次访问,这个时候服务端可以剔除页面里内嵌的css代码和javascript代码,同时可以阻止浏览器再异步加载外部css文件和外部javascript文件行为,这样用户再次访问网站的行为也不会被用户第一次访问行为干扰了。
上面场景里还有一个优化手段的使用是值得商榷的,那就是我们把网站所有的css代码和javascript代码合并到一个文件里。这里我首先来讲讲把所有javascript代码合并成一个文件的问题,一个网站会包含很多不同页面,不同的页面因为业务场景的不同,就会导致每个页面都有专属的处理业务逻辑的javascript代码,如果我们简单的认为把javascript代码放置到外部文件就会让页面加载的更快,那么当我们合并外部文件时候这些和页面紧耦合的业务代码也会合并到一个文件里,最后就会导致最终的外部javascript文件变得特别大,对于浏览器而言,javascript代码过多也会影响到页面的加载效率和javascript的执行效率,而且这个超大的外部javascript文件对于某一个功能页面而言有太多冗余的代码,所以我们简单把全部外部javascript文件合并成一个外部javascript文件这个做法其实并不是太好,因此到底哪些javascript外部文件应该被合并这是有所选择的。而且把某些业务相关的javascript代码就写在页面,和页面一起被下载可能比我们单独使用外部文件的javascript效率更高,因为单独的外部javascript文件会增加页面http请求的个数,那么我们在开发时候那些javascript代码需要内嵌入页面,那些javascript代码要把放在单独外部文件里这也是我们要注意的策略问题。
我们毫无原则的把外部css文件和javascript文件合并成少量的外部文件还会影响到网站的运维和浏览器的缓存策略,特别是缓存策略的失效机制,例如网站某个页面做了css代码或者javascript代码的修改,而这些代码上生产时候要被合并到一个外部的css文件和javascript文件里,而这些外部文件又被很多网页引用,那么我们就不得不让很多无关的网页也需要刷新浏览器缓存,如果这个修改是作用于公共代码这个问题还好理解,要是这个代码是用于营销活动这个特定场景下,那么这样的刷新缓存操作就会显得非常没有必要,如果有天营销活动结束了,我们是不是还要再刷新下缓存,剔除多余的代码呢?所以如何合并外部的css代码和javascript代码我们还是要应该根据业务场景进行合理的分组的。
Web前端的工作是十分繁重的,网站是和终端用户打交道,这些终端用户都是网站的需求方,而web前端是处理终端用户需求的排头兵,用户那么多,需求那么多,所以网站的前端页面会经常的被修改,修改的页面就要重新发布生产,这个时候我们就要刷新浏览器的缓存了,那如何来刷新页面的缓存了,方法其实很简单就是改变页面url的参数,一般网站的静态资源的url后面我们会专门加上一个版本号参数,例如:
www.cnblogs.com/sharpxiajun/a.css?v=1234556
我们只要改变12345这个参数的值就能让浏览器重新从服务端获取静态资源,这个时候问题来了,如果外部css文件或javascript文件被很多页面引用,那么我们就不得不去手动的更改页面里引用这些外部文件的版本号,这个操作难免会有遗漏,就算遗漏问题好解决,如果我们的页面是使用服务端模板开发的,那么就可能导致生产发布时候重启生产服务器,为了静态资源发布重启服务器的确让人感觉有点得不偿失。那么我们又如何来解决这个问题呢?
我们分析下这个问题的本质就是页面引用外部css文件和javascript文件的行为其实包含一个动态性,那么我们要解决这个问题就是要拆分出这个动态性,也就是把要变化的版本号这个动态性拆分出来进行单独处理,一般我们就会通过模板语言来重新编写link和script标签的代码,例如在jsp技术里我们可以自定义一个标签,将版本号作为参数传入标签里,当项目发布时候,模板引擎会根据版本参数不同重新编译出link和script标签,但是这个做法还是有问题的,例如jsp页面技术,要想更改版本号就得重启服务,所以这个时候我们把版本号的计算功能做到独立的缓存里,当文件改变后我们通过更改配置刷新缓存,这样就可以不用重启服务器就能刷新静态资源的版本号了。如果我们网站使用了网站静态化处理,那么我们可以把这个操作迁移到反向代理这边来做,把该操作作为动静整合的一部分,如果我们使用了ESI技术,那么无非就是刷新下ESI对应的缓存即可。这个动态刷新静态资源版本号的操作在互联网里已经很流行了,但是现在大部分技术都是关注在如何检测静态文件是否发生变化上,例如使用md5技术计算文件的md5值啊,或者是修改下文件的名字啊,但是这些手段使用时候都没考虑到是否重启服务器的问题,最终导致设计方案使用起来比较麻烦,我觉得如何检测文件是否变化很重要,如果方案能实现在检测变化的基础上做到不用重启服务器就能刷新缓存,这样才能让该方案更加完整和实用。
OK了,终于把网站静态系列写完了,后面我将开启一个新的系列那就是分布式和SOA,本来我想把分布式和SOA分成两个系列,最近觉得这两个系列合并在一起比较好,不过写新系列前可能需要一段时间准备,最近一直写博客都没抽出时间好好看书,应该要花点时间看书好好学习下了。
今天周五了,我是歌手马上要开始,要准备看电视了,最后还是按照惯例祝大家晚安,生活愉快啦。