网站优化之压缩页面输出

本文于2015年底完成,发布在个人博客网站上。 考虑个人博客因某种原因无法修复,于是在博客园安家,之前发布的文章逐步搬迁过来。


一切要从阿里高级专家君山的一次公开演讲有关。

本文基于tomcat 8.0.x版本输出。

君山的演讲

10月17日,有幸参加QCon上海2015全球软件大会。第一次参加这样的技术大会,当然对什么都感到新奇和震撼,但和本文无关,所以不一一细说。在听君山的讲座时,其中一页PPT引起我的极大兴趣。

这页PPT中提到了淘宝的网站系统使用了velocity来做模板引擎,而在使用过程中发现velocity存在诸多问题,主要的问题有:

  • velocity使用解释方式来执行模板,比如涉及相当多的反射调用,效率很低;
  • 生成的页面中,空行、空格非常多,无效信息占比过高;
  • 。。。

君山的本意是举一个例子来介绍阿里在应用系统代码层次优化方面所做的巨大努力,但我对这页PPT中提到的压缩页面输出更感兴趣,因为我年初刚换新项目组,参与生平第二个Web项目(第一个Web项目刚启动就被领导砍掉了)的开发,欠缺Web相关的开发、优化的经验。

淘宝的首页

回家之后,抑制不住好奇心,使用浏览器的调试工具分析淘宝的首页,看看有什么发现。

HTTP响应头部

首先检查浏览器加载首页过程中的通信内容,发现淘宝服务器返回主页时,HTTP头部信息如下:

age:64
cache-control:max-age=0, s-maxage=100
content-encoding:gzip
content-type:text/html; charset=utf-8
date:Mon, 23 Nov 2015 15:01:33 GMT
server:nginx
status:200 OK
timing-allow-origin:*
vary:Ali-Detector-Type
version:HTTP/1.1
via:cache55.l2cm3[0,200-0,H], cache45.l2cm3[0,0], cache165.cn75[0,200-0,H], cache161.cn75[1,0]
x-cache:HIT TCP_MEM_HIT dirn:-2:-2
x-response-time:416
x-swift-cachetime:93
x-swift-savetime:Mon, 23 Nov 2015 15:00:36 GMT

响应信息中包含content-encoding:gzip,可以断定启用了gzip压缩;其它HTTP头部先不去管。

页面内容

使用浏览器打开淘宝首页,右键菜单中选择查看源文件,可以看到如下被引用的网页代码。本来这里配一张图片可能更方便说明问题,但奈何本人比较懒,所以还是直接上文本吧,简单一些。

<!DOCTYPE html><html><head><meta charset="utf-8"><link rel="dns-prefetch" href="//g.tbcdn.cn"><link rel="dns-prefetch" href="//g.alicdn.com"><link rel="dns-prefetch" href="//gw.alicdn.com"><link rel="dns-prefetch" href="//gtms01.alicdn.com"><link rel="dns-prefetch" href="//gtms02.alicdn.com"><link rel="dns-prefetch" href="//gtms03.alicdn.com"><link rel="dns-prefetch" href="//gtms04.alicdn.com"><link rel="dns-prefetch" href="//log.mmstat.com"><link rel="dns-prefetch" href="//p.tanx.com"><link rel="dns-prefetch" href="//i.mmcdn.cn"><link rel="dns-prefetch" href="//delta.taobao.com"><title>淘宝网 - 淘!我喜欢</title><meta name="spm-id" content="a21bo"><meta name="description" content="淘宝网 - 亚洲最大、最安全的网上交易平台,提供各类服饰、美容、家居、数码、话费/点卡充值… 8亿优质特价商品,同时提供担保交易(先收货后付款)、先行赔付、假一赔三、七天无理由退换货、数码免费维修等安全交易保障服务,让你全面安心享受网上购物乐趣!"><meta name="keyword" content=""><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><meta name="renderer" content="webkit"><link href="//gtms03.alicdn.com/tps/i3/T1OjaVFl4dXXa.JOZB-114-114.png" rel="apple-touch-icon-precomposed"><script src="//g.alicdn.com/secdev/pointman/js/index.js" app="taobao" charset="utf-8"></script><link rel="stylesheet" href="//g.alicdn.com/tb-mod/??tb-pad/1.0.1/index.css,tb-sitenav/1.0.3/index.css,tb-sysinfo/1.0.0/index.css,tb-sysbanner/1.0.0/index.css,tb-double12-banner/0.0.10/index.css,tb-banner/1.0.12/index.css,tb-top-spy/1.0.4/index.css,tb-birthday/1.0.2/index.css,tb-search/1.0.29/index.css,tb-logo/1.0.5/index.css,tb-qr/1.0.0/index.css,tb-nav/1.0.7/index.css,tb-tanx/1.0.0/index.css,tb-promo/1.0.7/index.css,tb-tmall/1.0.9/index.css,tb-notice/1.0.4/index.css,tb-member/1.0.7/index.css,tb-headlines/1.0.3/index.css,tb-conve/1.0.16/index.css,tb-double12-service/0.0.5/index.css,tb-double12-belt/0.0.3/index.css,tb-belt/1.0.5/index.css,tb-belt-slide/1.0.10/index.css,tb-apps/1.0.8/index.css,tb-feature/1.0.4/index.css,tb-discover-goods/1.0.3/index.css,tb-footprint/1.0.9/index.css,tb-discover-shop/1.0.3/index.css,tb-custom/1.0.0/index.css,tb-sale/1.0.0/index.css,tb-helper/1.0.0/index.css,tb-footer/1.0.0/index.css,tb-decorations/1.0.27/index.css,tb-fixedtool/1.0.0/index.css,tb-inject/0.0.13/index.css,tb-service/1.0.12/index.css,tb-cat/1.0.2/index.css,tb-rmdimg/1.0.0/index.css,tb-market-ifashion/1.0.7/index.css,tb-market/1.0.2/index.css,tb-market2/1.0.3/index.css,tb-market-electronic/1.0.4/index.css,tb-market-diet/1.0.8/index.css,tb-oead/1.0.0/index.css,tb-market-furniture/1.0.5/index.css,tb-market-pannel/1.0.4/index.css,tb-channel/1.0.1/index.css,tb-channel-travel/1.0.1/index.css,tb-guang/1.0.1/index.css,tb-channel2/1.0.3/index.css"><link rel="stylesheet" href="//g.alicdn.com/tb-mod/??tb-channel-edu/1.0.0/index.css"><link rel="stylesheet" href="//g.alicdn.com/??tb/global/3.5.28/global-min.css,tb-page/tbindex/1.2.21/index-min.css"><script src="//g.alicdn.com/??kissy/k/1.4.14/seed-min.js,tb/global/3.5.28/global-min.js" data-config="{combine:true}"></script><script>window.g_config={appId:6,startDate:new Date},KISSY.config({combine:!0,packages:[{name:"kg",path:"//g.alicdn.com/kg/",ignorePackageNameInUri:!0,combine:!0},{name:"tb-mod",path:"//g.alicdn.com/",charset:"utf-8",combine:!0},{name:"tb-page",path:"//g.alicdn.com/",charset:"utf-8",combine:!0}],modules:function(){var a,e,t,c,n={};for(t="1.3.24",c=["ctrl","msg","getHelper","xctrl"],a=0,e=c.length;e>a;a++)n["market/"+c[a]]={alias:["tbc/market/"+t+"/"+c[a]]};for(t="1.0.46",c=["reporter","utils","tanx","oead","rmdimg","rmdlink","market","ald"],a=0,e=c.length;e>a;a++)n["cowboy/"+c[a]]={alias:["tbc/cowboy/"+t+"/mods/"+c[a]]};return n}()});</script><base target="_blank"></head><body data-spm="7724922"><script>
with(document)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("exparams","category=&userid=&aplus&yunid=&&asid=AQAAAABNKlNWsareQgAAAABVzIUMI1lGLw==",id="tb-beacon-aplus",src=(location>"https"?"//g":"//g")+".alicdn.com/alilog/mlog/aplus_v2.js")

其实这部分上述引用的只是部分页面,仅仅是开头的三行。可以发现有如下特点:

  • 没有多余的空格和换行。

  • 页面引用的jscss文件由cdn提供,例如如下代码:

    • <script src="//g.alicdn.com/secdev/pointman/js/index.js" app="taobao" charset="utf-8"></script>
    • <link rel="stylesheet" href="//g.alicdn.com/tb-mod/??tb-pad/1.0.1/index.css,tb-sitenav/1.0.3/index.css,tb-sysinfo/1.0.0/index.css,tb-sysbanner/1.0.0/index.css,tb-double12-banner/0.0.10/index.css,tb-banner/1.0.12/index.css,tb-top-spy/1.0.4/index.css,tb-birthday/1.0.2/index.css,tb-search/1.0.29/index.css,tb-logo/1.0.5/index.css,tb-qr/1.0.0/index.css,tb-nav/1.0.7/index.css,tb-tanx/1.0.0/index.css,tb-promo/1.0.7/index.css,tb-tmall/1.0.9/index.css,tb-notice/1.0.4/index.css,tb-member/1.0.7/index.css,tb-headlines/1.0.3/index.css,tb-conve/1.0.16/index.css,tb-double12-service/0.0.5/index.css,tb-double12-belt/0.0.3/index.css,tb-belt/1.0.5/index.css,tb-belt-slide/1.0.10/index.css,tb-apps/1.0.8/index.css,tb-feature/1.0.4/index.css,tb-discover-goods/1.0.3/index.css,tb-footprint/1.0.9/index.css,tb-discover-shop/1.0.3/index.css,tb-custom/1.0.0/index.css,tb-sale/1.0.0/index.css,tb-helper/1.0.0/index.css,tb-footer/1.0.0/index.css,tb-decorations/1.0.27/index.css,tb-fixedtool/1.0.0/index.css,tb-inject/0.0.13/index.css,tb-service/1.0.12/index.css,tb-cat/1.0.2/index.css,tb-rmdimg/1.0.0/index.css,tb-market-ifashion/1.0.7/index.css,tb-market/1.0.2/index.css,tb-market2/1.0.3/index.css,tb-market-electronic/1.0.4/index.css,tb-market-diet/1.0.8/index.css,tb-oead/1.0.0/index.css,tb-market-furniture/1.0.5/index.css,tb-market-pannel/1.0.4/index.css,tb-channel/1.0.1/index.css,tb-channel-travel/1.0.1/index.css,tb-guang/1.0.1/index.css,tb-channel2/1.0.3/index.css">
  • 页面上出现的js代码执行过压缩和混淆,例如如下代码:

      <script>window.g_config={appId:6,startDate:new Date},KISSY.config({combine:!0,packages:[{name:"kg",path:"//g.alicdn.com/kg/",ignorePackageNameInUri:!0,combine:!0},{name:"tb-mod",path:"//g.alicdn.com/",charset:"utf-8",combine:!0},{name:"tb-page",path:"//g.alicdn.com/",charset:"utf-8",combine:!0}],modules:function(){var a,e,t,c,n={};for(t="1.3.24",c=["ctrl","msg","getHelper","xctrl"],a=0,e=c.length;e>a;a++)n["market/"+c[a]]={alias:["tbc/market/"+t+"/"+c[a]]};for(t="1.0.46",c=["reporter","utils","tanx","oead","rmdimg","rmdlink","market","ald"],a=0,e=c.length;e>a;a++)n["cowboy/"+c[a]]={alias:["tbc/cowboy/"+t+"/mods/"+c[a]]};return n}()});</script><base target="_blank"></head><body data-spm="7724922"><script>
      with(document)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("exparams","category=&userid=&aplus&yunid=&&asid=AQAAAABNKlNWsareQgAAAABVzIUMI1lGLw==",id="tb-beacon-aplus",src=(location>"https"?"//g":"//g")+".alicdn.com/alilog/mlog/aplus_v2.js")
      </script>	
    

直白的讲,这样的网页源码可读性很差。但对于浏览器而言,则毫无压力,但为了省事,这里也不配图,有兴趣的同学可以自行分析。

压缩输出的意义

从理论和实践效果来看,压缩输出对于服务器侧和用户侧都会带来收益。

服务器侧的收益

  1. 降低对服务器出口带宽的占用,提升带宽的利用率,单位带宽可以服务更多的用户请求。
  2. 降低tomcat处理文本时引入的开销,比如内存和CPU的使用率,提升服务器的利用率。

用户侧的收益

  1. 减少网络传输时延对用户体验的影响。
  2. 降低浏览器加载页面内容时占用的内存,有利于改善浏览器的稳定性。

公司内的研讨

参加了QCon大会,回了公司自然要向周边的同事分享一下。在我的分享材料中,有一项即是对淘宝页面压缩输出的介绍。分享会上气氛比较热烈,大家聊了很多。

网站系统面对的问题大概类似,不过平心而论,这些问题的优先级、紧迫程度却有很大不同。

比如对于公司内的多数IT系统而言:

  • 运行环境是公司内部局域网络,网络稳定,带宽足够。
  • 与外部网络隔离,基本不存在网络攻击。
  • 使用用户群体固定、单一,访问量一般,对系统的压力一般。

那么,压缩web页面的输出,对于公司的IT系统说,无论重要性、紧迫性都一般。

有一个项目组的问题比较典型,他们使用了公司平台部门开发的Web平台,页面上引入了相当多的js文件。经过排查和分析,发现页面渲染时间对用户体验很大,但问题是,他们也不知道这些js文件哪些有用、哪些没用,所以无从改进。

压缩页面输出的方法

出于一些技术的、非技术的原因,我们项目在启动时没有使用公司内的Web平台,而使用了市面最流行的tomcat+SSI作为技术框架。那么问题来了,对于我们这样的技术组合,是否可以像阿里的应用系统一样、输出高度压缩的页面?

经过一段时间的探索,找到如下方法:

  • 调整tomcat的参数。
  • 改进tomcat里jsp编译器的实现。
  • 使用Ant的replaceregexp。
  • 开启tomcat的gzip特性。

调整tomcat的参数

刚开始想到的策略就是检查tomcat的配置文件,在网上搜索相关资料,确认tomcat当前是否具备类似的能力。根据热心网友提供的信息,基本上有如下方法:

  • 启用jsp编译器的trimSpaces选项。

  • 页面上增加如下jsp指令。

      <%@ page trimDirectiveWhitespaces="true" %>
    
  • 在应用的web.xml中增加如下的配置。

      <jsp-config>
      	<jsp-property-group>
      		<url-pattern>*.jsp</url-pattern>
      		<trim-directive-whitespaces>true</trim-directive-whitespaces>
      	</jsp-property-group>
      </jsp-config>
    

不过调整上述参数后,使用浏览器来分析页面,发现效果不佳,标签之间多余的空格似乎是消失了,但页面上js代码块内和css样式定义中的空格还是很多;此外,空行可是一个都没少。

改进jsp编译器的实现

这是想到的第二招,看是否可以改进tomcat源码中jsp编译器的相关实现,思路是在从jsp生成servlet时,删除jsp文件中多余的空格和换行。

本来以为会比较复杂,还专门预留了点时间,结果发现tomcat的源码非常清晰,直接在代码中搜索trimSpaces选项出现的地方,直接找到了处理jsp页面代码的位置。接下来的事情就简单了,修改页面处理代码,将多余的空格和换行替换为空格。

具体修改点为,在类org.apache.jasper.compiler.TextOptimizer中,修改内部类TextCatVisitorvoid visit(Node.TemplateText n)方法的实现,增加如下代码

private static final Pattern pNonChars = Pattern.compile("\\s{2,}");
@Override
public void visit(Node.TemplateText n) throws JasperException {
	
	if (options.getTrimSpaces()) {
    	String text = n.getText();
    	text = text == null ? "" : text.trim();
    	if (!text.isEmpty()) {
        	Matcher matcher = pNonChars.matcher(text);
        	text = matcher.replaceAll("");        		
    	}

    	n.setText(text);        		
	}
	// 其它源码

由于使用到了jsp编译器的trimSpaces选项,因此在使用时需要修改${CATALINA_HOME}/conf/web.xml文件中trimSpaces的值为true;当前也可以另外定义新的配置项,我这里先将就一下,借用原有的选项。

<servlet>
    <servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
        <param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>trimSpaces</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

这个方法效果不错,但相对要麻烦一些,因为要重新编译tomcat;假如tomcat升级版本,需要为新版本的tomcat打补丁,否则会丢失压缩的特性;另外页面上的cssjs代码在行尾一定要有分号;,否则会出现语法错误。

使用Ant的replaceregexp

后来有一天维护项目组的构建脚本时,发现Ant脚本里有如下的Task

<target name="replace_timestamp">
	<replace dir="${build.root}/WebRoot" includes="*.jsp" encoding="UTF-8">
        <replacefilter token="RELEASE_TIMESTMP" value="${current.timestamp}"/>
    </replace>
</target>

灵光一闪,突然想到是否可以使用Ant任务来处理jsp页面,将页面中包含的多余的空格和空行删除掉?

参考了官方的文档和热心网友的资料,使用Ant中名为replaceregexpTask,可以将jsp页面中出现的多个空格和多个换行都替换为单个空格。

样例脚本

<target name="trimSpaces">
    <replaceregexp encoding="UTF-8" flags="g">
		<regexp pattern="\s{2,}" />
		<substitution expression=" " />
		<fileset dir="${build.root}/WebRoot">
			<include name="**/*.jsp" />
		</fileset>
	</replaceregexp>
</target>

注意事项

  • flags的取值,根据官方文档,flags有如下取值。

    • g : Global replacement. Replace all occurrences found
    • i : Case Insensitive. Do not consider case in the match
    • m : Multiline. Treat the string as multiple lines of input, using "^" and "$" as the start or end of any line, respectively, rather than start or end of string.
    • s : Singleline. Treat the string as a single line of input, using "." to match any character, including a newline, which normally, it would not match.

    为了替换jsp页面中出现的全部换行和空格,flags需要取值为g

  • 页面上的cssjs代码在行尾一定要有分号;,否则会出现语法错误。

这个方法很好,非常灵活,不需要修改tomcat的源码和项目中jsp页面的实现,可以做到随时依据项目的需要来调整正则表达式。

开启tomcat的gzip特性

修改${CATALINA_HOME}/conf/server.xmlConnector的配置,配置样例如下。

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           compression="on" --- 增加这一行
           />

这个方法很好,操作简单,容易上手。

参考资料

热门相关:我有一张沾沾卡   我的黑月光女友   都市最强小村医   陆鸣至尊神殿   娶一送一:爹地,放开我妈咪!