在现代的网络应用中,用户体验至关重要。当用户访问一个动态生成的网页时,如果服务器端的数据库操作耗时较长,用户往往会面对一个空白的屏幕等待。为什么不利用这段时间来下载JavaScript和CSS文件呢?当服务器完成工作后,浏览器就可以立即渲染动态内容。这种优化技术可以提高任何需要在服务器端花费时间完成工作的动态页面的性能,并且可以同时下载一些JS和CSS文件。
例如,一个典型的页面可能需要7秒钟来渲染内容,其中JS/CSS文件的加载是在页面被传送到浏览器之后才开始的。而优化后的版本,在服务器还在生成页面内容的同时,浏览器就开始加载JS/CSS文件,这样渲染时间可以缩短到6秒,节省了1秒的时间。页面中JS、CSS文件越多,或者动态页面的生成越慢,得到的改进就越大。
实现这种优化其实很简单!使用像PHP、NodeJS、旧版ASP这样的技术,可以直接从代码中写入输出,并随时刷新响应。例如,一个PHP解决方案可以是这样的:
<?php
// 清除所有当前打开的缓冲区
while (ob_get_level() > 0) {
ob_end_flush();
}
ob_start();
?>
[一堆HTML...]
<!--
0001020304050607080[2K字节的填充]...
-->
<?
php
ob_flush();
flush();
require_once('trends.inc');
// 包含慢查询
?>
但对于ASP.NET WebForms或ASP.NET MVC,尤其是使用Razor视图引擎时,这就变得复杂了。不能随意调用Response.Flush()。对于ASP.NET WebForms,它会破坏视图状态、回发事件处理程序等。对于使用Razor的ASP.NET MVC,它就是不起作用。
在ASP.NET MVC中有一些巧妙的方法可以做到这一点。例如,使用Partial Views,它包含了想要立即发送的内容,比如
部分充满了js、css。一个例子可以是:
public ActionResult FlushDemo()
{
PartialView("Head").ExecuteResult(ControllerContext);
Response.Flush();
// 做其他工作
return PartialView("RemainderOfPage");
}
但这意味着不能使用布局来持有
部分。必须停止使用任何布局解决方案。此外,控制器现在必须为分割视图和提前发送部分视图做管道工作。它现在被一些“视图”类型的代码污染了。如果从视图中做这件事就好了,可以定义视图的哪一部分应该首先渲染,哪一部分应该在服务器端工作完成后渲染。此外,理想的解决方案应该能够很好地与布局一起工作。
首先,希望控制器只做“控制器”的事情,而不用担心视图、部分视图、响应刷新等。以下是如何在控制器中实现:
public ActionResult Index()
{
Thread.Sleep(5000);
// 在这里做一些昂贵的操作
var model = new HomeModel() { Text = "Hello" };
return View(model);
}
public ActionResult FastIndex()
{
var asyncModel = new AsyncModel(new HomeModel(), model =>
{
Thread.Sleep(5000);
// 在这里做一些昂贵的操作
model.Text = "Hello";
});
return View(asyncModel);
}
第一个Index()向展示了通常会如何编写控制器代码。这是慢的版本。FastIndex()是更快的版本。这个想法是将生成模型的代码的执行推迟到视图渲染开始时。
现在来看慢视图:
@{
ViewBag.Title = "Home";
}
<p>来自慢索引:@Model.Text</p>
这是同样视图的更快版本:
@{
ViewBag.Title = "Home";
}
@section AsyncBody {
@{ViewData.Model = Model.Execute();}
<p>来自更快索引:@Model.Text</p>
}
AsyncBody部分包含了在控制器中运行昂贵代码后渲染的视图内容。在该部分内部,首先调用Model.Execute(),这将调用AsyncModel中的Execute()函数。然后它触发在控制器中的FastIndex函数中定义的委托,运行代码。
现在来看布局部分。布局负责立即渲染
部分,然后渲染视图中的AsyncBody部分。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css"/>
<script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")"></script>
...
...
</head>
<body>
<p>在主体之前</p>
@if (IsSectionDefined("AsyncBody")) {
var sb = ((StringWriter)ViewContext.Writer).GetStringBuilder();
Response.Write(sb);
Response.Flush();
sb.Length = 0;
@RenderSection("AsyncBody");
}
else {
@RenderBody();
}
<p>在主体之后</p>
</body>
</html>
在这里,它获取了Razor内部缓冲区,该缓冲区保存了生成的内容,并将其刷新到响应中。然后它调用@RenderSection("AsyncBody"),这将调用Model.Execute(),进而调用控制器中的昂贵代码。
AsyncModel类实际上并没有做什么:
public class AsyncModel
{
private Action ControllerCode;
public T RealModel;
public AsyncModel(T realModel, Action controllerCode)
{
this.ControllerCode = controllerCode;
this.RealModel = realModel;
}
public T Execute()
{
this.ControllerCode(this.RealModel);
return this.RealModel;
}
}