介绍
用一个大的Javascript或者CSS文件替代多个小体积的Javascript和CSS文件这是一个很好的实践,可以获得更好的可维护性,但是在网站性能方面会产生一定的影响(这里指的是随着文件体积的增大,随之消耗服务器的内存也会增加)。尽管你应该把Javascript代码单独写成小支的文件,CSS文件拆分成小块,但是当浏览器请求这些文件时,会产生同等数量的http请求。每个http请求都会产生一次从你的浏览器到服务器端网络往返过程,并且导致推迟到达服务器端和返回浏览器端的时间,我们称之为延迟。因此,如果你有4个Javascript和3个css文件在页面中被加载,你浪费掉了7次因网络往返过程产生的时间。在美国,延迟平均是70毫秒,这样你就浪费了7*70 = 490毫秒,大致延迟了半秒的时间。在美国之外的国家访问你的页面,平均延迟大约是200毫秒,这意味着你的页面有1400毫秒的时间是在等待中度过。浏览器在你的CSS和Javascript文件完全加载完成之前是不能很好的渲染你的页面的。因此越多的延迟让你的页面载入越慢。
延迟导致多大的影响
下图显示每个请求的延迟造成页面加载时显著的延误
你可以通过使用CDN加速来减少等待时间。阅读我前一篇文章关于使用CDN. 然而,一个更好的解决方案是使用一个HttpHandler来合并多个文件成一个文件一次性输出。因此,你只要将多个《script》或者《link》标签合并成为一个并将他们指向HttpHandler,指定哪些文件需要作为一次响应传输到浏览器段。这样就减少了请求次数以及消除因其造成的延迟This saves browser from making many requests and eliminates the latency.
通过上图你可以看见通过合并多个JavaScripts和CSS文件为一所带来各方面的提升。
<script type="text/javascript" src="js/b.js"></script>
<script type="text/javascript" src="js/c.js"></script>
<script type="text/javascript" src="js/d.js"></script>
<script type="text/javascript" src="js/e.js"></script>
<script type="text/javascript" src="js/f.js"></script>
你可以用Http Handler通过scripts的设置来实现将多个单独的 《script》标签合并成一个:
HTTP Handler 通过配置文件中设置的名称读取所有文件合并成一次响应传输到客户端,通过gzip压缩响应节省了宽带使用。此外还会生成合适的缓存头来缓存响应的浏览器缓存,因此,浏览器不会再次向服务器发送请求。
在查询字符串中,‘s’指明配置文件中的设置名,‘t’为文件的内容类型,‘v’为版本号。一旦响应被缓存,如果你更改了配置中任何文件,你将不得不增加参数‘v’的值来让浏览器再次下载服务器端最新的响应:
<linktype="text/css"rel="stylesheet"href="HttpCombiner.ashx?s=CommonCss&t=text/css&v=1"></link> |
在web.config中的设置如下:
<appSettings>
<add key="all" value="~/js/a.js,
~/js/b.js,
~/js/c.js,
~/js/d.js,
~/js/e.js"/>
<add key="CommonCss" value="~/App_Themes/Default/Theme.css,
~/Css/Common.css,
~/Controls/Grid/grid.css"/>
</appSettings>
该处理程序如何工作:
1、首先通过传入的参数“s”获得设置名称
2、然后根据设置名称获得web.config中定义的文件名称(通过特定的分隔符分隔开)
3、读取每单个文件然后存储到缓冲区
4、通过gzip压缩缓冲区中的数据
5、发送压缩后缓冲区中的数据到浏览器端
6、已压缩后缓冲区的数据使用ASP.NET缓存模块缓存起来以便在频繁请求同一个设置的情况下直接访问缓存而不必从文件系统或者外部URL读取文件
该处理程序带来的好处:
可以节约因网络延迟造成的时间。如果一次性设置的文件越多,节省的网络延迟性时间越多,同时得到的性能提升就越可观。
因为缓存了所有压缩后的响应数据,这样节省了反复执行从文件系统中读取并压缩的步骤,提升了应用程序的伸缩性。
如何让HttpHandler工作
首先处理程序会从QueryString中获取setName,contentType,version三个关键参数:
如果设置的文件已经被缓存起来,它将直接被写入到缓存当中去,否则它们会从MemoryStream中分别被加载。如果浏览器支持压缩输出的话,MemoryStream会使用GZipStream进行压缩
当合并所有文件之后并压缩,合并的二进制流被缓存起来便与频繁的访问可以直接从缓存中读取。
GetFileBytes方法主要是根据文件路径或者http url读取文件并返回二进制。因此您可以使用你站点的虚拟路径或者使用外部站点Javascript/CSS的url
WriteBytes 方法很巧妙,它会基于二进制流是否为压缩而生成一个合适的header,并设置浏览器缓存头让浏览器缓存服务器端的响应
目前发现部署到运行环境中会出现异常(远程主机关闭了连接。错误代码是 0x80072746。),将上面图片中的代码最后2行替换成
response.Flush(); if (response.IsClientConnected) response.OutputStream.Write(bytes, 0, bytes.Length); response.End(); |
如何使用该文件:
包含HttpCombiner.ashx在你的项目中
在你的web.config的 <appSettings>定义需要设置的文件节点
更改你网站的 <link> 和 <script> 标签指向HttpCombiner.ashx 如下面的格式:
HttpCombiner.ashx?s=<appSettings里设置的节点名>&t=<文件类型>&v=<版本号>
HttpCombiner.ashx
using System;
using System.Net;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Configuration;
using System.Web;
public class HttpCombiner : IHttpHandler {
private const bool DO_GZIP = true;
private readonly static TimeSpan CACHE_DURATION = TimeSpan.FromDays(30);
public void ProcessRequest (HttpContext context) {
HttpRequest request = context.Request;
// Read setName, contentType and version. All are required. They are
// used as cache key
string setName = request["s"] ?? string.Empty;
string contentType = request["t"] ?? string.Empty;
string version = request["v"] ?? string.Empty;
// Decide if browser supports compressed response
bool isCompressed = DO_GZIP && this.CanGZip(context.Request);
// Response is written as UTF8 encoding. If you are using languages like
// Arabic, you should change this to proper encoding
UTF8Encoding encoding = new UTF8Encoding(false);
// If the set has already been cached, write the response directly from
// cache. Otherwise generate the response and cache it
if (!this.WriteFromCache(context, setName, version, isCompressed, contentType))
{
using (MemoryStream memoryStream = new MemoryStream(5000))
{
// Decide regular stream or GZipStream based on whether the response
// can be cached or not
using (Stream writer = isCompressed ?
(Stream)(new GZipStream(memoryStream, CompressionMode.Compress)) :
memoryStream)
{
// Load the files defined in <appSettings> and process each file
string setDefinition =
System.Configuration.ConfigurationManager.AppSettings[setName] ?? "";
string[] fileNames = setDefinition.Split(new char[] { ',' },
StringSplitOptions.RemoveEmptyEntries);
foreach (string fileName in fileNames)
{
byte[] fileBytes = this.GetFileBytes(context, fileName.Trim(), encoding);
writer.Write(fileBytes, 0, fileBytes.Length);
}
writer.Close();
}
// Cache the combined response so that it can be directly written
// in subsequent calls
byte[] responseBytes = memoryStream.ToArray();
context.Cache.Insert(GetCacheKey(setName, version, isCompressed),
responseBytes, null, System.Web.Caching.Cache.NoAbsoluteExpiration,
CACHE_DURATION);
// Generate the response
this.WriteBytes(responseBytes, context, isCompressed, contentType);
}
}
}
private byte[] GetFileBytes(HttpContext context, string virtualPath, Encoding encoding)
{
if (virtualPath.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase))
{
using (WebClient client = new WebClient())
{
return client.DownloadData(virtualPath);
}
}
else
{
string physicalPath = context.Server.MapPath(virtualPath);
byte[] bytes = File.ReadAllBytes(physicalPath);
// TODO: Convert unicode files to specified encoding. For now, assuming
// files are either ASCII or UTF8
return bytes;
}
}
private bool WriteFromCache(HttpContext context, string setName, string version,
bool isCompressed, string contentType)
{
byte[] responseBytes = context.Cache[GetCacheKey(setName, version, isCompressed)] as byte[];
if (null == responseBytes || 0 == responseBytes.Length) return false;
this.WriteBytes(responseBytes, context, isCompressed, contentType);
return true;
}
private void WriteBytes(byte[] bytes, HttpContext context,
bool isCompressed, string contentType)
{
HttpResponse response = context.Response;
response.AppendHeader("Content-Length", bytes.Length.ToString());
response.ContentType = contentType;
if (isCompressed)
response.AppendHeader("Content-Encoding", "gzip");
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetExpires(DateTime.Now.Add(CACHE_DURATION));
context.Response.Cache.SetMaxAge(CACHE_DURATION);
context.Response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
response.OutputStream.Write(bytes, 0, bytes.Length);
response.Flush();
}
private bool CanGZip(HttpRequest request)
{
string acceptEncoding = request.Headers["Accept-Encoding"];
if (!string.IsNullOrEmpty(acceptEncoding) &&
(acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate")))
return true;
return false;
}
private string GetCacheKey(string setName, string version, bool isCompressed)
{
return "HttpCombiner." + setName + "." + version + "." + isCompressed;
}
public bool IsReusable
{
get
{
return true;
}
}
}