如何优化 .NET 中的启动性能

作者:微信公众号:【架构师老卢】
9-17 18:44
157

优化 .NET 应用程序的启动性能至关重要,尤其是对于需要从一开始就响应的应用程序。以下是缩短 .NET Core/ASP.NET Core 应用程序和基于 .NET 构建的桌面应用程序的启动时间的关键技术和策略:

1. 最大限度地减少启动时加载的依赖项

在启动期间加载大量程序集或服务可能会减慢初始化过程。

延迟加载:与其预先加载所有内容,不如延迟服务、库和组件的初始化,直到实际需要它们。

services.AddSingleton<HeavyService>(provider =>   
    new Lazy<HeavyService>(() => new HeavyService()));

避免不必要的服务:仅注册启动时需要的服务,避免注册不会立即需要的服务。

避免 复杂逻辑 :保持 和 中的逻辑简单。Startup.ConfigureServicesStartup.ConfigureServicesStartup.Configure

2. 优化 ASP.NET Core 中的中间件

中间件组件的顺序和数量会影响性能。

使用最小中间件:在请求管道中仅添加必要的中间件组件。避免在启动过程中使用不必要的中间件。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    app.UseRouting();  
    app.UseEndpoints(endpoints => {  
        endpoints.MapControllers();  
    });  
}

3. 减小应用程序二进制文件的大小

应用程序越大,加载程序集所需的时间就越多。

剪裁未使用的库:使用 ILLinker(在 .NET Core 中提供)剪裁未使用的代码,删除应用程序中未使用的库的不必要部分。将此添加到您的 :.csproj

<PropertyGroup>  
    <PublishTrimmed>true</PublishTrimmed>  
</PropertyGroup>

此功能可分析依赖项并删除未使用的部分,从而减小应用程序大小并缩短启动时间。

单文件发布:在 .NET Core 3.0+ 和 .NET 5/6/7/8 中,您可以将应用程序发布为单文件可执行文件,以避免在启动时加载多个程序集的开销。

将此添加到您的 :.csproj

<PropertyGroup>  
    <PublishSingleFile>true</PublishSingleFile>  
</PropertyGroup>

4. 使用预先 (AOT) 编译

.NET 7 和 .NET 8 提供**本机 AOT(预先编译),**可将 .NET 应用程序直接编译为机器代码,无需在启动期间进行 JIT(即时)编译。

本机 AOT:对于启动性能至关重要的应用程序,您可以提前将 .NET 代码编译为本机代码。

将此添加到您的 :.csproj

<PropertyGroup>  
    <PublishAot>true</PublishAot>  
</PropertyGroup>

此功能通过减少 JIT 编译所花费的时间,大大提高了启动性能。

5. 优化 Entity Framework Core 初始化

如果您在 .NET 应用程序中使用 Entity Framework Core,则由于模型构建和数据库连接的原因,其启动速度可能会很慢。

预编译查询:使用预编译查询来避免重新编译频繁执行的 LINQ 查询的开销。

var query = dbContext.Users  
    .Where(u => u.Age > 18)  
    .TagWith("Precompiled query")  
    .ToList();

避免 / 启动时:在启动期间运行数据库迁移会显著降低应用程序的速度。最好将这些命令作为部署脚本的一部分运行,而不是在启动期间运行。EnsureCreatedMigrate

6. 使用 HTTP/2 并启用响应压缩

如果您的 .NET Core 应用程序提供 HTTP 流量,则从用户的角度来看,启用 HTTP/2响应压缩可以提高启动性能。

HTTP/2:HTTP/2 减少了连接数并允许请求多路复用。在 中启用它 :Program.cs

var builder = WebApplication.CreateBuilder(args);  
builder.WebHost.ConfigureKestrel(serverOptions =>  
{  
    serverOptions.Listen(IPAddress.Any, 5000, listenOptions =>  
    {  
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;  
    });  
});

响应压缩:使用响应压缩来减小发送到客户端的负载的大小,从而减少通过网络的启动时间。

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddResponseCompression();  
}  
  
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    app.UseResponseCompression();  
}

7. 优化配置和日志记录

如果优化不当,读取配置和设置日志记录可能会导致延迟。

使用编译的配置提供程序:考虑在启动期间将它们编译到应用程序中,而不是在启动期间从文件或远程源加载配置。例如,您可以有效地使用该方法从文件加载配置,而不会延迟。IConfigurationAddJsonFile

public IConfiguration Configuration { get; }  
  
public Startup(IConfiguration configuration)  
{  
    Configuration = configuration;  
}  
  
public void ConfigureServices(IServiceCollection services)  
{  
    services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));  
}

减少详细日志记录:过多的日志记录,尤其是在 or 等较高级别,可能会减慢启动时间。根据生产环境调整日志记录级别。DebugTrace

logging.AddFilter("System", LogLevel.Warning)

8. 用于后台任务IHostedService

对于任何后台任务,请避免在启动阶段执行它们。相反,请使用该接口将后台工作推迟到应用程序启动之后。IHostedService

通过实施 ,您可以执行长时间运行的后台任务,而不会阻止应用程序启动。IHostedService

public class BackgroundTaskService : IHostedService  
{  
    public Task StartAsync(CancellationToken cancellationToken)  
    {  
        // Perform work in the background  
        return Task.CompletedTask;  
    }  
  
    public Task StopAsync(CancellationToken cancellationToken)  
    {  
        return Task.CompletedTask;  
    }  
}  
  
// Register in Startup.cs  
services.AddHostedService<BackgroundTaskService>();

9. 热身技巧

预热应用程序可确保在实际用户请求到达应用程序之前初始化关键服务、数据和组件。

Pre-JIT Compilation:您可以使用预热服务执行 Pre-JIT (Just-In-Time) 编译,该服务在用户访问应用程序之前执行应用程序的关键路径,从而减少应用程序处理实际流量时的启动延迟。

public class WarmupMiddleware  
{  
    private readonly RequestDelegate _next;  
  
    public WarmupMiddleware(RequestDelegate next)  
    {  
        _next = next;  
    }  
  
    public async Task InvokeAsync(HttpContext context)  
    {  
        // Invoke critical paths during startup  
        // Simulate a real request by triggering controller actions, database queries, etc.  
        await _next(context);  
    }  
}

ASP.NET 核心应用程序预热模块:对于 Web 应用程序,请使用 IIS 中的应用程序初始化模块预加载应用程序,确保在处理用户请求时应用程序已经预热。

10. 用于推迟工作StartupBackgroundService

在 .NET 中,您可以使用自定义服务,在应用程序启动后将繁重的工作推迟到后台线程,从而确保非关键任务不会阻止启动。

public class StartupBackgroundService : BackgroundService  
{  
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)  
    {  
        // Perform time-consuming initialization after app startup  
        await Task.Delay(10000); // Simulate deferred work  
    }  
}  
  
// Register in Startup.cs  
services.AddHostedService<StartupBackgroundService>();

此方法可确保您的应用程序快速启动,同时将非关键初始化卸载到后台服务。

11. 优化 Razor 页面和视图(适用于 ASP.NET Core)

如果使用 Razor PagesViews 构建 Web 应用程序,则 Razor 文件的编译可能会导致启动延迟。

预编译 Razor 视图:预编译 Razor 视图,以避免在启动期间进行运行时编译。

将以下内容添加到您的文件中:.csproj

<PropertyGroup>  
    <RazorCompileOnPublish>true</RazorCompileOnPublish>  
</PropertyGroup>

这可确保在构建或发布时编译视图,从而减少启动时间。

12. 优化依赖注入 (DI)

如果注册的服务过多或服务解析效率低下,依赖关系注入 (DI) 可能会成为性能瓶颈。

适当使用单例服务:将单例生存期用于不需要多次实例化的服务,从而减少创建多个对象的开销。

services.AddSingleton<IMyService, MyService>();

避免服务过度使用:保持服务注册最少。避免在依赖项容器中注册启动期间未使用的服务。

13. 使用即用型 (R2R) 编译

即用型 (R2R) 是一项功能,可在构建时将 .NET 程序集预编译为本机代码,从而减少 JIT 编译所花费的时间。

在文件中启用 R2R:.csproj

<PropertyGroup>  
    <PublishReadyToRun>true</PublishReadyToRun>  
</PropertyGroup>

这可以显著缩短启动时间,特别是对于大型应用程序,因为无需在运行时进行 JIT 编译。

14. 禁用未使用的功能

禁用您不使用的功能以减少启动开销:

在 ASP.NET Core 中禁用未使用的功能:如果 Web 应用程序不使用某些功能(如 MVC、Razor Pages 或 Blazor),请避免添加不必要的中间件和服务。

// Remove unnecessary services if you're not using Razor Pages  
// services.AddControllersWithViews();  
// services.AddRazorPages();

删除未使用的 NuGet 包:未使用的包可能会在启动期间增加不必要的开销,尤其是在 DI 中注册服务或执行启动逻辑时。定期审核并删除未使用的 NuGet 包。

15. 并行启动任务

并行化非依赖启动任务可以减少应用程序准备就绪所需的时间。

Task.WhenAll:用于并发执行独立任务。Task.WhenAll

public void ConfigureServices(IServiceCollection services)  
{  
    // Parallelize multiple independent tasks  
    Task.WhenAll(  
        Task.Run(() => services.AddDbContext<MyDbContext>()),  
        Task.Run(() => services.AddHttpClient())  
    ).Wait();  
}

确保任务不相互依赖,以避免争用条件。

16. 使用 HTTP/3(适用于 ASP.NET Core)

HTTP/3 通过其改进的 HTTP/2 连接模型减少了延迟并增强了 Web 应用程序的启动性能。.NET 6 及更高版本支持 HTTP/3。

通过配置 Kestrel 启用 HTTP/3:

builder.WebHost.ConfigureKestrel(serverOptions =>  
{  
    serverOptions.Listen(IPAddress.Any, 5001, listenOptions =>  
    {  
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;  
    });  
});

HTTP/3 提供了更快的连接建立速度,从客户端的角度来看,这提高了感知的启动性能。

17. 异步启动代码

尽可能在应用程序启动期间使用异步代码,尤其是在访问 IO 绑定的资源(如数据库或外部服务)时。

例:

public async Task ConfigureAsync(IServiceProvider services)  
{  
    var dbContext = services.GetRequiredService<MyDbContext>();  
    await dbContext.Database.MigrateAsync();  // Avoid blocking  
}

这可确保线程池保持响应,从而减少由阻塞操作导致的启动延迟。

18. 优化垃圾收集 (GC)

根据应用程序的内存使用情况和工作负载调整应用程序的垃圾回收设置。

对于服务器工作负载,启用服务器 GC 以获得更好的多线程应用程序性能

<PropertyGroup>  
    <ServerGarbageCollection>true</ServerGarbageCollection>  
</PropertyGroup>

GCSettings.LatencyMode:对于延迟敏感型应用程序(例如 Web API),您可以配置延迟模式,以最大限度地减少启动期间垃圾回收的影响。

GCSettings.LatencyMode = GCLatencyMode.LowLatency;

19. 分析启动性能

使用分析工具确定启动时间的瓶颈。dotTracedotnet-tracePerfView 等工具可帮助您分析启动性能。

使用 dotnet-trace 捕获性能跟踪:

dotnet-trace collect --process-id <PID> --providers Microsoft-Windows-DotNETRuntime

这些工具允许您测量 CPU 使用率、内存分配以及各个组件在启动期间加载和初始化所花费的时间。

相关留言评论
昵称:
邮箱:
阅读排行