ASP.NET MVC ETag & Cache等优化方法
背景
最近有一个项目是用SmartAdmin + Jquery + EasyUI 一个ASP.NET MVC5的项目,一直存在一个性能问题,加载速度比较慢,第一次加载需要(在没有cache的情况下)需要4-5秒完成全部的加载.
如下图就是用Chrome PageSpeed 测试的结果
有几个非常重要的指标
Fist Contentfu Paint:第一次绘出页面,需要4秒,前4秒都是白屏,确实有点长
Fist Meaningfull Paint:第一次绘出有意义的内容,需要8.6秒,才出现可见的操作页面.
Eliminate render-blocking resources:阻塞加载资源文件,因为的项目在head中加载了jquery和css,因为有些代码必须先执行导致的
Remove unused css:存在大量的没用的css样式定义,这也很难避免.
分析一下原因
出现上述问题的主要原因,页面本身的大小,所有资源加起来超过3.2M,Jquery EasyUI的JS+css 就接近3M,另外页面里有嵌入了好几个PartialView,还有就是执行js的时间,EasyUI DataGrid需要从后台抓起数据并生成复杂的Dom结构这都需要时间.
一般的优化手段
Cache
第一想到的就是使用cache,单只能解决第二次访问的速度问题,对少有点用,我一般会这样做,设置的方法有
- 添加outputcache 例如:
1 2 | [OutputCache(Duration = 360, VaryByParam = "none" )] public ActionResult Index() => this .View(); |
- web.Config 添加对静态文件的缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <system.webServer> <staticContent> <remove fileExtension= ".js" /> <mimeMap fileExtension= ".js" mimeType= "text/javascript" /> <remove fileExtension= ".ico" /> <mimeMap fileExtension= ".ico" mimeType= "image/x-icon" /> <remove fileExtension= ".eot" /> <mimeMap fileExtension= ".eot" mimeType= "application/vnd.ms-fontobject" /> <remove fileExtension= ".woff" /> <mimeMap fileExtension= ".woff" mimeType= "application/x-font-woff" /> <remove fileExtension= ".woff2" /> <mimeMap fileExtension= ".woff2" mimeType= "application/x-font-woff2" /> <remove fileExtension= ".svg" /> <mimeMap fileExtension= ".svg" mimeType= "image/svg+xml" /> <remove fileExtension= ".ttf" /> <mimeMap fileExtension= ".ttf" mimeType= "application/x-font-ttf" /> <clientCache cacheControlMode= "UseMaxAge" httpExpires= "365.00:00:00" cacheControlMaxAge= "365.00:00:00" /> </staticContent> </system.webServer> |
压缩和合并资源文件
尽量减少资源文件的大小和请求次数,通常的做法就是使用BundleConfig.cs合并和压缩js,css文件.我现在使用bundleconfig.json配置代替System.Web.Optimization.配置灵活一点,如果使用bundleconfig.json 编译压缩还需要解决客户端更新的缓存的问题,我使用一下代码添加一个指纹标志
public class Fingerprint { public static string Tag(string rootRelativePath) { if (HttpRuntime.Cache[rootRelativePath] == null) { string absolute = HostingEnvironment.MapPath("~" + rootRelativePath); DateTime date = File.GetLastWriteTime(absolute); int index = rootRelativePath.LastIndexOf('/'); string result = rootRelativePath.Insert(index, "/v-" + date.Ticks); HttpRuntime.Cache.Insert(rootRelativePath, result, new CacheDependency(absolute)); } return HttpRuntime.Cache[rootRelativePath] as string; } }
<system.webServer>
<urlCompression doStaticCompression="true" doDynamicCompression="true" dynamicCompressionBeforeCache="false" /> <rewrite> <rules> <rule name="fingerprint"> <match url="([\S]+)(/v-[0-9]+/)([\S]+)" /> <action type="Rewrite" url="{R:1}/{R:3}" /> </rule> </rules> </rewrite> </system.webServer>
<link rel="stylesheet" href="@Fingerprint.Tag("/content/site.css")" />
ETag
ETags 是用于 Web 缓存验证的工具,允许有条件的客户端请求。通过 ETags,浏览器可以判断某项资源是否被需要。如果不需要,浏览器就不会向 Web 服务器发送请求,从而最小化请求数量。配置方法
- 全局方案,自定义一个HttpModule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | public class ETagHttpModule : IHttpModule { #region IHttpModule Members void IHttpModule.Dispose() { // Nothing to dispose; } void IHttpModule.Init(HttpApplication context) { context.BeginRequest += new EventHandler(context_BeginRequest); WebPageHttpHandler.DisableWebPagesResponseHeader = true ; } #endregion void context_BeginRequest( object sender, EventArgs e) { HttpApplication app = sender as HttpApplication; //if (app.Request.CurrentExecutionFilePath.EndsWith("/") || app.Request.CurrentExecutionFilePath.EndsWith(".cshtml")) //{ app.Response.Filter = new ETagStream(app.Response, app.Request); //} } #region Stream filter public class ETagStream : MemoryStream { private HttpResponse _response = null ; private HttpRequest _request; private Stream _filter = null ; public ETagStream(HttpResponse response, HttpRequest request) { _response = response; _request = request; _filter = response.Filter; } private string GetToken(Stream stream) { var checksum = new byte [0]; checksum = MD5.Create().ComputeHash(stream); return Convert.ToBase64String(checksum, 0, checksum.Length); } public override void Write( byte [] buffer, int offset, int count) { var data = new byte [count]; Buffer.BlockCopy(buffer, offset, data, 0, count); var token = GetToken( new MemoryStream(data)); var clientToken = _request.Headers[ "If-None-Match" ]; if (token != clientToken) { _response.AddHeader( "ETag" , token); _filter.Write(data, 0, count); } else { _response.SuppressContent = true ; _response.StatusCode = 304; _response.StatusDescription = "Not Modified" ; _response.AddHeader( "Content-Length" , "0" ); } } } #endregion } |
1 2 3 4 5 | <modules> <remove name= "FormsAuthentication" /> <!--<add type= "WhitespaceModule" name= "WhitespaceModule" />--> <add type= "WebApp.ETagHttpModule" name= "ETagHttpModule" /> </modules> |
- Action页面级
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class ETagAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) => filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request); } public class ETagFilter : MemoryStream { private HttpResponseBase _response = null ; private HttpRequestBase _request; private Stream _filter = null ; public ETagFilter(HttpResponseBase response, HttpRequestBase request) { _response = response; _request = request; _filter = response.Filter; } private string GetToken(Stream stream) { var checksum = new byte [0]; checksum = MD5.Create().ComputeHash(stream); return Convert.ToBase64String(checksum, 0, checksum.Length); } public override void Write( byte [] buffer, int offset, int count) { var data = new byte [count]; Buffer.BlockCopy(buffer, offset, data, 0, count); var token = GetToken( new MemoryStream(data)); var clientToken = _request.Headers[ "If-None-Match" ]; if (token != clientToken) { _response.AddHeader( "ETag" , token); _filter.Write(data, 0, count); } else { _response.SuppressContent = true ; _response.StatusCode = 304; _response.StatusDescription = "Not Modified" ; _response.AddHeader( "Content-Length" , "0" ); } } } |
1 2 3 | //[OutputCache(Duration = 360, VaryByParam = "none")] [ETag] public ActionResult Index() => this .View(); |
效果图,回发的字节数确实减少了很多,单响应时间差不多,不是很明显.
总结
优化方案有很多,但是感觉效果都不是很理想,要做到极致的用户体验,可能真的要抛弃Jquery,EasyUI,这类肥大又复杂的类库.
问题
另外大家有没有非常好用又简单的方法解决初始加载白屏的问题,我试过用js preloading图层动画,但是效果还是不理想.但看过一些网址和APP做的效果非常好,不知道具体是如何实现的,在Asp.net mvc环境下能不能用
参考文章
(ASP.NET MVC 应用提速的十种方法)http://blog.oneapm.com/apm-tech/679.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix