性能优化之页面缓存(以Javascript方式缓存页面部件)



本篇文章为大家讲解一个关于客户端缓存页面的技巧——以Javascript的方式来缓存页面的静态“部件”。

如果整个页面能够被缓存到浏览器上,一个满载HTML的巨大页面也能运行地很棒。你可以使用Http响应缓存头来解决这个问题,要么将它们手工注入你的代码,要么在aspx页面上使用@OutputCache标签来申明:



<%@ OutputCache Location=”Client” Duration=”86400” VaryByParam=”” VaryByHeader=”” %>


但是,浏览器上的这些缓存记录一般只能维持一天的时间。如果你有一个既有静态内容又有动态内用的页面,你则不能再页面级别使用这种输出缓存的形式。一般来讲,站点的页面、logo、左边的导航菜单和页脚都是静态部分,参考下图:



上图中,其实只有区域二是绝对动态的,区域一和区域三以及页脚和菜单都是可以部分甚至全部缓存!

有时候在页面的主体区域也有很多部分不是经常变化。将所有这些合并起来的时候,会占用相当长的下载时间。这样,当一些有效部分从不发生变化的时候,用户也不得不将其页面记录不断地下载到本地。如果你能够将这些静态部分缓存到浏览器上,每次页面加载的时候你会少下载很多字节。如果整个页面的大小为50KB,则至少有20KB的内容是静态的,而其他30KB也许是动态的。如果你能使用页面片段的客户端缓存(并不是asp.net的服务端页面的输出缓存),你可以很容易地节约40%的下载时间。此外,也不需要为这些静态内容发送请求到服务端,因为它们已经缓存到了浏览器上。因此,每次加载的时候,服务端不必处理这么巨大的页面。

Asp.net 通过使用@Outputcache的方式提供了页面片段缓存,虽然这样确实很有好处,但是问题是该缓存是基于服务端的。它负责返回用户控件的输出并从服务端缓存中返回。但是你不能消除下载这些字节所付出的代价。因为它仅仅节省了服务器上一些CPU处理能力,这对用户来讲没有多大的益处。

对页面部分进行缓存仅有的方式是允许浏览器单独地下载这些部分并使得这些部分就像图像、CSS或Javascript文件一样是缓存的。因此,我们需要单独地下载页面片段并将它们缓存到浏览器缓存中。Iframe框架很容易实现这种方式,但是它会使页面很笨拙并且也不支持适应父框架的页面CSS样式。在Iframe框架内部,你需要再次下载你可能要使用的其他Javascript代码和Ajax框架。由于这些文件是从缓存中获取,虽然下载速度似乎很快,但是再次下载整个框架以及大量的Javascript代码时会增加浏览器的压力。

有一种更好的办法:使用Javascript代码来呈现页面内容;这样Javascript将从浏览器的缓存中获得内容。想法如下:

1、  将整个页面分割成很多部分。

2、  使用Javascript代码生成页面内容。每块可缓存的部分都由Javascript代码控制,然后再呈现给Html

3、  浏览器仅缓存可缓存的部分,因此它们不会再次下载(直到用户进行刷新或清除缓存为止)。页面上那些不可缓存的部分和虽然频繁发生变化但浏览器没有缓存的部分被考虑作为页面布局,如下图所示:



上图为典型的主页布局,其中主体段是动态的,而页眉页脚、左菜单和logo是静态的

因为仅仅主体部分是动态的,页面的其余部分是完全可缓存的。因此,Default.aspx呈现出的整个页面看起来类似如下代码:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ OutputCache NoStore="true" Location="None" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;

<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<table width="100%" border="1" style="height:450px">
<tr>
<td>Some logo here</td>
<td><script id="script1" src="Header.aspx" type="text/javascript"></script></td>
</tr>
<tr>
<td><script id="script2" src="LeftMenu.aspx" type="text/javascript"></script></td>
<td bgcolor="lightgrey">
<div>This is hte dynamic part which gets changed on every load.
Check out the time when it was generated:<%=DateTime.Now %></div>
</td>
</tr>
<tr>
<td colspan="2">
<script id="script3" src="Footer.aspx" type="text/javascript"></script>
</td>
</tr>
</table>
</form>
</body>
</html>


在浏览器上缓存页面的各个部分可以消除下载静态块的发生。例如,页眉、左菜单和页脚都是不会发生变化的部分,因此它们被缓存起来以便用户的重复访问,但是主体部分每次用户访问的时候都需要从服务器上获取新内容。

被缓存的部分会持续30min以上,因为浏览器根本不会从服务器上下载它们,并且这也节省了大数据的传输耗费。仅仅主体部分才从服务器上下载。

首次访问的时候,页面部分会一个接着一个地进行下载,正如你从下图中看到的一样:



但是在第二次访问的时候,仅下载Default.aspx页面并且其他部分会立即从缓存中进行加载。下面一份图展示了加载页面不同缓存部分的实例。



第二次访问时,缓存部分的内容立即从浏览器缓存中获得。因此,整个下载字节仅为Default.aspx页面进行,而不是页面上的各个小部分区域。因此,下载时间大大地减少了并且再次访问时也非常地快。

与第一次访问页面上的每块要用1S的下载时间相比,第二次访问页面部分区域的下载时间是5-7ms之间。这说明了再次访问页面缓存部分区域时是多么的快。

让我们看看一个名为Header.aspx的文件,其内容是从缓存中获得的。

缓存Header.aspx页面;注意到与一个标准的aspx页面相比变化的仅是ContentType属性

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Header.aspx.cs" Inherits="Header" ContentType="text/html/javascript" %>
<%@ OutputCache Location="Client" Duration="86400" VaryByParam="" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;

<html xmlns="http://www.w3.org/1999/xhtml"&gt;
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<h1>This is the big fat header.Lots of HTML</h1>
Generater on server at:<%=DateTime.Now %>
</div>
</form>
</body>
</html>


Content type被设置为text/html/javascript值,这使得你需要对页面进行一些手工实现。

当你在某个脚本标签里放置一个aspx页面的时候,会出现错误。因为<script id=”script1” src=”Header.aspx” type=”type/javascript”>的预期输出为Javascript而不是HTML内容。如果提供了HTML输出,浏览器会完全地忽略它。因此,首先要满足<script>标签能正常工作,Header.aspx页面必须返回Javascript代码而不是HTML内容。其次,借助document.writeln方法使得javascript呈现Header.aspx页面的HTML输出。

一个Http Module会截获所有对.aspx页面的调用请求。当某个页面准备发送到浏览器端的时候,会检查content type是否为text/html/javascript。如果是,然后将页面输出转换为一个类似于javascript表现。

我们先创建一个名为Html2JSPageFilter.cs的响应过滤器文件对响应流的Write方法进行重写,并将页面的HTML转换为Javascript表示。因此,asp.net为你产生Html,并且你将它转换为Javascript表示用来在浏览器上呈现源Html。

HttpResponse的Filter属性

这里我们将使用到HttpResponse的Filter属性,这是一个非常有用但很少被使用的属性。MSDN对其的解释是——获取或设置一个包装的筛选器对象以在传输到客户端之前修改Http实体内容。也就是说,你可以给你的每个页面输出设置你自己的筛选器。HttpResponse将发送所有的内容到筛选器

使用Http Module

你也许在想,是否可以使用一个Http handler来处理这个问题。例如,你需要使用asp.net的默认页面handler来截获发送所有以.aspx为扩展名文件的调用请求。但是你不能将相同的扩展名的文件注册为另一个handler。本方案中,你需要使用HttpModule,它能截获任何一个来自asp.net管道的请求。为了实现这个功能,你需要:

1、  以Html的方式返回整个页面输出

2、  过滤包含在<form>标签内的内容。Asp.net总是会产生一个<form>标签,并且页面上的内容仅在该标签内部时才是可用的

下面的代码从aspx页面返回产生的HTML内容,并从<form>标签中解析出内容

public override void Write(byte[] buffer, int offset, int count)
{
string strBuffer = System.Text.UTF8Encoding.UTF8.GetString(buffer, offset, count);
//——————————–
//Wait for the colsing </html> tag
//——————————–
Regex eof = new Regex("</html>", RegexOptions.IgnoreCase);

if (!eof.IsMatch(strBuffer))
{
responseHtml.Append(strBuffer);
}
else
{
responseHtml.Append(strBuffer);
string finalHtml = responseHtml.ToString();

int formTagStart = finalHtml.IndexOf("<form");
int formTagStartEnd = finalHtml.IndexOf(‘>’, formTagStart);
int formTagEnd = finalHtml.LastIndexOf("</form>");

string pageContentInsideFormTag = finalHtml.Substring(formTagStartEnd + 1, formTagEnd - formTagStartEnd - 1);


3、  移出隐藏字段的ViewState,否则它会与Default.aspx页面上的Viewstate发生冲突(默认页面有它自己的Viewstate)。因此,包含Viewstate的<input>标签不能再次发送到浏览器端。这意味着你不能使用带Viewstate的控件,虽然它是实现这种方法的快捷方式。通常来说,缓存部分存放的都是静态内容,因此不管怎样,需要Viewstate在这里是不应该的。

下面的代码移出<input>字段的Viewstate,以避免它和Default.aspx页面的Viewstate发生冲突:

Regex re = new Regex("(<input.?__VIEWSTATE.?/>)", RegexOptions.IgnoreCase);

pageContentInsideFormTag = re.Replace(pageContentInsideFormTag, string.Empty);


4、  将整个HTML输出转换为Javascript字符串格式。该字符串包含一个未格式化的HTML,可以作为innerHTML使用或者在document.write(‘’)语句的方法里是使用

下面的代码将HTML输出转换为一个Javascript字符串表示并且消除新的行、空格、省略符号,等。可将这一的字符串设置为一个元素的innerHTML值或将它传递给document方法:

string javascript2html = pageContentInsideFormTag.Replace("\r", "")
.Replace("\n", "")
.Replace(" ", " ")
.Replace(" ", " ")
.Replace(" ", " ")
.Replace("\", "\\")
.Replace("’", "\‘");


5、  使用document.write方法,它能够将javascript字符串输出到浏览器上。HTML被直接添加到页面内容

生成一个document.write的语句,用来将Html输出到浏览器上:

string pageOutput = "document.write(‘"+javascript2html+"’);";
byte[] data = System.Text.UTF8Encoding.UTF8.GetBytes(pageOutput);

responseStream.Write(data, 0, data.Length);


这是一个非常完美的骗局。先使用一个响应过滤器来返回.aspx文件的输出,然后将它转换为Javascript表示。使用document.write方法将HTML在浏览器的DOM中呈现并获得该Javascript代码的缓存。为了方便,这里使用一个HttpModule对象来挂钩asp.net管道,并等待.aspx文件发送text/html/javascript形式的内容。然后再挂钩该响应过滤器到asp.net请求管道。

 

注册HttpModule

HttpModule对象非常简单。它会挂钩上下文的ReleaseRequestState事件,当页面输出准备发送到浏览器的时候会激活该事件。在该事件的句柄中,调用响应过滤器来将HTML转换为Javascript表示的格式

下面是实现代码:

public void Init(HttpApplication context)
{
context.ReleaseRequestState += new EventHandler(InstallResponseFilter);
}

private void InstallResponseFilter(object sender, EventArgs e)
{
HttpResponse response = HttpContext.Current.Response;

if (response.ContentType=="text/html/javascript")
{
response.ContentType = "text/javascript";
response.Filter = new Html2JSPageFilter(response.Filter);
}
}


最后,通过在web.config文件中添加一个入口节点<httpModule>来进行注册

<httpModules>
<add name="Html2JSModule" type="Html2JSModule"/>
</httpModules>


你可以在你的.aspx文件中使用这种方法在客户端充分节省用户的下载时间。虽然首次访问时会稍微增加下载时间——每块脚本标签在网络上的平均往返时间大约是200ms,但是再次访问时会更加轻松。你自己留意一下这些不同的性能问题:登陆www.pageflakes.com站点并让整个站点完成加载。然后,关闭浏览器,再打开它,并再次输入URL地址。可以看到再次访问速度是多么地快。没错,它们使用的就是这种技巧!你会看到与第一次访问发送的大约400KB的数据相比,第二次访问时仅仅发送了10-12KB的数据。这是因为所有页面片段都被缓存到浏览器中了,只要缓存没有过期,后续的用户访问站点时并不需要多少下载时间.

源代码下载



版权声明:本文为博主原创文章,未经博主允许不得转载。