10.提升应用程序的性能

随着 ASP.NET 的每个新版本的发布,ASP.NET 团队继续将性能放在首位。当 ASP.NET Core 引入一种构建 Web 应用程序的不同方式并进行简化增强(包括中间件Razor Pages)时,重点也一直放在改进 C# 语言上。这些技术赋予了 ASP.NET 活力和速度。

ASP.NET 是跨平台的,具有对依赖注入的内置支持,是开源的,并且是业内性能最快的框架之一。

虽然这是一本包含一章关于性能的 ASP.NET 书籍,但 Web 开发的其他方面也同样重要。我们将尽可能专注于 ASP.NET 和 C# 性能。

在本章中,我们将介绍以下主要主题:

  • 性能为何如此重要
  • 建立基准
  • 应用性能最佳实践

在本章结束时,您将理解性能在应用程序中的重要性,如何建立客户端和服务器端基准以及如何优化客户端资源以加快交付速度的技术,以及最后如何使用服务器端性能技术(例如优化 HTML、实施各种缓存技术和识别慢速查询)更快地交付内容。

技术要求

在为您的 Web 应用程序创建基线和测试性能时,需要一个您觉得编写代码很舒服的 IDE 或编辑器。我们建议使用您最喜欢的编辑器查看 GitHub 存储库。我们的建议包括以下内容:

  • Visual Studio(最好是最新版本)
  • Visual Studio Code
  • JetBrains Rider

本章的代码位于 Packt Publishing 的 GitHub 存储库中,网址为 https://github.com/PacktPublishing/ASP.NET-Core-8-Best-Practices。

性能为何重要

在 Web 开发中,性能有多种形式,因为有太多的组件来确保网站始终可供用户使用。作为开发人员,如果有人就网站速度慢的问题寻求帮助,您会推荐什么建议?如果不检查网站,很难口头回答这个问题。对于网站而言,有时性能可能不只是一种技术;问题可能不止一个瓶颈。

例如,在浏览器中加载网页时,您是否看到内容出现,但图像需要很长时间才能逐行绘制?访问数据库怎么样?您是否遇到过查询速度慢的情况,服务器需要一分钟才能检索记录?Web API 每次请求的执行时间是否超过两秒?

如您所见,性能是对整个网站的分析,包括浏览器、服务器、C#、API 和数据库。

亚马逊发布了一项研究,计算结果表明,如果其网站上的页面加载速度减慢 1 秒,可能会损失 16 亿美元的销售额。

一秒钟如何让亚马逊损失 16 亿美元的销售额

这项研究由 Fast Company 报道,网址为 https://fastcompany.com/1825005/how-one-second-could-cost-amazon-16-billion-sales。

虽然这很引人注目,但最近有一篇文章介绍了 Netflix 如何用纯 JavaScript(通常称为 Vanilla JavaScript)取代 React。这大大提高了性能。在案例研究中,它报告说一个页面有 300 KB 的 JavaScript,这很多。但是,与其他网站(如 CNN.com(4.4 MB 的 JavaScript)和 USAToday.com(1.5 MB 的 JavaScript)相比,300 KB 的 JavaScript 被认为是最少的。

Netflix Web 性能案例研究

Google Chrome 工程主管 Addy Osmani 撰写了一篇关于 Netflix 通过优化获得的性能提升的文章。案例研究可在 https://medium.com/dev-channel/a-netflix-web-performance-case-study-c0bcde26a9d9 找到。

通过这些特定的场景和案例研究,许多公司注意到了这一点,并主要关注性能。甚至微软也将其结果提交给 TechEmpower 的行业框架基准测试结果,将其努力集中在性能上。由于不断改进,ASP.NET 现在被评为最快的 Web 平台之一。

TECHEMPOWER 框架基准测试结果

每年,TechEmpower 都会在图表中更新其结果,该图表可在 https://techempower.com/benchmarks/ 找到。截至 2022 年 7 月 19 日,ASP.NET 在性能方面排名第 9。

最后,由于 Google 是搜索引擎行业最强大的参与者,它将页面加载速度与搜索引擎结果页面SERP)联系起来。翻译:您的网站速度是您在搜索结果中排名靠前的因素之一(我们将在下一节中讨论)。GOOGLE USING 网站速度在网络搜索排名中的作用

在 Google 的博客上,他们提到页面速度是网站排名时要考虑的另一个因素。该帖子位于 https://developers.google.com/search/blog/2010/04/using-site-speed-in-web-search-ranking。

性能是我最喜欢的话题之一。通过小改动实现大幅性能提升的想法绝对令人兴奋。它也可以在视觉上显现出来。本章旨在帮助使用技术和工具来识别任何网站上的性能问题。

好消息是,在之前的章节中,我们已经提到了提高性能的具体方法,我们将在相关时参考这些方法。正如我在 [第 4 章] 中所说,在构建 ASP.NET Web 应用程序时,性能应该是首要考虑因素,安全性紧随其后。

正如您将在本章的某些部分中看到的那样,性能一直是艺术与科学的结合。有 感知 性能,然后有 实际 性能。

实际性能是活动或任务立即响应并通知用户已完成时的测量值。立即响应是一个目标。感知性能是一种主观测量,即使活动或任务不是很快,用户也会感觉它很快。感知性能的一个例子是用户请求网页并且浏览器立即呈现页面。内容在后台继续加载,同时通过允许用户滚动页面等待其他内容来保持用户的注意力。因此,用户认为该网站“很快”,因为它立即响应。微调器和进度条是在处理某些内容时实现感知性能的其他方法。

虽然感知性能是在等待进程完成时转移用户注意力的一种方法,但本章将更多地关注实际性能。

在下一节中,我们将学习如何使用公共 Web 工具和特定服务器工具(如 Visual Studio Performance ProfilerBenchmark.netApplication Insights)为客户端和服务器端代码创建基线。

建立基准

那么,您如何知道网站速度变慢了?是因为最近发布了软件产品,还是安装了新的 NuGet 包导致速度变慢?

在确定问题时,您可能会问自己,“发生了什么变化?”但每个人都应该问的问题是“如何衡量性能?”为了衡量性能,需要有一个性能预期基准。

应用程序的每个部分都应包括性能测试。无论是前端、C# 子系统、Web API 还是数据库,都应建立适当的系统,以便在系统性能未达到预期时通知团队。

使用客户端工具

客户端的问题主要是由于加载时间、未找到资源的交付(例如 HTML 页面、图像、CSS 和 JavaScript)或一般的 JavaScript 错误。但是,这并不意味着整个问题都在客户端上。

在开发过程中,应通过测试工具(如 CypressSelenium)为客户端代码创建基准,并记录测试持续时间。将最新场景与之前的测试结果进行比较,以查看时间差异。

确定基准的另一种方法是使用网络上的各种工具,例如本节中列出的工具。将这些工具视为将您的汽车送到机械师那里进行维修。这些工具会扫描您的公共网站,分析网站的各个方面,并提供有关如何修复发现的每个问题的报告。

一些可让您深入理解网站性能的工具包括:

  • Google PageSpeed Insights (https://pagespeed.web.dev):Google 使用其搜索引擎对您的网站进行排名,并提供出色的工具来帮助解决网站问题。
  • Lighthouse (https://developer.chrome.com/docs/lighthouse/):如果您的网站无法通过这些工具之一公开访问以进行分析,则可以使用 Lighthouse 扩展程序在内部对网站进行测试。Lighthouse 会生成一份完整的报告,其中包含有关如何提高网站性能的建议。
  • GTMetrix (https://gtmetrix.com):GTMetrix 是我多年来一直在使用的工具,它每年都在不断改进,令人印象深刻。它提供了性能摘要、速度可视化和建议。
  • Google Search Console (https://search.google.com/search-console):Google 创建此工具是为了帮助网站管理员识别性能问题,同时还提供其他常规维护工具,例如人们在 Google 中输入什么来查找您的网站。
  • DevTools:DevTools 是位于 Google Chrome、Mozilla Firefox、Apple Safari 和 Microsoft Edge 中的 Web 开发人员工具面板,可帮助 Web 开发人员分析网页,它正在成为互联网的 IDE。在浏览器中按 F12 将打开该面板。
    这些工具非常适合衡量您的网站在互联网上的表现以及基于上次修订的表现。如果您的上一个版本需要 0.5 秒才能加载,而最新版本现在需要 3 秒,那么是时候检查发生了什么变化了。除了通过在部署站点之前报告性能问题来自动化该过程之外,还有什么更好的方法来检查这一点?

使用服务器端工具

使用 ASP.NET,使用许多可用的工具,为您的代码创建基准同样容易。

在本节中,我们将回顾一些可用于为您的代码创建基准的工具,例如 Visual Studio、Benchmark.net、Application Insights 和其他工具,例如 NDepend。

Visual Studio 性能工具

由于 Visual Studio 是业界可靠的 IDE,因此衡量 C# 性能的能力变得越来越普遍,因为如果代码运行缓慢,开发人员希望找到一种方法来定位瓶颈。

图 10.1 – Visual Studio 2022 中的性能分析器

在这里插入图片描述

启动性能分析器时,您会看到一个选项列表:

图 10.2 – 运行性能分析器之前的可用选项列表

在这里插入图片描述

如您所见,有大量选项跨越多个接触点。例如,有一个数据库选项可以查看您的查询在应用程序中的执行情况。

数据库的指标类似于实体框架详细信息,解释了执行查询所需的时间。另一个选项是确定异步/等待问题可能发生的位置以及内存使用情况和对象分配。

Benchmark.net

如果需要测试较小的、自包含的方法,那么最好的微基准测试工具之一就是 Benchmark.net (https://benchmarkdotnet.org/)。

Benchmark.net 采用特定方法并在不同场景下对其进行测试。但有一个需要注意的地方,即 Benchmark 项目 必须 是控制台应用程序。

例如,如果我们想测试一个古老的争论,即字符串连接和 StringBuilder 类哪个更快,我们会编写两个基准测试来确定哪个更快,如下所示:

public class Benchmarks
{
    [Benchmark(Baseline = true)]
    public void StringConcatenationScenario()
    {
        var input = string.Empty;
        for (int i = 0; i < 10000; i++)
        {
            input += «a»;
        }
    }
    [Benchmark]
    public void StringBuilderScenario()
    {
        var input = new StringBuilder();
        for (int i = 0; i < 10000; i++)
        {
            input.Append(«a»);
        }
    }
}

在前面的代码中,我们在一个场景中创建了一个字符串,在另一个场景中创建了一个 StringBuilder() 类的实例。为了实现相同的目标,我们添加了 10,000 个“a”并开始基准测试。

根据图 10.3 中的结果,显然使用 StringBuilder() 执行大型字符串连接是最佳选择:

图 10.3 – 比较字符串连接与 StringBuilder() 类的性能

在这里插入图片描述

关于创建基准,我们在第一个场景中向 [Benchmark] 属性添加了一个附加参数,称为 Baseline,并将其设置为 true。这告诉 Benchmark.net 在测量其他方法的性能时将其用作基准。您可以使用任意数量的方法来获得相同的结果,但所有内容都将与 Benchmark 属性中 Baseline=true 的方法进行比较。

对于小型、紧凑的方法,Benchmark.net 绝对是一款出色的工具,它能够深入理解使用微优化创建更快代码的方法。

Application Insights

Microsoft 的 Application Insights 旨在成为一种通用分析工具,用于收集有关应用程序执行的所有操作的遥测数据。设置后,Application Insights 可以收集以下数据:

  • 请求 - 网页和 API 调用
  • 依赖项 - 应用程序在后台加载什么?
  • 异常 - 应用程序抛出的每个异常
  • 性能计数器 - 自动识别减速
  • 心跳 - 应用程序是否仍在运行?
  • 日志 - 用于收集应用程序所有类型日志的集中位置

添加 Application Insights 时,Application Insights 确实需要 Azure 订阅。

应用程序洞察其他材料

有多种设置 Application Insights 的方法,本章无法一一介绍。有关 Application Insights 的更多信息以及如何为您的应用程序设置它,请导航至 https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core。

创建基线和识别瓶颈的其他一些建议包括:

  • JetBrains dotTrace/dotMemory – dotTrace 是一种性能分析器工具,dotMemory 是一种内存分析器工具。两者都是出色的工具,可以深入研究应用程序的性能。dotTrace 和 dotMemory 使您能够将结果基线与另一组结果进行比较(“比较快照”)。
  • RedGate ANTS 性能分析器/内存分析器 – ANTS 性能和内存分析器能够分析 .NET 代码和内存分配,在代码运行时进行深入分析时,它展示了类似的性能和内存分析方法。
  • NDepend – 第一次针对代码运行 NDepend 时,您会立即创建代码的基线。下次运行 NDepend 时,它会将您的基线与您编写的新代码进行比较,例如,可以提供有关“引发过多异常”或循环复杂度(即您的代码基于条件分支的复杂程度,例如 if…else 或 switch)的数据。这些也可以由用户定义,以满足您的代码质量要求,使用 Code Query for LINQ (CQLinq)。NDepend 还具有集成到您的 CI/CD 管道中以自动化该过程的功能。
  • 自行构建指标 - 在 [第 7 章],我们解释了如何“识别缓慢的集成测试”。使用单元、集成和 API 上的诊断秒表,您甚至可以在发布版本之前执行并报告这些指标。

当这些工具检查您的应用程序时,它们会通过查找热点来分析如何优化代码。 如果热点被调用的次数足够多,您的应用程序的性能就会受到影响。 到达此热点的路径称为 热路径

数据库

虽然您可以使用数据库创建基线,但大多数优化都是在数据库级别通过分析存储过程、索引管理和架构定义完成的。 每种数据库类型都有自己的性能工具来查找瓶颈。 现在,我们将特别关注 SQL Server 工具。

SQL Server Management Studio (SSMS) Profiler

通过使用分析器界面的 SSMS,开发人员能够确定特定的即席查询、存储过程或表是否未按预期执行。

SQL Server Profiler 位于 工具 选项下,作为第一个菜单项,如图 10.4 所示:

图 10.4 – SSMS 中的 SQL Server Profiler

在这里插入图片描述

在运行 SQL Server Profiler 时,会记录发送到数据库的每个请求以及它花费的时间、需要多少次读写以及返回的结果。

查询存储

SQL Server 2016 的最新功能之一是 查询存储。查询存储为您提供了有关如何提高 SQL Server 性能的见解。

一旦启用(右键单击数据库 | 属性 | 查询存储 | 操作模式:开启),它将开始分析您主动使用的 SQL Server 工作负载并就如何提高性能提出建议。

收集数据后,可以使用存储过程获取指标,以识别性能较慢的查询。

查询存储其他材料

有关 Microsoft 查询存储的其他材料,请导航至 https://learn.microsoft.com/en-us/sql/relational-databases/performance/manage-the-query-store。如需使用查询存储进行性能调整,请导航至 https://learn.microsoft.com/en-us/sql/relational-databases/performance/tune-performance-with-the-query-store。

在本节中,我们介绍了建立基准的重要性,同时列出了用于衡量性能的各种客户端工具,例如 Google Page Speed Insights、Lighthouse、GTMetrix、Google Search Console 和 Chrome DevTools。我们还研究了用于识别代码库问题的服务器端工具,例如 Visual Studio Performance Profiler、Benchmark.net、Application Insights、JetBrains dotMemory 和 dotTrace、RedGate ANTS Performance Profiler/Memory Profiler 和 NDepend。对于数据库,我们提到了两种用于识别性能瓶颈的工具:SQL Server Management Studio Profiler 和 Query Store。我们还提到了热点或热路径,在这些热点或热路径中,频繁调用的未优化代码可能会导致应用程序出现性能问题。

下一节将介绍一些客户端和服务器端技术的最佳实践,但主要关注使用 C# 的服务器端优化。

应用性能最佳实践

正如本章开头所述,本章中的内容适用于客户端服务器技术,以充分利用您的 ASP.NET 网站。

在本节中,我们将首先关注通过应用图像优化、最小化请求、使用 CDN 和其他技巧来优化客户端,以提高客户端性能。然后,我们将重点介绍服务器端技术,例如优化 HTML、缓存和 Entity Framework Core 性能技术以及识别慢速查询。

优化客户端性能

在本节中,我们将理解图像优化、识别 Google 的 Core Web Vitals 指标、在适用时使用 CDN、如何最小化请求以及在何处放置脚本和样式。Fixing Image Optimization

根据 Web Almanac (https://almanac.httparchive.org/en/2022/media#bytesizes),图像优化是网络上最严重的问题之一。支持它所需的设备数量并没有让这变得更容易。让我们看看如何优化这种体验。

以下是 标签的基本用法:

<img width="100" height="100"
     src="/images/logo.jpg"
     alt="Buck's Coffee Shop Logo"

However, for responsive layouts, the tag has an srcset attribute:

<img src="/images/logo-400.jpg" 
     alt="Buck›s Coffee Shop Logo" 
     width="100"
     height="100" 
     loading="lazy"
     srcset="logo-400.jpg 400w, 
             logo-800.jpg 800w, 
             logo-1024.jpg 1024w"
     sizes="(max-width: 640px) 400px, 800px, 1024px">

上述代码确定了视口(网页)的大小并加载了相应的图像。max-width 媒体条件表示,如果视口为 640px,则使用 400px 图像。如果 max-width 大于 640px 且小于 800px,则使用 800px 图像。

这样,您就可以支持多种不同的响应式布局。定义站点布局后,图像也应与布局大小相辅相成。这是什么意思?对于每幅图像,您都应该为每个响应式布局创建一个图像。例如,前面的默认图像徽标应该有三幅图像:logo-400.jpglogo-800.jpglogo-1024.jpg

此外,loading=“lazy” 告诉浏览器推迟加载图像,直到浏览器可以确定视口的大小以显示正确的图像。

最后,图像可能会变得非常大,并且可能包含编码数据,例如拍摄照片时的 GPS 数据。压缩图像是删除多余数据的过程,使图像更小,并在浏览器中加载更快。这是一项服务器端任务,可以作为一项任务包含在客户端任务运行器中。

图像优化的最低步骤应如下所示:

  1. 确定网站的响应式布局 – 确定图像所需的尺寸(400px、800px 等)

  2. 根据布局创建图像 – 每种布局尺寸都应有一个调整大小的图像。

  3. 优化图像 – 对于网站上的每张图片,通过删除附加到每张图片的额外数据,压缩图像使其更小并加载更快。使用图像服务,例如 Optimazilla (https://imagecompressor.com/) 或 TinyPNG (https://tinypng.com/)。

  4. 添加新的 属性 – 每个图像标签都应包含 srcsetsizes 属性,以便浏览器可以根据视口大小确定要显示的最佳图像。

图像优化是一个太大的话题,无法用一小章来描述,但这个快速概述足以为网站用户提供更好的体验。

最小化请求

上述用于创建基线的大多数客户端工具都可用于识别对资源发出的多个请求。平均而言,网站有 58 个 JavaScript 和 CSS 请求(计算图像)。每个请求都会导致延迟,并且根据资源的不同,它会比用户愿意等待的时间更长。

我们已经在 [第 6 章]中学习了如何在构建 JavaScript 和 CSS 时使用更好的方法,从而消除大量对 JavaScript 和 CSS 文件的请求。

最后,如果有大量大小一致的图像,并且您要单独调用每个图像,那么更好的方法是创建一个包含所有图像的大图像(精灵表)并使用 CSS 显示它们。您的浏览器无需请求 15 个社交网络徽标,只需调用一个图像并使用 CSS 将它们拆分出来,如图 10.5 所示:

图 10.5 – 32x32 社交网络图标的精灵表

在这里插入图片描述

要使用此精灵表,CSS 将如下所示:

.bg-YouTube_32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -1px -1px;
}
.bg-facebook_32x32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -35px -1px;
}
.bg-github_32x32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -1px -35px;
}
.bg-Instagram_32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -35px -35px;
}
.bg-LinkedIn_32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -69px -1px;
}
.bg-quora_32x32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -69px -35px;
}
.bg-RSS_32x32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -1px -69px;
}
.bg-Twitter_32 {
    width: 32px; height: 32px;
    background: url('css_sprites.png') -35px -69px;
}

背景使用偏移顶部和左侧位置来标识要使用哪个图像作为背景。要在 HTML 中显示 RSS 图标,它将按以下方式呈现:

<div class="bg-RSS_32x32"></div>

此类创建精灵的服务包括 CodeShack 的图像到精灵表生成器 (https://codeshack.io/images-sprite-sheet-generator/) 和 Toptal 的 CSS 精灵生成器 (https://www.toptal.com/developers/css/sprite-generator)。

使用 CDN

如果站点使用大量静态文件,则使用 内容交付网络 (CDN) 可提供基于位置交付内容的急需服务。这些基于地理位置的服务器会缓存文件,以便根据用户所在的位置更快地交付文件。

例如,如果加利福尼亚州的人从内华达州请求文件,那么比从英格兰请求文件的人更快。内容越近,用户收到的速度就越快。

关于客户端性能的最终想法

虽然我们可以介绍大量的客户端技巧,但让我们用一些关于如何让客户端更快的最终想法来结束本节:

  • 脚本在底部,样式在顶部 – 避免将脚本放在标题中,但绝对要将样式放在标题中。将脚本放在底部确认文档对象模型 (DOM) 已完全加载,如果立即执行,JavaScript 能够找到 DOM 元素,因为它们已经被渲染。
  • 将 Google 的核心 Web 指标应用到您的网站 – 如果您使用 Lighthouse 或 Google 的 Page Speed Insights,您会注意到以下用于识别您网站性能的首字母缩略词:FCP(首次内容绘制)、LCP(最大内容绘制)、CLS(累积布局偏移)和 FID(首次输入延迟)。查看 https://web.dev/vitals 上的这些术语,为您的用户提供更好的网络体验。
  • 用 HTML 替换 JavaScript – 有时,使用简单的 HTML 比加载完整的 JavaScript 框架更好。例如,如果您需要手风琴,/ HTML 标记可能就足够了。此外,浏览器变得越来越现代化,并出现了新的标记,例如 标记,其中 JavaScript 不是必需的。请参阅 https://caniuse.com/ 理解浏览器支持。

在本节中,您学习了如何通过优化图像来优化客户端,以及 CDN 如何改善静态内容的加载以及如何最小化请求以降低延迟问题。作为最后的笔记,我们研究了一些技巧,例如将脚本放在底部并将样式放在顶部,将 Google 的核心 Web Vitals 应用于网站,提供无论设备如何都响应的网站,以及在有意义的地方使用 HTML 而不是 JavaScript。

在下一节中,我们将把重点从客户端转移到服务器端,并研究优化 C# 和 Entity Framework Core 时的一些常见做法。

常见的服务器端实践

由于 C# 是一种非常强大的语言,因此有很多方法可以创建 Web 应用程序。正如您在 [第 5 章] 中看到的 Entity Framework Core,每种设计模式都满足特定需求,但无论哪种模式,其工作方式都相同。这些性能技术的好消息是,它们适用于行业中已经使用的 Web 标准和设计模式。ETag 就是其中一个例子。它们一度被视为需要特定代码的独立 Web 概念。现在,当使用静态文件时,这些 ETag 无需任何额外编码即可集成到网站中。它们被视为浏览器的 Web 标准。

本节将讨论如何通过将这些 Web 标准和设计模式添加到我们自己的 Web 应用程序中来提高性能,从而使其运行速度更快。

在本节中,我们将理解可以使用 C# 应用于代码的各种性能增强,包括可以立即应用于您自己的网站的快速性能提升,我们还将学习如何添加中间件组件来优化 HTML、仅用四个字母提高 Entity Framework Core 性能以及识别速度缓慢的 Entity Framework Core 查询。

应用快速性能提升

虽然其中一些快速提升是众所周知的(有些已在前几章中介绍过),但回顾一下它们以从您的网站中获得最佳性能并没有什么坏处:

  • 关闭调试 – 当您使用调试模式运行应用程序时,其他信息会被编译到每个程序集中以用于调试目的。当更改为发布模式时,您将获得用于部署的程序集的优化版本。
  • 使用 async/await – 如前几章所述,使用 async/await 可提高性能,应将其用于涉及文件 I/O、数据库和 API 调用的任务。
  • 使用数据库 – 使用 Entity Framework Core 时,尝试评估目标并评估最佳方法:它是 Entity Framework Core 简单数据访问方法还是存储过程可以提供更快的性能。
  • 使用 .AsNoTracking() – 如 [第 5 章]中所述,如果您有一个不需要 ChangeState 管理的 Entity Framework 查询(例如更新实体),请使用 .AsNoTracking() 来减少 Entity Framework 开销。

虽然这些是快速提升 Web 应用程序性能的一些技巧,但我们现在准备深入研究更复杂的基于代码的技术。

优化 HTML

由于我们理解了优化图像(在上一节中)以及优化 JavaScript 和 CSS,我们现在需要关注 其他 客户端资源:HTML。

当您在浏览器中“查看源代码”时,您希望看到每个人都能理解的格式精美的文档。但是当浏览器收到此文档时,它并不关心它有多大,甚至不关心它有多“漂亮”。浏览器只是解析并呈现传入的 HTML。

您是否注意到为了格式化而浪费了多少空间?例如,让我们加载“Buck’s Coffee Shop”网页。

在 Chrome DevTools 的 Network 选项卡中,我们看到它的大小为 4.1 KB:

图 10.6 – Buck’s Coffee Shop 的近似大小(带空格)(4.1 KB)

在这里插入图片描述

既然浏览器不关心,如果我们可以减小 HTML 的大小,不是更好吗?

中间件可以协助完成此操作。如果我们使用 [第 2 章]中的标准中间件模板,我们可以创建一个 HtmlShrink 组件:

public class HtmlShrinkMiddleware
{
    private readonly RequestDelegate _next;
    public HtmlShrinkMiddleware(RequestDelegate next) => _next = next;
    public async Task InvokeAsync(HttpContext context)
    {
        using var buffer = new MemoryStream();
        // 用我们的缓冲区替换上下文响应
        var stream = context.Response.Body;
        context.Response.Body = buffer;
        // 如果有任何其他中间件组件,则调用管道的其余部分
        await _next(context);
        // 重置并读出内容
        buffer.Seek(0, SeekOrigin.Begin);
        // 调整响应流以删除空格。
        var compressedHtmlStream = new HtmlShrinkStream(stream);
        // 再次重置流
        buffer.Seek(0, SeekOrigin.Begin);
        // 将我们的内容复制到原始流并放回
        await buffer.CopyToAsync(compressedHtmlStream);
        context.Response.Body = compressedHtmlStream;
    }
}
public static class HtmlShrinkMiddlewareExtensions
{
    public static IApplicationBuilder UseHtmlShrink(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<HtmlShrinkMiddleware>();
    }
}

上述代码包含我们熟悉的中间件框架。我们的 HtmlShrinkMiddleware 组件现在实例化一个 HtmlShrinkStream 类来执行压缩,删除 HTML 中的任何空格。此外,我们在代码底部创建了标准扩展。

我们的 HtmlShrinkStream 类如下所示:

public class HtmlShrinkStream: Stream
{
    private readonly Stream _responseStream;
    public HtmlShrinkStream(Stream responseStream)
    {
        ArgumentNullException.ThrowIfNull(responseStream);
        _responseStream = responseStream;
    }
    public override bool CanRead => _responseStream.CanRead;
    public override bool CanSeek => _responseStream.CanSeek;
    public override bool CanWrite => _responseStream.CanWrite;
    public override long Length => _responseStream.Length;
    public override long Position
    {
        get => _responseStream.Position;
        set => _responseStream.Position = value;
    }

    public override void Flush() => _responseStream.Flush();

    public override int Read(byte[] buffer, int offset, int count) =>
        _responseStream.Read(buffer, offset, count);

    public override long Seek(long offset, SeekOrigin origin) =>
        _responseStream.Seek(offset, origin);

    public override void SetLength(long value) =>
        _responseStream.SetLength(value);

    public override void Write(byte[] buffer, int offset, int count)
    {
        var html = Encoding.UTF8.GetString(buffer, offset, count);
        var removeSpaces = new Regex(@"(?<=\s)\s+(?![^<>]*</pre>)", RegexOptions.Multiline);
        html = removeSpaces.Replace(html, string.Empty);
        var removeCrLf = new Regex(@"(\r\n|\r|\n)", RegexOptions.Multiline);
        html = removeCrLf.Replace(html, string.Empty);
        buffer = Encoding.UTF8.GetBytes(html);
        _responseStream.WriteAsync(buffer, 0, buffer.Length);
    }
}

在我们的 HtmlShrinkStream 类中,我们的工作重点集中在 Write() 方法上。我们查看收到的缓冲区,将其转换为 HTML 字符串,使用 RegEx 替换所有空格,最后将 缓冲区 写入 responseStream

现在,我们可以将以下行添加到我们的 Program.cs 文件,将 HtmlShrink 中间件扩展添加到我们的管道中:

app.UseHtmlShrink();

添加后,浏览器中收到的任何 HTML 都将被删除任何空格。如果我们查看 Buck’s Coffee Shop 主页,我们可以看到一切正常,但如果我们查看源代码,我们可以看到一切都更加紧凑:

图 10.7 – 查看 Buck’s Coffee Shop 主页的源代码

在这里插入图片描述

它可能看起来不太好看,但如果我们查看 Chrome DevTools 中的 Network 选项卡,我们可以看到发送到浏览器的内容有所不同:

图 10.8 – Buck’s Coffee Shop 主页不带空格的大小(3.3 KB)

在这里插入图片描述

这比原始大小小了近 20%。

启用 DbContext 池

连接池是为多个用户重用连接的能力。默认情况下,数据库连接已经通过 SqlConnection 使用连接池。此概念已应用于 Entity Framework Core 的 DbContext

如果 Web 应用程序大量使用 Entity Framework Core,则您希望获得最佳性能。只需更新中间件 DbContext 连接即可。

例如,我的中间件中可能有以下行:

services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));

我们可以通过在此行中添加四个字母来立即提高我们的性能:

services.AddDbContextPool<MyDbContext>(options =>options.UseSqlServer(connectionString));

使用 AddDbContextPool<>() 方法包含相同的语法,但在 DbContext 完成后,它将重置其状态并将其存储起来,以备以后需要 DbContext 的新实例时使用。我们正在回收我们的 DbContext

根据您的 DbContext 的大小,每次创建新实例时,DbContext 的创建都需要时间。使用 .AddDbContextPool<>() 方法可以提高我们所需的性能。

ENTITY FRAMEWORK CORE DBCONTEXT POOLING 基准测试

Microsoft 执行了有和没有 DbContext 池的基准测试。实施 DbContext 池后,性能提高了 50% 以上。Microsoft 甚至将源代码包含在基准测试代码中。结果可在 https://learn.microsoft.com/en-us/ef/core/performance/advanced-performance-topics#benchmarks 中找到。

识别慢查询

识别慢查询的能力有时很困难,因为我们在 Visual Studio 中,可能看不到向数据库发送查询时后台发生的情况。那么,我们如何在 Web 应用程序中找到这些慢查询?

在 DbContext 的 OnConfiguring() 方法中,将 .LogTo() 方法添加到您的 DbContextOptionsBuilder,您将看到每个数据库调用及其执行所花费的时间:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        var connString = _configuration.GetConnectionString(«DefaultConnection»);
        if (!string.IsNullOrEmpty(connString))
        {
            optionsBuilder.UseSqlServer(connString)
                .LogTo(Console.WriteLine, LogLevel.Information);
        }
    }
}

.LogTo() 方法将产生以下日志条目:

Microsoft.EntityFrameworkCore.Database.Command: Information: Executed DbCommand (46ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [a].[ID], [a].[LocationID], [a].[Name], [l].[ID], [l].[Name]
FROM [Attractions] AS [a]
INNER JOIN [Locations] AS [l] ON [a].[LocationID] = [l].[ID]

对于此特定查询,执行耗时 46ms.LogTo() 方法提供了一种简单的方法来识别查询是否已发挥其最大能力或是否可能需要优化。

在本节中,我们学习了一些简短的优化以及用于缩小 HTML 的新中间件、如何使用 DbContext 池加速 Entity Framework Core 以及如何在整个应用程序中定位慢查询。

在下一节中,我们将重点介绍各种类型的缓存以及每种缓存的不同之处以及如何协同工作以提高应用程序的整体性能。

理解缓存

由于缓存对 Web 应用程序极为重要,因此它应该有自己的部分来介绍所有可能的缓存类型。业内有句俗语:“最好的数据库调用就是根本不调用。”他们可能指的是缓存。

在本节中,我们将学习不同类型的缓存,包括响应和输出缓存、数据缓存和缓存静态文件。

使用响应缓存和输出缓存

无论是调用网页还是 API,缓存数据的能力都非常重要。实施简单的缓存策略以立即返回数据是有效的。

ResponseCaching 是一个中间件扩展,最适合来自客户端的 GET 或 HEAD API 请求。在使用响应缓存时,.NET 使用标准 HTTP 缓存语义。

RFC 9111:HTTP 缓存

有关 HTTP 缓存的其他材料,请导航至 https://www.rfc-editor.org/rfc/rfc9111。

要添加响应缓存,构建器必须将其添加到服务中,并且应用程序 (app) 必须“使用”它,如下所示:

Var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseHttpsRedirection();
//如果使用 Cors,则必须将 UseCors 放在 UseResponseCaching 之前
// app.UseCors();
app.UseResponseCaching();

一旦到位,任何 API 调用都会默认提供来自浏览器的缓存数据。

RESPONSECACHING 中间件

有关 ResponseCaching 的更多详细信息,请访问 https://learn.microsoft.com/en-us/aspnet/core/performance/caching/middleware。

但是,对于大多数 Web UI(例如 Razor Pages),OutputCaching 是更好的选择,因为浏览器会设置请求标头以防止缓存。OutputCaching 的配置类似于 ResponseCaching,如下所示:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOutputCache();
// 将服务添加到容器中。
builder.Services.AddRazorPages();
var app = builder.Build();
// 配置 HTTP 请求管道。
If (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // 默认 HSTS 值为 30 天。您可能需要针对生产场景更改此值,请参阅 https://aka.ms/aspnetcore-hsts。
    App.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// 如果使用 Cors,则 UseOutputCache 必须放在 useCors() 之后。
//app.UseCors();
app.UseOutputCache();

在中间件配置中,我们将 AddOutputCache() 方法添加到服务集合,并将 UseOutputCache() 方法放在 UseRouting() 方法之后,如果使用,则放在 UseCors() 方法之后。

OutputCache 添加到中间件时,并不意味着我们会自动缓存 UI 页面。我们还需要通过向 Razor Page 类添加 [OutputCache] 属性来识别缓存了哪些页面:

[OutputCache]
public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }
    public void OnGet() { }
}

如果属性中未定义任何参数,则缓存页面的默认策略如下:

  • 缓存 HTTP 200 状态代码
  • 缓存 HTTP GET 或 HEAD 请求
  • 缓存设置了 cookie 的响应
  • 缓存对经过身份验证的请求的响应

响应缓存用于在客户端或通过浏览器进行缓存,而输出缓存则缓存在服务器上。如果两个用户从两个不同的浏览器访问同一页面,响应缓存将无济于事,因为每个浏览器都会在每个浏览器中缓存页面。但是,如果实施了输出缓存,则会在服务器上缓存页面并快速将页面传递给两个用户。

如果将页面缓存与数据缓存结合使用,则可以为用户提供更好的体验,我们将在下文中讨论这一点。

实施数据缓存

当用户访问网站时,系统会根据用户身份向他们显示一定量的数据。例如,当第一个用户访问博客时,他们可能会看到与下一个访问该网站的访问者相同的数据。如果数据不经常更改,那么一路返回数据库再次检索相同数据就没有意义了。数据缓存可以帮助我们解决这个问题。数据缓存是获取常用数据并将其存储一段时间。

让我们看一个例子来展示这种方法。由于我们使用的是 Entity Framework Core,我们将有一个现有的服务(CoffeeService),其中包含一个简单的 .GetAll() 方法,用于返回所有咖啡。我们可以围绕该服务包装一个名为 CacheCoffeeService 的新缓存类,如下所示:

public class CacheCoffeeService : CoffeeService, ICachedCoffeeService
{
    private const string keyCoffeeList = «EntireCoffeeList»;
    private readonly IMemoryCache _cache;
    public CacheCoffeeService(IBucksDbContext dbContext,
                              IMemoryCache cache)
        : base(dbContext)
        {
            _cache = cache;
        }
    public List<Coffee> GetAll(bool reload = false)
    {
        //如果我们在缓存中找不到它或者想要重新加载...
        if (!_cache.TryGetValue(keyCoffeeList, out List<Coffee>         coffees) || reload)
        {
            coffees = base.GetAll();
            _cache.Set(keyCoffeeList, coffees,
                       new MemoryCacheEntryOptions()
                       .SetSlidingExpiration(TimeSpan.FromSeconds(60))                     // 1min
                       .SetAbsoluteExpiration(TimeSpan.FromSeconds(3600))                     // 6min
                       .SetPriority(CacheItemPriority.Normal)
                      );
        }
        return coffees;
    }
}
public interface ICachedCoffeeService
{
    List<Coffee> GetAll(bool reload = false);
}

CacheCoffeeService 继承自 CoffeeService 并使用 ICachedCoffeeService 接口。ICachedCoffeeService 接口应该与 CoffeeService 完全相同,除了一个小细节:每次调用时都会添加一个默认为 false 的 reload 参数。

如果我们在缓存中找不到整个咖啡列表,或者我们决定要重新加载整个咖啡列表,我们会调用基类 (CoffeeService.GetAll())),将新列表保存到缓存中,并返回整个列表。

默认情况下,当您调用不带任何参数的 CachedCoffeeService.GetAll() 时,您将获得列表的缓存版本。将 true 传递给 .GetAll(),您将刷新缓存并收到最新的咖啡列表。

这种方法的好处是将缓存层与标准数据访问相结合,让我们可以两全其美。创建这些数据缓存时,其好处显而易见:使用内存作为数据库可提高性能,这是线程安全的。但是,请注意在缓存中存储了多少表或多少数据。

虽然使用内存作为数据库似乎是一种权衡,但另一种缓存选项是使用分布式缓存。分布式缓存是跨多个应用服务器共享的缓存,具有以下优点:

  • 它与服务器之间的请求保持一致/可感知
  • 如果服务器断电,缓存的数据将保留
  • 如上所述,分布式缓存不使用本地内存

数据缓存的最佳候选对象包括小型查找表(< 100 条记录)和很少访问的表数据。

缓存静态文件

由于所有这些静态文件(如图像、CSS 和 JavaScript)都可用于我们的 Web 应用程序,因此您会认为也有办法缓存这些文件。

.UseStaticFiles() 方法中,有一个包含 HttpContext 的 context 参数,因此我们可以使用响应对象来更改静态文件的缓存控制标头:

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // 缓存24小时。
        var response = ctx.Context.Response;
        var duration = 60 * 60 * 24; // 24h duration.
        response.Headers[HeaderNames.CacheControl] =
        "public,max-age="+duration;
    }
});

上述代码采用了我们的静态文件中间件组件,并允许使用 StaticFileOptions 实例,该实例还具有可供我们使用的 OnPrepareResponse 事件。对于缓存持续时间,我们将每个静态文件标头的缓存持续时间设置为 24 小时。

如果我们想禁用缓存,我们可以修改响应以更改以下标头:

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        var response = ctx.Context.Response;
        // disable all caching
        response.Headers[HeaderNames.CacheControl] = "no-cache, no-store";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
    }
});

上述代码示例禁用了每个静态文件的缓存。

再次强调,虽然这些文件缓存在服务器的本地内存中,但请记住,断电时缓存也会断电。

如果您想要缓存某个文件夹或文件类型,ctx 参数不仅包含 HttpContext 类型的 Context 属性,还包含 File 属性,该属性包含包含 FileInfo 数据的 IFileInfo 类型。

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0neKing2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值