flickr对javascript干的好事
在一个讨论web技术的网站vitamin上发现这篇《Serving JavaScript Fast》,读过之后大有收获,茅塞顿开。于是就有了翻译过来的念头——我这人有个毛病,看到有意思的英文文章,就想自己翻过来(虽然英文水平很烂)。先在网上查了查,已经有blog谈到这篇文章(我算是后知后觉了),有总结要点的《Flickr 的开发者的 Web 应用优化技巧》,也有延伸开来的《接着讲Flickr的八卦》,但似乎没有全文翻译的(这下就好,不会忙了半天发现是无用功)。之后,就写信问作者可不可以,作者一口答应:“sure – i’d love you to translate it”,只是要求我翻好之后给他一个链接地址。得到准许,心里就有底了。
先介绍一下作者。Cal Henderson,伦敦人,现居加利福尼亚的旧金山。PHP,MySQL和Perl专家,现任flickr架构师(flickr被收购后就在yahoo了),同时也是vitamin的特聘顾问(写些技术性文章)。
既然他是架构师,flickr用的应该就是文中谈到的这些技术,于是参照文章,再对比网站,种种迹象表明确实如此。虽然在中国访问flickr速度不敢恭维,加速效果不得而知,但其用了n多css和javascript资源却似乎从没出过什么问题,也从侧面印证了这些技术的有效性。
仔细的看完文章,还有个强烈的感觉:这老兄也太能卖关子了,一句话非分成三句说,摆事实讲道理是够透彻,就是有点太@#$%了…… 算了,他怎么说我怎么翻吧,忠实于原著嘛,要不就成篡改了。经过几天努力,加上同事thincat兄倾力援手(小弟不胜感激啊),终于完工(@_@ 真是苦力活啊,我再也不想干了~)。
全文翻译如下:
让javascript跑得更快
作者:Cal Henderson
下一代web应用让javascript和css得堪大用。我们会告诉你怎样使这些应用又快又灵。
建立了号称“Web 2.0”的应用,也实现了富内容(rich content)和交互,我们期待着css和javascript扮演更加重要的角色。为使应用干净利落,我们需要完善那些渲染页面的文件,优化其大小和形态,以确保提供最好的用户体验——在实践中,这就意味着一种结合:使内容尽可能小、下载尽可能快,同时避免对未改动资源不必要的重新获取。
由于css和js文件的形态,情况有点复杂。跟图片相比,其源代码很有可能频繁改动。而一旦改动,就需要客户端重新下载,使本地缓存无效(保存在其他缓存里的版本也是如此)。在这篇文章里,我们将着重探讨怎样使用户体验最快:包括初始页面的下载,随后页面的下载,以及随着应用渐进、内容变化而进行的资源下载。
我始终坚信这一点:对开发者来说,应该尽可能让事情变得简单。所以我们青睐于那些能让系统自动处理优化难题的方法。只需少许工作量,我们就能建立一举多得的环境:它使开发变得简单,有极佳的终端性能,也不会改变现有的工作方式。
好大一沱
老的思路是,为优化性能,可以把多个css和js文件合并成极少数大文件。跟十个5k的js文件相比,合并成一个50k的文件更好。虽然代码总字节数没变,却避免了多个HTTP请求造成的开销。每个请求都会在客户端和服务器两边有个建立和消除的过程,导致请求和响应header带来开销,还有服务器端更多的进程和线程资源消耗(可能还有为压缩内容耗费的cpu时间)。
(除了HTTP请求,)并发问题也很重要。默认情况下,在使用持久连接(persistent connections)时,ie和firefox在同一域名内只会同时下载两个资源(在HTTP 1.1规格书中第8.1.4节的建议)(htmlor注:可以通过修改注册表等方法改变这一默认配置)。这就意味着,在我们等待下载2个js文件的同时,将无法下载图片资源。也就是说,这段时间内用户在页面上看不到图片。
(虽然合并文件能解决以上两个问题,)可是,这个方法有两个缺点。第一,把所有资源一起打包,将强制用户一次下载完所有资源。如果(不这么做,而是)把大块内容变成多个文件,下载开销就分散到了多个页面,同时缓解了会话中的速度压力(或完全避免了某些开销,这取决于用户选择的路径)。如果为了随后页面下载得更快而让初始页面下载得很慢,我们将发现更多用户根本不会傻等着再去打开下一个页面。
第二(这个影响更大,一直以来却没怎么被考虑过),在一个文件改动很频繁的环境里,如果采用单文件系统,那么每次改动文件都需要客户端把所有css和js重新下载一遍。假如我们的应用有个100k的合成的js大文件,任何微小的改动都将强制客户端把这100k再消化一遍。
分解之道
(看来合并成大文件不太合适。)替代方案是个折中的办法:把css和js资源分散成多个子文件,按功能划分、保持文件个数尽可能少。这个方案也是有代价的,虽说开发时代码分散成逻辑块(logical chunks)能提高效率,可在下载时为提高性能还得合并文件。不过,只要给build系统(把开发代码变成产品代码的工具集,是为部署准备的)加点东西,就没什么问题了。
对于有着不同开发和产品环境的应用来说,用些简单的技术可以让代码更好管理。在开发环境下,为使条理清晰,代码可以分散为多个逻辑部分(logical components)。可以在Smarty(一种php模板语言)里建立一个简单的函数来管理javascript的下载:
SMARTY:
{insert_js files="foo.js,bar.js,baz.js"}
PHP:
function smarty_insert_js($args){
foreach (explode(',', $args['files']) as $file){
echo "<script type=\"text/javascript\" SOURCE=\"/javascript/$file\"></script>\\n";
}
}
OUTPUT:
<script type="text/javascript" SOURCE="/javascript/foo.js"></script>
<script type="text/javascript" SOURCE="/javascript/bar.js"></script>
<script type="text/javascript" SOURCE="/javascript/baz.js"></script>
(htmlor注:wordpress中会把“src”替换成不知所谓的字符,因此这里只有写成“SOURCE”,使用代码时请注意替换,下同)
就这么简单。然后我们就命令build过程(build process)去把确定的文件合并起来。这个例子里,合并的是foo.js和bar.js,因为它们几乎总是一起下载。我们能让应用配置记住这一点,并修改模板函数去使用它。(代码如下:)
SMARTY:
{insert_js files="foo.js,bar.js,baz.js"}
PHP:
# 源文件映射图。在build过程合并文件之后用这个图找到js的源文件。
$GLOBALS['config']['js_source_map'] = array(
'foo.js' => 'foobar.js',
'bar.js' => 'foobar.js',
'baz.js' => 'baz.js',
);
function smarty_insert_js($args){
if ($GLOBALS['config']['is_dev_site']){
$files = explode(',', $args['files']);
}else{
$files = array();
foreach (explode(',', $args['files']) as $file){
$files[$GLOBALS['config']['js_source_map'][$file]]++;
}
$files = array_keys($files);
}
foreach ($files as $file){
echo "<script type=\"text/javascript\" SOURCE=\"/javascript/$file\"></script>\\n";
}
}
OUTPUT:
<script type="text/javascript" SOURCE="/javascript/foobar.js"></script>
<script type="text/javascript" SOURCE="/javascript/baz.js"></script>
模板里的源代码没必要为了分别适应开发和产品阶段而改动,它帮助我们在开发时保持文件分散,发布成产品时把文件合并。想更进一步的话,可以把合并过程(merge process)写在php里,然后使用同一个(合并文件的)配置去执行。这样就只有一个配置文件,避免了同步问题。为了做的更加完美,我们还可以分析css和js文件在页面中同时出现的几率,以此决定合并哪些文件最合理(几乎总是同时出现的文件是合并的首选)。
对css来说,可以先建立一个主从关系的模型,它很有用。一个主样式表控制应用的所有样式表,多个子样式表控制不同的应用区域。采用这个方法,大多数页面只需下载两个css文件,而其中一个(指主样式表)在页面第一次请求时就会缓存。
对没有太多css和js资源的应用来说,这个方法在第一次请求时可能比单个大文件慢,但如果保持文件数量很少的话,你会发现其实它更快,因为每个页面的数据量更小。让人头疼的下载花销被分散到不同的应用区域,因此并发下载数保持在一个最小值,同时也使得页面的平均下载数据量很小。
压缩
谈到资源压缩,大多数人马上会想到mod_gzip(但要当心,mod_gzip实际上是个魔鬼,至少能让人做恶梦)。它的原理很简单:浏览器请求资源时,会发送一个header表明自己能接受的内容编码。就像这样:
Accept-Encoding: gzip,deflate
服务器遇到这样的header请求时,就用gzip或deflate压缩内容发往客户端,然后客户端解压缩。这过程减少了数据传输量,同时消耗了客户端和服务器的cpu时间。也算差强人意。但是,mod_gzip的工作方式是这样的:先在磁盘上创建一个临时文件,然后发送(给客户端),最后删除这个文件。在高容量的系统中,由于磁盘io问题,很快就会达到极限。要避免这种情况,可以改用mod_deflate(apache 2才支持)。它采用更合理的方式:在内存里做压缩。对于apache 1的用户来说,可以建立一块ram磁盘,让mod_gzip在它上面写临时文件。虽然没有纯内存方式快,但也不会比往磁盘上写文件慢。
话虽如此,其实还是有办法完全避免压缩开销的,那就是预压缩相关静态资源,下载时由mod_gzip提供合适的压缩版本。如果把压缩添加在build过程,它就很透明了。需要压缩的文件通常很少(用不着压缩图片,因为并不能减小更多体积),只有css和js文件(和其他未压缩的静态内容)。
配置选项会告诉mod_gzip去哪里找到预压缩过的文件。
mod_gzip_can_negotiate Yes mod_gzip_static_suffix .gz AddEncoding gzip .gz
新一点的mod_gzip版本(从1.3.26.1a开始)添加一个额外的配置选项后,就能自动预压缩文件。不过在此之前,必须确认apache有正确的权限去创建和覆盖压缩文件。
mod_gzip_update_static Yes
可惜,事情没那么简单。某些Netscape 4的版本(尤其是4.06-4.08)认为自己能够解释压缩内容(它们发送一个header这么说来着),但其实它们不能正确的解压缩。大多数其他版本的Netscape 4在下载压缩内容时也有各种各样的问题。所以要在服务器端探测代理类型,(如果是Netscape 4,就要)让它们得到未压缩的版本。这还算简单的。ie(版本4-6)有些更有意思的问题:当下载压缩的javascript时,有时候ie会不正确的解压缩文件,或者解压缩到一半中断,然后把这半个文件显示在客户端。如果你的应用对javascript的依赖比较大(htmlor注:比如ajax应用),那么就得避免发送压缩文件给ie。在某些情况下,一些更老的5.x版本的ie倒是能正确的收到压缩的javascript,可它们会忽略这个文件的etag header,不缓存它。(thincat友情提示:尽管压缩存在一些浏览器不兼容的现象,由于这些不能很好的支持压缩的浏览器数量现在已经非常少了,我认为这种由于浏览器导致的压缩不正常的情况可以忽略不计。这些过时的浏览器还能不能在现在流行的windows或unix环境下面安装都存在不小的问题)
既然gzip压缩有这么多问题,我们不妨把注意力转到另一边:不改变文件格式的压缩。现在有很多这样的javascript压缩脚本可用,大多数都用一个正则表达式驱动的语句集来减小源代码的体积。它们做的不外乎几件事:去掉注释,压缩空格,缩短私有变量名和去掉可省略的语法。
不幸的是,大多数脚本效果并不理想,要么压缩率相当低,要么某种情形下会把代码搞得一团糟(或者两者兼而有之)。由于对解析树的理解不完整,压缩器很难区分一句注释和一句看似注释的引用字符串。因为闭合结构的混合使用,要用正则表达式发现哪些变量是私有的并不容易,因此一些缩短变量名的技术会打乱某些闭合代码。
还好有个压缩器能避免这些问题:dojo压缩器(现成的版本在这里)。它使用rhino(mozilla的javascript引擎,是用java实现的)建立一个解析树,然后将其提交给文件。它能很好的减小代码体积,仅用很小的成本:因为只在build时压缩一次。由于压缩是在build过程中实现的,所以一清二楚。(既然压缩没有问题了,)我们可以在源代码里随心所欲的添加空格和注释,而不必担心影响到产品代码。
与javascript相比,css文件的压缩相对简单一些。由于css语法里不会有太多引用字符串(通常是url路径跟字体名),我们可以用正则表达式大刀阔斧的干掉空格(htmlor注:这句翻的最爽,哈哈)。如果确实有引用字符串的话,我们总可以把一串空格合成一个(因为不需要在url路径和字体名里查找多个空格和tab)。这样的话,一个简单的perl脚本就够了:
#!/usr/bin/perl
my $data = '';
open F, $ARGV[0] or die "Can't open source file: $!";
$data .= $_ while <F>;
close F;
$data =~ s!\/\*(.*?)\*\/!!g; # 去掉注释
$data =~ s!\s+! !g; # 压缩空格
$data =~ s!\} !}\\n!g; # 在结束大括号后添加换行
$data =~ s!\\n$!!; # 删除最后一个换行
$data =~ s! \{ ! {!g; # 去除开始大括号后的空格
$data =~ s!; \}!}!g; # 去除结束大括号前的空格
print $data;
然后,就可以把单个的css文件传给脚本去压缩了。命令如下:
perl compress.pl site.source.css > site.compress.css
做完这些简单的纯文本优化工作后,我们就能减少数据传输量多达50%了(这个量取决于你的代码格式,可能更多)。这带来了更快的用户体验。不过我们真正想做的是,尽可能避免用户请求的发生——除非确实有必要。这下HTTP缓存知识派上用场了。
缓存是好东西
当用户代理(如浏览器)向服务器请求一个资源时,第一次请求过后它就会缓存服务器的响应,以避免重复之后的相同请求。缓存时间的长短取决于两个因素:代理的配置和服务器的缓存控制header。所有浏览器都有不同的配置选项和处理方式,但大多数都会把一个资源至少缓存到会话结束(除非被明确告知)。
为了不让浏览器缓存改动频繁的页面,你很可能已经发送过header不缓存动态内容。在php中,以下两行命令可以做到:
<?php
header("Cache-Control: private");
header("Cache-Control: no-cache", false);
?>
听起来太简单了?确实如此——因为有些代理(浏览器)在某些环境下将忽略这些header。要确保浏览器不缓存文档,应该更强硬一些:
<?php
# 让它在过去就“失效”
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
# 永远是改动过的
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
# HTTP/1.1
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
# HTTP/1.0
header("Pragma: no-cache");
?>
这样,对于我们不想缓存的内容来说已经行了。但对于那些不会每次请求时都有改动的内容,应该鼓励浏览器更霸道的缓存它。“If-Modified-Since”请求header能够做到这点。如果客户端在请求中发送一个“If-Modified-Since”header,apache(或其他服务器)会以状态代码304(没改过)响应,告诉浏览器缓存已经是最新的。使用这个机制,能够避免重复发送文件给浏览器,不过仍然导致了一个HTTP请求的消耗。嗯,再想想。
与If-Modified-Since机制类似的是实体标记(entity tags)。在apache环境下,每个对静态文件的响应都会发出一个“ETag”header,它包含了一个由文件修改时间、文件大小和inode号生成的校验和(checksum)。在下载文件之前,浏览器会发送一个HEAD请求去检查文件的etag。可ETag跟If-Modified-Since有同样的问题:客户端仍旧需要执行HTTP请求来验证本地缓存是否有效。
此外,如果你使用多台服务器提供内容,得小心使用if-modified-since和etags。在两台负载平衡的服务器环境下,对一个代理(浏览器)来说,一个资源可以这次从A服务器得到,下次从B服务器得到(htmlor注:lvs负载平衡系统就是个典型的例子)。这很好,也是采用平衡负载的原因。可是,如果两台服务器给同一个文件生成了不同的etag或者文件修改日期,浏览器就无所适从了(每次都会重新下载)。默认情况下,etag是由文件的inode号生成的,而多台服务器之间文件的inode号是不同的。可以使用apache的配置选项关掉它:
FileETag MTime Size
使用这个选项,apache将只用文件修改日期和文件大小来决定etag。很不幸,这导致了另一个问题(一样能影响if-modified-since)。既然etag依赖于修改时间,就得让时间同步。可往多台服务器上传文件时,上传时间差个一到两秒是常有的事。这样一来,两台服务器生成的etag还是不一样。当然,我们还可以改变配置,让etag的生成只取决于文件大小,但这就意味着如果文件内容变了而大小没变,etag也不会变。这可不行。
缓存真是个好东西
看来我们正从错误的方向入手解决问题。(现在的问题是,)这些可能的缓存策略导致了一件事情反复发生,那就是:客户端向服务器查询本地缓存是否最新。假如服务器在改动文件的时候通知客户端,客户端不就知道它的缓存是最新的了(直到接到下一次通知)?可惜天公不做美——(事实)是客户端向服务器发出请求。
其实,也不尽然。在获取js或css文件之前,客户端会用<script>或<link>标记向服务器发送一个请求,说明哪个页面要加载这些文件。这时候就可以用服务器的响应来通知客户端这些文件有了改动。有点含糊,说得再详细点就是:如果改变css和js文件内容的同时,也改变它们的文件名,就可以告诉客户端对url全都永久缓存——因为每个url都是唯一的。
假如能确定一个资源永不更改,我们就可以发出一些霸气十足的缓存header(htmlor注:这句也很有气势吧)。在php里,两行就好:
<?php
header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");
header("Cache-Control: max-age=315360000");
?>
我们告诉浏览器这个内容在10年后(10年大概会有315,360,000秒,或多或少)过期,浏览器将会保留它10年。当然,很有可能不用php输出css和js文件(因此就不能发出header),这种情况将在稍后说明。
人力有时而穷
当文件内容更改时,手动去改文件名是很危险的。假如你改了文件名,模板却没有指向它?假如你改了一些模板另一些却没改?假如你改了模板却没改文件名?还有最糟的,假如你改动了文件却忘了改名或者忘了改变对它的引用?最好的结果,是用户看到老的而看不到新的内容。最坏的结果,是找不到文件,网站没法运转了。听起来这(指改动文件内容时修改url)似乎是个馊主意。
幸运的是,计算机做这类事情——当某种变化发生,需要相当准确地完成的、重复重复再重复的(htmlor注:番茄鸡蛋伺候~)、枯燥乏味的工作——总是十分在行。
这个过程(改变文件的url)没那么痛苦,因为我们根本不需要改文件名。资源的url和磁盘上文件的位置也没必要保持一致。使用apache的mod_rewrite模块,可以建立简单的规则,让确定的url重定向到确定的文件。
RewriteEngine on RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L]
这条规则匹配任何带有指定扩展名同时含有“版本”信息(version nugget)的url,它会把这些url重定向到一个不含版本信息的路径。如下所示:
URL Path /images/foo.v2.gif -> /images/foo.gif /css/main.v1.27.css -> /css/main.css /javascript/md5.v6.js -> /javascript/md5.js
使用这条规则,就可以做到不改变文件路径而更改url(因为版本号变了)。由于url变了,浏览器就认为它是另一个资源(会重新下载)。想更进一步的话,可以把我们之前说的脚本编组函数结合起来,根据需要生成一个带有版本号的<script>标记列表。
说到这里,你可能会问我,为什么不在url结尾加一个查询字符串(query string)呢(如/css/main.css?v=4)?根据HTTP缓存规格书所说,用户代理对含有查询字符串的url永不缓存。虽然ie跟firefox忽略了这点,opera和safari却没有——为了确保所有浏览器都缓存你的资源,还是不要在url里用查询字符串的好。
现在不移动文件就能更改url了,如果能让url自动更新就更好了。在小型的产品环境下(如果有大型的产品环境,就是开发环境了),使用模板功能可以很轻易的实现这点。这里用的是smarty,用其他模板引擎也行。
SMARTY:
<link xhref="{version xsrc='/css/group.css'}" rel="stylesheet" type="text/css" />
PHP:
function smarty_version($args){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];
echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']);
}
OUTPUT:
<link xhref="/css/group.v1234567890.css" mce_href="/css/group.v1234567890.css" rel="stylesheet" type="text/css" />
对每个链接到的资源文件,我们得到它在磁盘上的路径,检查它的mtime(文件最后修改的日期和时间),然后把这个时间当作版本号插入到url中。对于低流量的站点(它们的stat操作开销不大)或者开发环境来说,这个方案不错,但对于高容量的环境就不适用了——因为每次stat操作都要磁盘读取(导致服务器负载升高)。
解决方案相当简单。在大型系统中每个资源都已经有了一个版本号,就是版本控制的修订号(你们应该使用了版本控制,对吧?)。当我们建立站点准备部署的时候,可以轻易的查到每个文件的修订号,写在一个静态配置文件里。
<?php $GLOBALS['config']['resource_versions'] = array( '/images/foo.gif' => '2.1', '/css/main.css' => '1.27', '/javascript/md5.js' => '6.1.4', ); ?>
当我们发布产品时,可以修改模板函数来使用版本号。
<?php
function smarty_version($args){
if ($GLOBALS['config']['is_dev_site']){
$stat = stat($GLOBALS['config']['site_root'].$args['src']);
$version = $stat['mtime'];
}else{
$version = $GLOBALS['config']['resource_versions'][$args['src']];
}
echo preg_replace('!\.([a-z]+?)$!', ".v$version.\$1", $args['src']);
}
?>
就这样,不需要改文件名,也不需要记住改了哪些文件——当文件有新版本发布时它的url就会自动更新——有意思吧?我们就快搞定了。
只欠东风
之前谈到为静态文件发送超长周期(very-long-period)的缓存header时曾说过,如果不用php输出,就不能轻易的发送缓存header。很显然,有两个办法可以解决:用php输出,或者让apache来做。
php出马,手到擒来。我们要做的仅仅是改变rewrite规则,把静态文件指向php脚本,用php在输出文件内容之前发送header。
Apache:
RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /redir.php?path=$1$2 [L]
PHP:
header("Expires: ".gmdate("D, d M Y H:i:s", time()+315360000)." GMT");
header("Cache-Control: max-age=315360000");
# 忽略带有“..”的路径
if (preg_match('!\.\.!', $_GET[path])){ go_404(); }
# 保证路径开头是确定的目录
if (!preg_match('!^(javascript|css|images)!', $_GET[path])){ go_404(); }
# 文件不存在?
if (!file_exists($_GET[path])){ go_404(); }
# 发出一个文件类型header
$ext = array_pop(explode('.', $_GET[path]));
switch ($ext){
case 'css':
header("Content-type: text/css");
break;
case 'js' :
header("Content-type: text/javascript");
break;
case 'gif':
header("Content-type: image/gif");
break;
case 'jpg':
header("Content-type: image/jpeg");
break;
case 'png':
header("Content-type: image/png");
break;
default:
header("Content-type: text/plain");
}
# 输出文件内容
echo implode('', file($_GET[path]));
function go_404(){
header("HTTP/1.0 404 File not found");
exit;
}
这个方案有效,但并不出色。(因为)跟apache相比,php需要更多内存和执行时间。另外,我们还得小心防止可能由path参数传递伪造值引起的exploits。为避免这些问题,应该用apache直接发送header。rewrite规则语句允许当规则匹配时设置环境变量(environment variable),当给定的环境变量设置后,Header命令就可以添加header。结合以下两条语句,我们就把rewrite规则和header设置绑定在了一起:
RewriteEngine on RewriteRule ^/(.*\.)v[0-9.]+\.(css|js|gif|png|jpg)$ /$1$2 [L,E=VERSIONED_FILE:1] Header add "Expires" "Mon, 28 Jul 2014 23:30:00 GMT" env=VERSIONED_FILE Header add "Cache-Control" "max-age=315360000" env=VERSIONED_FILE
考虑到apache的执行顺序,应该把rewrite规则加在主配置文件(httpd.conf)而不是目录配置文件(.htaccess)中。否则在环境变量设置之前,header行会先执行(就那没意义了)。至于header行,则可以放在两文件任何一个当中,没什么区别。
眼观六路
(htmlor注:多谢tchaikov告知“skinning rabbits”的含义,但我不想翻的太正式,眼下的这个应该不算太离谱吧。)
通过结合使用以上技术,我们可以建立一个灵活的开发环境和一个快速又高性能的产品环境。当然,这离终极目标“速度”还有一段距离。有许多更深层的技术(比如分离伺服静态内容,用多域名提升并发量等)值得我们关注,包括与我们谈到的方法(建立apache过滤器,修改资源url,加上版本信息)殊途同归的其他路子。你可以留下评论,告诉我们那些你正在使用的卓有成效的技术和方法。
(完)
CHARLOTTExR26 留言,
2010年01月20日 星期三 @ 15:27:30
Some custom made essays connecting with would be written for people which want to have knowledge superb facts close to to our costs.
MeganFv19 留言,
2010年01月30日 星期六 @ 03:30:39
Your knowledge related to this good topic seems to be great! Therefore people not should compose the thesis writing or doctoral thesis by their own efforts, they can use your help.