一次ABP页面展现优化过程
我们最近SRM项目都是采用ABP技术架构路线,ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称,ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。 ABP实现分层的体系结构,ABP遵循DDD(领域驱动设计)的原则,将分为四个层次: 展现层(Presentation):提供一个用户界面,实现用户交互操作。 应用层(Application):进行展现层与领域层之间的协调,协调业务对象来执行特定的应用程序的任务。它不包含业务逻辑。 领域层(Domain):包括业务对象和业务规则,这是应用程序的核心层。 基础设施层(Infrastructure):提供通用技术来支持更高的层。例如基础设施层的仓储(Repository)可通过ORM来实现数据库交互。
ABP的一个典型的架构方式:

18年开始逐步都使用这个方式来做SRM方面的开发,但是在19年刚开始上线的时候,发现页面打开速度出现问题。几乎随便点一个页面都要盯着白屏等待2-5秒之久,体验很差。通过对页面性能逐步的分析和判断,并做相应优化,最终页面打开速度在都控制3秒内,普通页面都控制1秒内,发布后打开速度约60ms。
技术特点:
服务器端: ASP.NET MVC 5、Web API 2、C# 5.0 DDD领域驱动设计 (Entities、Repositories、Domain Services、Domain Events、Application Services、DTOs等) Castle windsor (依赖注入容器) Entity Framework 6 \ NHibernate,数据迁移 Log4Net(日志记录) AutoMapper(实现Dto类与实体类的双向自动转换)
客户端: Bootstrap Less AngularJs jQuery Modernizr 其他JS库: jQuery.validate、jQuery.form、jQuery.blockUI、json2
页面结构:页面是一个典型的页面,开始进行监测分析。页面打开速度在15秒左右,页面结构和功能都相对简单采用上中下结构,上面和下面忽略不计,中部区域,左边是功能树,右边是功能区,功能区是多卡片功能去,多卡片功能去上面是工具栏,工具栏下面是列表显示或者明细展示,工具栏可以展开一个搜索条件区域,可以按照模块的配置条件条件组合搜索,下面就是一个典型的预示计划发布的展示模块,分页展示20到50条数据作业,展示大约10-30个字段

分析页面执行周期:浏览器发起访问-->IIS应用服务器-->AOP容器定位Congtroller-->解析页面脚本-->通过EF访问数据-->形成页面-->返回页面-->重绘浏览器UI元素

我们大致从几个维度来判断产生页面加载慢的原因:
- 网络传输
- 数据库操作
- 业务逻辑执行
- Razor渲染
- 浏览器页面渲染
- 编程逻辑(c#编写逻辑)
- DBServer优化(内存,磁盘,语句Cache等,Temp,索引,碎片等)
- 应用服务(IIS是否被回收,空闲超时,是否独立应用等等)
- ...
用排除法,我首先检查了客户端浏览器,Web服务器,数据库服务器的网络环境都正常,页面使用Bundle合并压缩了JS,CSS,数据库操作都是简单的查询,所以主要跟传输耗时相关的1,2两项可以先排除,由于6,7,8,9都有大量的分析,这个分析也有一些特定的文章来说明,这里不多做说明;
下一步,我检查了业务执行逻辑,使用比较简单的根据搜索条件查询数据,并返回一个json,json也是经过压缩,返回的时间使用EF+Linq,速度在可接受的秒内,但是没有达到10毫秒级,这里要求同事做了优化,采用sql+dapper优化到10毫秒级。
页面渲染使用bootstrap-table来显示列表,当字段多的时候,渲染就比较慢,但是当字段比较少的时候,虽然也没有特别优化,但这种简单的页面渲染耗时应该大概在50ms,字段多的时候就比较慢一些,但是我并没考虑页面渲染的时间。
从MVC2.0,使用了Razor开发多页应用,对性能还是比较了解,记得Razor的效率也没有这么低啊,一个渲染模板引擎的执行耗费5秒。
好吧,无论如何目前这项是最可疑的,先用Log4Net查看下视图渲染用了多少时间,证明下我的猜想。
ActionFilterAttribute中监测记录View用时经典代码:
//View生成时间监控
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
...
LogManager.WriteLog("OnResultExecuting Request " + filterContext.Request.RequestUri.ToString());
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
...
LogManager.WriteLog("OnResultExecuting Request " + filterContext.Request.RequestUri.ToString());
}
记录下来渲染出视图居然耗费了5秒之多!那么是什么原因导致的呢?
查询了关于Razor的技术信息,做了下面两个方面的改进。
1.在global.asax文件Application_Start()方法中设置仅使用Razor视图引擎。由于默认MVC会首先加载WebForm视图引擎,而本项目中仅用Razor引擎。
//只使用Razor引擎 ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(new RazorViewEngine());
2.设置Web.Config,设置debug模式为false。
<compilation debug="false" targetFramework="4.0" />
这会带来几个方面的优化,如视图文件地址缓存,启用请求超时,Bundle功能生效等。
这些也似乎没有什么好优化的,然后,我再次运行页面,发现打开时间几乎没有减少,WTF?!
再次改进:
再看监测日志,视图渲染仍然用去大概2秒以上,这其中必有蹊跷,不达目的不罢休,于是我又引入了性能监测工具,MiniProfiler。

然后再次打开页面,看到红色的监测提示时,瞬间明白了问题所在。

这个请求过程中sql请求占用90%以上的时间,居然使用了31条SQL,而且我打开看重复语句达20多条!
按道理说,我在看业务逻辑代码时,如果代码中会产生这么多SQL调用,一眼就可以看出来了,然而并没有,而且日志记录了时间耗费是计算在视图渲染中的。
我恍然大悟,如果自己来写Razor渲染的cshtml代码,一定会将Model数据完善后,传过来直接使用,不可能在cshtml代码中再去请求数据,而这里cshtml中的代码,却在反复的去请求数据库数据。
后台的Linq语句大概是ApplicationServier.GetList(),cshtml页面Model为@model PagedList<UserDtos>,而嵌在cshtml中的除了 model.UserName这一类,还有大量的H_Employee.id,大量的员工翻译为姓名,大量的延迟加载导航属性导致反复的数据库访问请求。
在后台的Linq语句中,有很多的一次性Include语句,如.Include(x=>x.H_Employee),把这些语句使用Dapper一次性拉取,修改后再来看打开页面的监测结果。
可以看到用时从2769ms减少到656ms,页面打开用时较之前减少了四分之三,控制在1秒之内,发布之后页面打开用时约60ms,sql仅仅用了5条便完成业务逻辑,这才是asp.net mvc最起码该有的样子。
文章部分节选自览岳 https://www.cnblogs.com/jiujiduilie/p/9419460.html

浙公网安备 33010602011771号