ASP.NET性能优化的15个技巧
对于ASP.NET的Web应用来说性能一直是非常重要的。各种证据表明,缓慢的加载时间和不佳的互动体验都可能将客户推往他处。即使是那些对于用户来说别无选择的内部应用,他们的满意度也是会与速度挂钩的。
我们可以用很多种方法来提高网站的性能。在此,让我们来看看其中的十五个。
1.量化一切
首先要做的第一件事是收集应用程序的性能基线。有时候,你的本意是想通过一些修改来提高网站的性能,但是实际上性能却被降低了。虽然谈不上是个黑盒子,但是对于性能的调优,确实会时常给我们带来意想不到的效果。因此,量化性能需要有一个全面的对于服务器、JavaScript和加载效率的衡量。除了手中的秒表,你还需要有诸如Prefix之类的各种性能测试工具(请参见:https://stackify.com/prefix/)。
Prefix能让你更关注于低效的查询,以及大量的JavaScript文件等方面。量化的数据会告诉你,对于哪些方面的优化能够真正起到作用。你可以自己生产一张列表,并按照优先级进行排序。而那些位于列表底部的,则往往是一些重要程度最小、你几乎可以忽略的方面。
2.先摘下“低垂的水果”
通常情况下,你应该优先处理上述清单中影响比较大的部分。很明显,如果你能尽快地向用户证明性能影响的“痛点”所在,你就能从精神上和资金上获得更多的优化支持,当然这也是你不断进行性能调优的动力。比如说:全局性的因素(如JavaScript、CSS、及其同类的加载)都会比单个页面的修改产生更大的影响。
本文后面介绍的内容,将粗略地以影响程度从大到小来依次展开。当然,根据各个网站的差异性,你可以有所取舍地根据自身情况来予以实践。
3.启用压缩
HTTP协议并非是一个特别有效率的协议。在默认情况下,它不会对内容进行压缩。虽然有些网络资源,比如说图片已经被压缩了,但是像HTML、CSS和JavaScript通常还是以文本的形式在进行传输。就算是最古老的浏览器也能支持通过gzip的算法来对HTTP内容进行压缩,而且gzip大约能够压缩HTML文件的三分之二。也就是说:一个100KB的文件被压缩后最终至多只有33KB,这比例是相当惊人的!
与之对应的另一个改良算法是Brotli(请参见:https://en.wikipedia.org/wiki/Brotli)。其效果更佳,而且能够被当前大多数浏览器所支持。
4.减少HTTP请求
实际上,浏览器每打开和建立一个与服务器的连接,都是要“交税”的。而这个税收就是以TCP/IP连接的开销形式来体现的。这个现象在那些具有高延迟、并需要花费很长时间才能建立新连接的情况下,就显得尤为突然。基于这个客观事实,为了减少HTTP的请求数量,浏览器应该将它们的请求限制在单台服务器上,以实现最大程度的优化。
延迟与带宽
我们在优化网页的加载时,理解延迟和带宽之间的差别是非常必要的。让我们想象一下:你有20头驴,你需要从坐落在班芙的一个集散点转移到另一个坐落在大峡谷的集散点。为了让这些驴子能够尽快地转移过去,你需要优化两个方面:你一次性能够转移的驴子的数量,和单次转移驴子所需要的时间。
带宽就像是你一次性能够转移的驴子的数量。在高带宽的情况下,你可以一次性将多个驴子放入家畜车进行转移。而在低带宽的情况下,你只能将一头驴子放在你那2001年产的本田思域的单个座位上进行转移。
而延迟则是你从班芙到大峡谷,进行单次转移驴子所需要的时间。高延迟意味着沿途有多次延误,所以减慢了单次的转移时间。而低延迟则意味着你通宵达旦地一路狂奔,不会在任何旅游景点处停车观景。因此在理想情况下,你应该一次性转移尽可能多的驴子,而且避免沿途上的任何停顿。
封装
根据向服务器请求的资源类型不同,我们可以用几个不同的方法来降低请求的数量。对于JavaScript,可用类似webpack、gulp或grunt的工具将各个JavaScript脚本捆绑“串联”到一起成为一个单独文件。我们也可以使用相同工具里的不同任务,来将CSS文件合并为单一文件。
而对于图像则有点复杂。如果你在网站上使用了许多小的图像,那么就可以使用CSS Spriting的技术。比如说:我们将所有的图像合并成单独的一个,然后使用CSS的偏移量将图像进行转换,只显示我们所需要的单个子图像。还有其他的一些工具,能够简化该过程,不过它们都需要人工的干预。而另一种可选的方法则是使用icon font。
5. 基于SSL的HTTP/2
HTTP/2是HTTP的新版本,它进行了许多非常有用的优化。首先,在上面提到压缩方面,它进行了扩展,同时也包含了各种协议的包头。更有趣的是,在与服务器之间的连接上,它使用一种被称为“管道”的机制,来传输多个文件。这就意味着:通过合并文件来减少HTTP请求的方式在很大程度上已经不必要了,而它的效率则更高。
如今,几乎每一种浏览器都能支持HTTP/2。但颇具讽刺意味的是:在服务器端的支持上却有着各种限制。例如:在写数据的时候,Azure的Web应用程序就不能支持HTTP/2。
不过,服务器现在也能够对网页上的内容做出智能的决策,并在它们被请求之前就主动向下推送资源。因此,如果索引(主)页面上包含一个JavaScript文件的话,它只有在浏览器解析了整个页面之后才会被发现;而现在的服务器完全可以根据指令,在浏览器意识到它之前,就主动将该文件传输过去。
因为所有支持HTTP2的浏览器都能够提供HTTPS的服务,因此SSL自然也是必需支持的技术之一。
6.缩减文件尺寸
虽然压缩对于减少传输线路上的数据总量来说是一种很好的手段,但是所有的压缩算法对于发送HTML、CSS和JavaScript来说都是无损的压缩方式。这就意味着对compress(x)的结果进行解压缩--decompress(x)总能得到x。在理解了这一点的基础上,我们就可以寻求在尺寸上的进一步缩减了。举例而言,如下的JavaScript:
function doSomething(){ var size_of_something_to_do = 55; for (var counter_of_stuff = 0; counter_of_stuff < size_of_something_to_do; counter_of_stuff++) { size_of_something_to_do--; } }
它在功能上等同于:
function doSomething(){var a=55;for(var b=0;b<a;b++){a--;}}
由于变量的作用域是完全私有的,因此空白的占位符就显得没有必要了。这个过程被称为缩减。与压缩技术类似,该技术也可以被应用到CSS、甚至是HTML上。
7.优先加载CSS
请务必优先加载你网站上的CSS内容,而且最好放在页面的头部。
要理解这个原因,你需要先了解浏览器是如何实现其惊人的浏览速度的。在下载页面的同时,浏览器会尽快将其从应用程序那里得到的内容渲染出来。通常情况下,由于浏览器并不知道页面上的哪些内容是无效的,因此它只能像玩猜谜游戏那些做出猜测。
之后,当浏览器意识到其猜测有误时,它不得不抛弃之前所做的一切,重新开始渲染。其中导致这些返工的一个原因就是可能要增加新的样式表。因此,优先加载样式表,就可以避免对已经渲染的元素进行返工。
8.最后加载JavaScript
JavaScript则是CSS的另一面,应当最后被加载。这是因为我们希望页面能够被尽可快地渲染出来,而JavaScript则没必要在一开始就被渲染。用户通常会花些时间来阅读页面的内容,以决定下一步做什么。那么这个时间窗口就被用来在后台加载脚本、和显示页面上的交互效果了。
不过,这里需要注意的是:如果你的网站上有大量的JavaScript,例如Angular或React之类的应用,那么你可能会发现最后加载JavaScript反而不一定是最好的方案。你不妨考虑采用只加载JavaScript的必要部分到bootstrap的应用中,并且在后台加载更多的JavaScript。如果速度对你来说非常重要的话,你甚至可以考虑所谓的同构或是通用的应用程序。因为在这些应用中,各个页面在服务器端被渲染,然后JavaScript应用被附加到已渲染的HTML中,并予以接管。这些应用程序具有在加载的过程中无缝衔接的速度优势。
9.缩小图片
在理想状态下,你的网站根本不会包含任何图片。通常情况,矢量图片会比各种真彩图片要小许多,因此如果使用inline-SVG和CSS的技巧来为你的网页创建矢量图的话,效率会非常高。然而事实上却不能完全是这样,因此,你必需进行一些缩小图片的工作。虽然要搞清楚各种正确的编码设置着实不容易,但是我们可以借助像tinypng之类的服务(请参见https://tinypng.com/)来达到目标。另外,它的logo是一只非常可爱的熊猫。
当然,也有一些JavaScript构建工具的其他插件,可以达到类似的优化效果,请参见:https://www.npmjs.com/package/gulp-image-optimization。
10.检查你的查询
各种对象关系映射(object-relational mappers,ORM)已经对开发人员的生产力起到了提升作用,但它们只是提供了优化查询的一个抽象层。当你可能会有N+1个选择错误、或是从服务器获取了太多的数据时,只有通过Prefix才能够突显具体的次数。你会惊奇地发现通过使用预先加载、而非延迟加载,以及检查各种预测值,问题就会变得非常容易解决。当然,微软对于优化实体框架(Entity Framework,EF)的各种SQL调用,也有一些独到的建议,请参见:https://msdn.microsoft.com/en-us/library/hh949853(v=vs.113).aspx。
11.缓存你的页面
通常情况下,你网页上的数据会随着时间的推移而缓慢发生变化。比如说:在Stack Overflow(译者注:它是一个技术问答网站。用户可以在该网站上免费创建主页、提交问题、浏览问题、索引相关内容。)上的那些热点问题的页面会实时地被更新,但是其数据的变化并不够显著,不至于触发对数据库的重新查询。因此我们没有必要去调整数据库、并对复杂的页面进行重新渲染,而是可以将该网页推送到缓存队列中,并使用这些数据来响应后续的请求。
如果你碰巧使用着ASP.NET的MVC缓存,那么一个action的响应就应该只是简单地添加单一的属性到action中。
[HandleError] public class HomeController : Controller { [OutputCache(Duration=10, VaryByParam="none")] public ActionResult Index() { return View(); } }
如果你所缓存的页面全部内容并非是你都需要的,那么请继续往下看第12个技巧。
12.仅缓存页面的部分内容
你可能只想缓存页面的一部分内容; 这被俗称为甜甜圈孔洞缓存(donut hole caching)。当你在同一页上,既有用户特有的数据、又有一般的数据时,这会是一种非常有用的方法。用户特有数据随着用户的不同而改变,而页面上的其余部分则对于所有用户都是相同的。在MVC 5的应用中,这是通过做局部视图(请参见:https://visualstudiomagazine.com/articles/2017/05/01/doughnut-hole-caching.aspx)来实现的,并且在MVC的核心,我们会用到缓存标签帮手(请参见:https://www.davepaquette.com/archive/2015/06/03/mvc-6-cache-tag-helper.aspx)。
13.内容分发网络(CDN)
全球各处都有非常多的内容交付网络,它们可以通过离你最近的节点,将内容高速地传递到你的面前。
14.缩小你的库
如果你正在使用像jQuery之类的库,那么你可以考虑是否真的会用到它的所有功能,也许你可以去使用一个更小、更有针对性的库。比如说:Zeptojs就是一个能够支持jQuery的很多功能、而又更小的库。其他像jQuery UI的库,也能为构建个性化的包提供删减过的功能。如果你正在使用Augular的话,那么你在对产品进行编译的时候,就可以像“摇动树干”一样,将那些在你的项目中完全用不到的库去除掉。可见这种方式在保留同等功能的情况下,能有效减少传输中的数据载荷。
15.避免客户端重定向
最后的技巧是避免通过使用客户端的重定向。重定向势必添加了额外的服务器跳转的开销。而在诸如蜂窝网络这样高延迟的网络中则是完全不可取的。相反地,如果使用服务器端的重定向,则不会增加跳转的开销。不过,此法对于将用户重定向到SSL版本的页面上却不太有效果。针对这种情况,HTTP严格传输安全(HTTP Strict Transport Security,HSTS,请参见:https://www.troyhunt.com/understanding-http-strict-transport/)和预加载正好能够提供帮助。只要你将网站加入预加载列表中,就会被自动重定向到相应的SSL版网页上。