资讯动态

ASP.NET Core性能优化实战:从请求管道到GC调优的系统性指南

发布时间:2026/9/1 23:31:36 来源:尧图企业网站定制
你的 ASP.NET Core 应用是不是在用户量稍微上来一点后响应就开始变慢CPU 和内存占用却居高不下你尝试过加缓存、优化 SQL但效果总是不尽如人意性能瓶颈像打地鼠一样解决一个又冒出一个。很多开发者对 ASP.NET Core 性能优化的理解还停留在“加个缓存”、“用异步”的层面。但真正的性能问题往往隐藏在框架的默认配置、不合理的中间件管道、被忽略的垃圾回收策略甚至是启动时的 JIT 编译过程中。“ASP.NET Core 性能优化 B1218”这个项目其核心价值不在于提供一个万能优化清单而在于揭示一套从请求入口到响应出口、从应用启动到运行时监控的系统性优化思维框架。它告诉你优化不是零散的技巧堆砌而是一场有章法的“外科手术”。本文将带你深入 ASP.NET Core 的内部从 HTTP 请求的生命周期出发拆解每一个可能成为瓶颈的环节。你会看到如何通过配置、代码和工具将应用的吞吐量提升一个数量级同时保持代码的清晰和可维护性。无论你是在应对即将到来的流量高峰还是想让现有应用运行得更“轻盈”这篇文章都将提供一套可直接落地的实战指南。1. 重新理解 ASP.NET Core 性能瓶颈从“点”到“链”在开始具体优化之前我们必须建立一个正确的认知性能瓶颈很少是孤立的。一个慢速的 API 接口问题可能出在数据库也可能出在序列化、中间件、甚至是依赖注入容器的配置上。ASP.NET Core 的请求处理是一条清晰的管道Pipeline请求进入 Kestrel 服务器经过一系列中间件Middleware最终到达你的控制器Controller或最小 API 端点处理后再沿原路返回。优化就是审视这条管道上的每一个环节。常见的瓶颈环节包括启动时间特别是在容器化Docker环境下冷启动速度直接影响弹性伸缩的效率。中间件管道不必要的中间件、同步的中间件逻辑、不当的异常处理。依赖注入DI单例Singleton、作用域Scoped、瞬时Transient服务的错误使用导致内存泄漏或创建开销过大。序列化/反序列化JSON 序列化特别是处理大型或复杂对象时是 CPU 消耗大户。数据库访问这虽然是老生常谈但连接池配置、EF Core 查询跟踪等 ASP.NET Core 特有的细节常被忽略。垃圾回收GC.NET 的 GC 非常高效但不合理的对象分配模式会触发不必要的 GC导致线程暂停影响响应时间。“B1218”这个代号提示我们优化需要像版本号一样有系统性和迭代性。接下来我们将沿着请求链逐一攻克这些环节。2. 环境准备与基准测试没有度量就没有优化在动手优化之前你必须先建立一个性能基准。盲目优化可能适得其反。2.1 必备工具链.NET SDK确保使用与生产环境一致的 LTS 版本或项目指定的版本如 .NET 8。新版本运行时通常包含性能改进。IDE/编辑器Visual Studio 2022 或 JetBrains Rider它们内置了强大的性能剖析工具。性能剖析工具dotnet-counters实时监控 CPU、内存、GC、HTTP 请求等指标。dotnet-trace收集应用程序的跟踪信息用于分析性能事件。Visual Studio Diagnostic Tools图形化界面易于进行 CPU 和内存使用率分析。BenchmarkDotNet用于对小块代码如某个算法、序列化方法进行精确的微基准测试。压力测试工具wrk,ab(ApacheBench), 或k6。用于模拟多用户并发测试应用整体吞吐量和稳定性。2.2 建立基准测试创建一个简单的测试端点并在优化前后使用相同的参数进行压测。首先我们创建一个有潜在性能问题的 API 作为我们的“优化靶子”// 文件路径Controllers/ProductController.cs using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Diagnostics; namespace PerformanceDemo.Controllers; [ApiController] [Route(api/[controller])] public class ProductController : ControllerBase { private readonly AppDbContext _context; private readonly ILoggerProductController _logger; public ProductController(AppDbContext context, ILoggerProductController logger) { _context context; _logger logger; } // 这是一个存在多个问题的初始版本API [HttpGet(slow)] public async TaskIActionResult GetSlowProducts() { var stopwatch Stopwatch.StartNew(); // 问题1: 同步日志记录模拟 _logger.LogInformation(开始处理获取产品请求。); // 问题2: 查询时未使用 AsNoTracking且选择了过多字段 var products await _context.Products .Include(p p.Category) // 可能不必要的贪婪加载 .ToListAsync(); // 获取所有字段 // 问题3: 在内存中进行复杂转换模拟业务逻辑 var result products.Select(p new ProductDto { Id p.Id, Name p.Name, CategoryName p.Category.Name, CalculatedPrice p.BasePrice * (1 p.TaxRate), // 模拟计算 FormattedDescription p.Description.ToUpper() !!! // 模拟字符串操作 }).ToList(); stopwatch.Stop(); _logger.LogInformation($GetSlowProducts 耗时: {stopwatch.ElapsedMilliseconds}ms); return Ok(result); } } // 简单的DTO和DbContext仅作演示 public class ProductDto { public int Id; public string Name; public string CategoryName; public decimal CalculatedPrice; public string FormattedDescription; } public class Product { public int Id; public string Name; public string Description; public decimal BasePrice; public decimal TaxRate; public Category Category; } public class Category { public int Id; public string Name; } public class AppDbContext : DbContext { public DbSetProduct Products SetProduct(); public DbSetCategory Categories SetCategory(); }使用wrk进行基准压测请在终端中运行# 针对上述慢接口进行30秒压测使用12个线程保持400个HTTP连接 wrk -t12 -c400 -d30s http://localhost:5000/api/product/slow记录下结果中的Requests/sec每秒请求数和Latency延迟分布。这是我们优化的起点。3. 启动性能优化第一印象至关重要对于需要快速扩缩容的云原生应用启动速度就是金钱。ASP.NET Core 9 及后续版本在启动性能上持续投入。3.1 启用 ReadyToRun (R2R) 编译R2R 是一种预先编译AOT形式将中间语言IL编译为本机代码减少运行时 JIT 编译开销。!-- 文件路径PerformanceDemo.csproj -- Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework !-- 发布时启用R2R -- PublishReadyToRuntrue/PublishReadyToRun /PropertyGroup /Project注意R2R 会增大发布包体积但能显著提升启动速度和初期运行时性能。适合容器场景。3.2 修剪未使用的代码使用 .NET 的代码修剪功能移除未使用的程序集和类型减小应用体积。PropertyGroup TargetFrameworknet8.0/TargetFramework !-- 启用修剪 -- PublishTrimmedtrue/PublishTrimmed !-- 对于Web应用建议使用默认的“partial”修剪模式更安全 -- TrimModepartial/TrimMode /PropertyGroup警告过度修剪可能导致运行时反射失败如 ORM、序列化库。务必在修剪后进行全面测试。3.3 延迟初始化服务对于启动时非必需的重型服务可以使用IOptions模式或LazyT进行延迟加载。但更优雅的方式是使用 ASP.NET Core 内置的延迟初始化// 在 Program.cs 中注册为延迟初始化的单例 builder.Services.AddSingletonIMyHeavyService(sp new LazyMyHeavyService(() sp.GetRequiredServiceMyHeavyService()).Value); // 更好的方式仅在需要时创建 builder.Services.AddSingletonIMyHeavyService, MyHeavyService(); // 然后在构造函数中注入 LazyT public class MyController { private readonly LazyIMyHeavyService _heavyService; public MyController(LazyIMyHeavyService heavyService) _heavyService heavyService; public void Action() { var service _heavyService.Value; // 第一次访问时才初始化 // ... 使用 service } }4. HTTP管道与中间件优化让请求流动得更快中间件是 ASP.NET Core 的核心但每个中间件都有成本。4.1 精简中间件管道检查Program.cs或Startup.cs移除开发环境或不需要的中间件。// Program.cs var app builder.Build(); // 生产环境应移除开发者异常页面 if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler(/Error); // app.UseHsts(); // 根据需求决定是否启用HSTS } else { app.UseDeveloperExceptionPage(); } // 仔细评估每个中间件的必要性 app.UseHttpsRedirection(); // 如果前端代理已处理HTTPS可移除 app.UseStaticFiles(); // 如果不需要静态文件可移除 app.UseRouting(); app.UseCors(MyPolicy); // 明确指定需要的CORS策略而不是用AllowAll app.UseAuthentication(); // 如果应用无需认证可移除 app.UseAuthorization(); // app.UseResponseCompression(); // 如果响应内容已压缩如图片或CPU是瓶颈需评估 app.MapControllers();规则中间件顺序很重要且每个请求都会经过它们。将使用频率低或条件执行的中间件放在靠后位置或使用MapWhen、UseWhen进行条件分支。4.2 使用IHttpContextAccessor的陷阱在自定义服务中注入IHttpContextAccessor来获取当前 HTTP 上下文非常方便但滥用会导致性能问题因为它依赖于异步本地存储AsyncLocal。应尽量避免在后台服务、单例服务中通过它访问HttpContext而应通过方法参数传递所需数据。// 不推荐在服务构造函数或后台任务中依赖IHttpContextAccessor public class BadService { private readonly IHttpContextAccessor _accessor; public BadService(IHttpContextAccessor accessor) _accessor accessor; public void Process() { var user _accessor.HttpContext?.User; // 可能为null且性能有损耗 } } // 推荐通过方法参数传递数据 public class GoodService { public void Process(ClaimsPrincipal user, string someDataFromContext) { // 直接使用参数 } } // 在控制器中调用 goodService.Process(User, data);5. 依赖注入与对象生命周期管理错误的生命周期配置是内存泄漏和性能问题的常见根源。5.1 正确选择服务生命周期单例Singleton全局唯一实例。用于无状态、线程安全的服务如配置、缓存客户端、日志器。切勿在单例服务中注入Scoped或Transient服务除非你非常清楚自己在做什么例如注入IServiceProvider来创建作用域。作用域Scoped每个 HTTP 请求创建一个实例。这是DbContext、仓储Repository和大多数业务逻辑服务的默认选择。瞬时Transient每次请求时都创建新实例。用于轻量级、无状态的服务。常见错误将DbContext注册为Singleton会导致数据库连接池耗尽和并发问题将本应为Singleton的重型服务注册为Transient会导致频繁创建和 GC 压力。5.2 避免捕获 Scoped 服务在后台任务如IHostedService或单例服务中直接捕获一个Scoped服务会导致该服务生命周期被延长可能引发DbContext已释放仍被访问的异常。// 错误示例在单例中捕获Scoped服务 public class SingletonBackgroundService : IHostedService { private readonly IServiceProvider _serviceProvider; public SingletonBackgroundService(IServiceProvider serviceProvider) _serviceProvider serviceProvider; public Task StartAsync(CancellationToken ct) { _ Task.Run(async () { using (var scope _serviceProvider.CreateScope()) { var scopedService scope.ServiceProvider.GetRequiredServiceIMyScopedService(); // 正确在创建的作用域内使用scopedService await scopedService.DoWorkAsync(); } // 作用域结束Scoped服务被释放 }, ct); return Task.CompletedTask; } }6. 数据访问与序列化深度优化这是性能问题的重灾区。6.1 EF Core 查询优化回到我们最初的“慢接口”让我们优化它[HttpGet(optimized)] public async TaskIActionResult GetOptimizedProducts() { // 使用 Stopwatch 仅在开发环境或需要时记录 // 生产环境应使用结构化日志和 Application Insights 等APM工具 // 优化1: 使用 AsNoTracking因为我们不会修改实体 // 优化2: 使用 Select 只查询需要的字段避免 SELECT * // 优化3: 将计算转移到数据库端如果逻辑简单或至少在内存中只计算一次 var productDtos await _context.Products .AsNoTracking() // 关键禁用变更跟踪大幅提升查询速度 .Include(p p.Category) // 评估是否真的需要。如果CategoryName常用可考虑DTO投影或缓存。 .Select(p new ProductDto // 在数据库端进行投影只传输必要数据 { Id p.Id, Name p.Name, CategoryName p.Category.Name, // 通过Include加载关联实体后可以访问其属性 // 注意复杂计算如果无法翻译成SQL仍需在内存中进行。 // 这里假设BasePrice和TaxRate来自数据库计算在内存中完成。 BasePrice p.BasePrice, TaxRate p.TaxRate, RawDescription p.Description }) .ToListAsync(); // 在内存中进行后续处理如果无法在SQL中完成 foreach (var dto in productDtos) { dto.CalculatedPrice dto.BasePrice * (1 dto.TaxRate); dto.FormattedDescription dto.RawDescription.ToUpperInvariant() !!!; // 移除临时字段或使用另一个DTO dto.BasePrice 0; dto.TaxRate 0; dto.RawDescription null; } return Ok(productDtos); }关键点AsNoTracking()对于只读查询这是必须的。投影Select查询数据库时只获取需要的列减少网络传输和内存占用。警惕N1查询问题使用Include或投影Select来一次性加载关联数据而不是在循环中 lazy loading。6.2 JSON 序列化优化System.Text.Json 是默认且高性能的序列化器。进一步优化// Program.cs 中配置Json选项 builder.Services.AddControllers() .AddJsonOptions(options { // 使用不区分大小写的属性名匹配根据需求 options.JsonSerializerOptions.PropertyNameCaseInsensitive true; // 使用更紧凑的编码默认已是false为兼容性可保持 // options.JsonSerializerOptions.WriteIndented false; // 忽略循环引用根据模型决定 options.JsonSerializerOptions.ReferenceHandler ReferenceHandler.IgnoreCycles; // 对于大型集合考虑使用源生成以提高性能.NET 6 // 需要为你的类型创建JsonSerializerContext });对于极高性能场景考虑使用源生成Source Generation它能在编译时生成序列化代码避免运行时反射。// 1. 创建一个JsonSerializerContext通常放在一个单独文件 [JsonSerializable(typeof(ListProductDto))] [JsonSerializable(typeof(ProductDto))] public partial class AppJsonContext : JsonSerializerContext { } // 2. 在控制器或服务中使用 [HttpGet(fast)] public IActionResult GetFastProducts() { var products GetProductsFromCacheOrService(); // 假设已获取数据 // 使用源生成的序列化器性能更高 var json JsonSerializer.Serialize(products, AppJsonContext.Default.ListProductDto); return Content(json, application/json); }7. 缓存策略多级缓存的正确姿势缓存是性能优化的银弹但用错了就是炸弹。7.1 内存缓存IMemoryCache适合缓存数据量小、访问频繁、对一致性要求不极高的数据。// 注册服务 builder.Services.AddMemoryCache(); // 在控制器或服务中使用 public class ProductService { private readonly IMemoryCache _cache; private readonly AppDbContext _context; public ProductService(IMemoryCache cache, AppDbContext context) (_cache, _context) (cache, context); public async TaskListProductDto GetTopProductsAsync() { const string cacheKey TopProducts; // 尝试从缓存获取 if (!_cache.TryGetValue(cacheKey, out ListProductDto products)) { // 缓存未命中从数据库获取 products await _context.Products .AsNoTracking() .OrderByDescending(p p.ViewCount) .Take(10) .Select(p new ProductDto { /* 投影 */ }) .ToListAsync(); // 设置缓存选项绝对过期时间 滑动过期时间并设置缓存优先级和大小 var cacheOptions new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromMinutes(5)) // 5分钟后绝对过期 .SetSlidingExpiration(TimeSpan.FromMinutes(2)) // 如果2分钟内未被访问则过期 .SetPriority(CacheItemPriority.High) // 内存不足时优先保留 .SetSize(1); // 设置相对大小用于控制总缓存大小 _cache.Set(cacheKey, products, cacheOptions); } return products; } }7.2 分布式缓存IDistributedCache当应用部署在多台服务器如 Web Farm时必须使用分布式缓存如 Redis、SQL Server来保证缓存一致性。// 使用StackExchange.Redis builder.Services.AddStackExchangeRedisCache(options { options.Configuration builder.Configuration.GetConnectionString(Redis); options.InstanceName PerformanceDemo:; // 为所有键添加前缀 }); // 使用方式与IMemoryCache类似但值需要序列化/反序列化 public async TaskListProductDto GetProductsDistributedAsync() { var cachedData await _distributedCache.GetStringAsync(cacheKey); if (cachedData ! null) { return JsonSerializer.DeserializeListProductDto(cachedData); } // ... 从数据库获取并缓存 var dataToCache JsonSerializer.Serialize(products); await _distributedCache.SetStringAsync(cacheKey, dataToCache, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow TimeSpan.FromMinutes(5) }); return products; }7.3 响应缓存Response Caching对于完全静态或更新不频繁的 API 响应可以使用 HTTP 响应缓存。// Program.cs 中启用响应缓存中间件 builder.Services.AddResponseCaching(); app.UseResponseCaching(); // 注意中间件顺序通常放在UseRouting之后UseEndpoints之前 // 在控制器或Action上使用特性 [ApiController] [Route(api/[controller])] [ResponseCache(Duration 30)] // 缓存30秒 public class CatalogController : ControllerBase { [HttpGet(list)] [ResponseCache(Duration 60, Location ResponseCacheLocation.Any)] // 覆盖Controller设置 public IActionResult GetList() { /* ... */ } }注意响应缓存基于 HTTP 标准缓存可能发生在客户端、代理服务器或服务器端。对于个性化数据如包含用户ID的响应要慎用或使用VaryByQueryKeys。8. 高级主题垃圾回收GC与服务器配置8.1 选择正确的 GC 模式.NET 提供了多种 GC 模式适用于不同场景工作站模式Workstation GC默认模式优化 UI 响应和单线程性能。适合客户端应用或轻负载服务。服务器模式Server GC为多核服务器和高吞吐量优化。它会为每个 CPU 核心创建独立的 GC 堆和线程并行执行回收减少暂停时间但内存占用更高。并发模式Concurrent GC在工作站模式下允许在 GC 执行部分阶段时用户线程继续运行2代回收除外减少暂停时间。后台 GCBackground GC在服务器模式下2代回收在后台线程进行几乎不阻塞用户线程是 .NET Core 及更高版本的默认行为。配置在项目文件.csproj或runtimeconfig.json中配置。!-- 项目文件.csproj -- PropertyGroup TargetFrameworknet8.0/TargetFramework ServerGarbageCollectiontrue/ServerGarbageCollection !-- 启用服务器GC -- ConcurrentGarbageCollectiontrue/ConcurrentGarbageCollection !-- 启用并发GC工作站模式 -- /PropertyGroup对于高吞吐量的 Web 服务器应用启用 Server GC 通常是正确的选择。8.2 Kestrel 服务器调优Kestrel 是 ASP.NET Core 的默认 Web 服务器性能极佳但默认配置可能不适合极高并发场景。// Program.cs builder.WebHost.ConfigureKestrel(serverOptions { // 配置监听地址和端口 serverOptions.Listen(IPAddress.Any, 5000); // serverOptions.Listen(IPAddress.Any, 5001, listenOptions listenOptions.UseHttps()); // 调整连接限制根据服务器内存和负载调整 serverOptions.Limits.MaxConcurrentConnections 1000; // 默认无限制(null) serverOptions.Limits.MaxConcurrentUpgradedConnections 100; // WebSockets等升级连接 serverOptions.Limits.MaxRequestBodySize 30_000_000; // 最大请求体大小默认30MB // 调整请求超时 serverOptions.Limits.KeepAliveTimeout TimeSpan.FromMinutes(2); serverOptions.Limits.RequestHeadersTimeout TimeSpan.FromSeconds(30); });关键参数MaxConcurrentConnections需要根据实际压测结果调整设置过低会导致连接被拒绝过高可能导致内存耗尽。9. 性能监控与持续优化优化不是一劳永逸的需要持续监控。9.1 使用 Application Insights 或 OpenTelemetry集成 APM应用性能监控工具监控请求速率、响应时间、失败率、依赖调用如 SQL、HTTP和异常。// Program.cs builder.Services.AddApplicationInsightsTelemetry(); // 或使用 OpenTelemetry builder.Services.AddOpenTelemetry() .WithTracing(tracing tracing .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddEntityFrameworkCoreInstrumentation() .AddConsoleExporter()); // 导出到控制台生产环境应导出到Jaeger/Prometheus等9.2 使用 Health Checks健康检查不仅用于探活还可以集成自定义的性能指标检查。builder.Services.AddHealthChecks() .AddDbContextCheckAppDbContext(database) // 检查数据库连接 .AddRedis(builder.Configuration.GetConnectionString(Redis), redis) // 检查Redis .AddUrlGroup(new Uri(https://api.example.com), external-api); // 检查外部API app.MapHealthChecks(/health);9.3 定期进行负载测试将压测如使用 k6 或 Azure Load Testing集成到 CI/CD 管道中在每次重大更改后自动运行防止性能回归。10. 常见问题与排查清单问题现象可能原因排查方式解决方案CPU 持续高占用1. 存在死循环或低效算法。2. 大量同步阻塞调用如.Result,.Wait()。3. JSON 序列化/复杂正则表达式消耗CPU。1. 使用dotnet-counters监控 CPU。2. 使用dotnet-trace或 Visual Studio Profiler 进行 CPU 采样查看热点函数。1. 优化算法使用异步。2. 使用System.Text.Json源生成。3. 缓存计算结果。内存使用率不断增长内存泄漏1. 单例或静态集合持有对象引用阻止GC回收。2. 未及时释放IDisposable对象如DbContext。3. 事件未取消订阅。1. 使用dotnet-counters监控 GC 和内存。2. 使用 DebugDiag 或 Visual Studio 内存快照分析对象根。1. 检查服务生命周期。2. 确保使用using语句或调用Dispose。3. 使用弱事件模式或及时取消订阅。API 响应时间慢但数据库查询快1. 中间件管道过长或存在同步阻塞。2. 序列化/反序列化耗时。3. 日志记录同步输出到控制台或文件。1. 使用 Application Insights 分布式跟踪查看各环节耗时。2. 在开发环境使用app.UseMiddlewareResponseTimeMiddleware();记录中间件时间。1. 精简中间件使用异步。2. 优化 DTO减少嵌套和循环引用。3. 使用异步日志器如 Serilog并配置合适的输出目标。应用启动非常慢1. 首次 JIT 编译。2. 大量服务在启动时初始化。3. 冷启动时从网络加载依赖。1. 检查启动日志。2. 使用dotnet-trace分析启动过程。1. 启用 ReadyToRun (R2R)。2. 延迟初始化非关键服务。3. 使用基础镜像预编译。并发量高时出现大量错误或超时1. 数据库连接池耗尽。2. 线程池饥饿大量同步阻塞任务。3. Kestrel 连接限制或操作系统端口耗尽。1. 监控数据库活动连接数。2. 监控线程池可用线程数ThreadPool.GetAvailableThreads。3. 查看系统日志和 Kestrel 日志。1. 优化连接字符串调整Max Pool Size。2. 将同步代码改为异步。3. 调整 KestrelLimits和操作系统net.core.somaxconn参数。11. 最佳实践总结度量先行优化前、后都必须有可量化的指标RPS 延迟 CPU 内存。瓶颈驱动使用性能剖析工具找到真正的瓶颈而不是猜测。异步全链路从控制器到数据库访问确保整个调用链是异步的避免阻塞线程池线程。谨慎缓存理解数据的更新频率和一致性要求选择正确的缓存策略和过期时间。关注 GC对于服务器应用启用 Server GC。监控 GC 暂停时间优化大对象分配。依赖注入清醒深刻理解 Singleton、Scoped、Transient 的生命周期避免内存泄漏和并发问题。EF Core 优化AsNoTracking()是只读查询的标配使用投影Select减少数据传输警惕 N1 查询。配置可调整将 Kestrel 限制、数据库连接字符串、缓存过期时间等配置化便于不同环境调优。结构化日志使用像 Serilog 这样的库并输出到像 Elasticsearch 这样的集中式日志系统便于问题排查。持续监控将性能监控作为应用的一部分建立警报机制主动发现问题。回到开头的“B1218”项目它不是一个具体的工具包而是一套贯穿应用生命周期的优化哲学。性能优化是一个迭代和平衡的过程需要在开发速度、代码清晰度、资源消耗和用户体验之间找到最佳平衡点。从今天起在你编写每一行 ASP.NET Core 代码时都带着“性能意识”这比任何事后的优化都更有效。将本文中的策略应用到你的项目中重新运行一次压测你将会看到显著的提升。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价