修复 Conda 导致的 PowerShell 启动缓慢

修复 Conda 导致的 PowerShell 启动缓慢

问题描述

每次启动 PowerShell 时都有明显的卡顿,启动时间长达数秒。经过排查,发现这是由于 Conda 的初始化脚本导致的。

原因

在使用 conda init 命令配置 PowerShell 环境时,Conda 会向配置文件(Profile)中注入一段初始化代码。这段代码会在每次启动 PowerShell 时立即执行,加载整个 Conda 环境,从而拖慢启动速度。

排查过程

  1. 测试当前启动时间 使用 Measure-Command 测试加载配置文件的启动耗时:

    Measure-Command { pwsh.exe -Command "exit" }

    测试结果显示需要约 5 秒。

  2. 测试裸启动时间 测试不加载配置文件(-NoProfile)的启动耗时:

    Measure-Command { pwsh.exe -NoProfile -Command "exit" }

    结果仅需 0.5 秒左右。巨大的时间差异证实了问题出在配置文件(Profile)的加载过程中。

  3. 定位配置文件

    $PROFILE | Get-Member -Type NoteProperty
  4. 分析配置文件内容 打开配置文件(通常是 CurrentUserAllHosts),找到 Conda 注入的代码块:

    #region conda initialize
    # !! Contents within this block are managed by 'conda init' !!
    If (Test-Path "xx\Scripts\conda.exe") {
        (& "xx\Scripts\conda.exe" "shell.powershell" "hook") | Out-String | ?{$_} | Invoke-Expression
    }
    #endregion
  5. 验证假设 临时注释掉上述代码块后,再次测试启动时间:

    Measure-Command { pwsh.exe -Command "exit" }
    Measure-Command { pwsh.exe -NoProfile -Command "exit" }

    发现与裸启动时间基本持平,确认该段代码是性能瓶颈的根源。

解决方案

直接注释掉代码虽然能解决启动慢的问题,但会导致无法使用 conda 命令。 更好的方案是将同步初始化改为懒加载(Lazy Loading):仅在首次输入 conda 命令时才执行初始化逻辑。

注:只需将脚本中的 xx.exe 替换为你实际的 conda 安装路径即可。

#region conda initialize

# ===== 配置:🛑 改为实际的 conda.exe 路径 =====
$script:CondaExePath = 'xx\Scripts\conda.exe'

# ===== 主逻辑 =====
if (Test-Path -LiteralPath $script:CondaExePath -PathType Leaf) {
    
    # 状态标记:防止递归调用
    $script:CondaInitialized = $false
    
    # 定义核心初始化逻辑
    function Global:Invoke-CondaInit {
        # 只有未初始化时才执行 hook 逻辑
        if (-not $script:CondaInitialized) {
            Write-Host "🔄 正在初始化 Conda..." -ForegroundColor Cyan
            $script:CondaInitialized = $true
            
            try {
                # 1. 获取 Hook 脚本 (屏蔽 stderr 警告)
                $hookOutput = & $script:CondaExePath 'shell.powershell' 'hook' 2>$null
                $exitCode = $LASTEXITCODE
                
                # 2. 严格检查:任何异常直接报错
                if ($exitCode -ne 0) { throw "Conda hook 进程退出码异常: $exitCode" }
                if ([string]::IsNullOrWhiteSpace($hookOutput)) { throw "Conda hook 返回空内容" }
                
                # 3. 执行官方 Hook
                Invoke-Expression ($hookOutput | Out-String)
               
                # 4. 双重保险:确保 Global 作用域生效
                try {
                    $fn = Get-Command conda -CommandType Function -ErrorAction Stop
                    Set-Item -Path Function:\Global:conda -Value $fn.ScriptBlock -Force
                }
                catch {
                    throw "Hook 执行后未检测到 conda 函数,环境可能已损坏"
                }

                Write-Host "✓ Conda 初始化成功" -ForegroundColor Green
            }
            catch {
                # 失败处理:重置状态并直接抛出致命错误
                $script:CondaInitialized = $false
                Write-Error "Conda 初始化失败: $($_.Exception.Message)" -ErrorAction Stop
            }
        }
        
        # 执行用户命令
        if ($args.Count -gt 0) {
            & conda @args
        }
    }
    
    # 应用代理:拦截首次调用
    Set-Item Function:\Global:conda -Value { Invoke-CondaInit @args } -Force
    
}
else {
    # 路径配置错误的报错逻辑
    Write-Warning "未找到 Conda: $script:CondaExePath"
    
    function Global:conda {
        Write-Error "Conda 配置路径无效: $script:CondaExePath" -ErrorAction Stop
    }
}
#endregion

代码解释

当PowerShell启动时:

PowerShell 启动
    ↓
Set-Item Function:\Global:conda -Value { Invoke-CondaInit @args }
    ↓
此时 conda = Invoke-CondaInit

在上面的代码中,Set-Item Function:\Global:conda ... 会将 conda 命令重定向到代理函数 Invoke-CondaInit。 此时,当第一次调用 conda 命令时,会触发代理函数 Invoke-CondaInit:

用户输入: conda -V
    ↓
调用代理函数 Invoke-CondaInit
    ↓
进入 Invoke-CondaInit
    ↓
执行官方 Hook: Invoke-Expression ($hookOutput | Out-String)
    ↓
【关键】官方 Hook 会定义一个新的 conda 函数!
    ↓
Set-Item -Path ... (仅作为防御性检查,通常官方 Hook 已经将 conda 函数作用到全局)
    ↓
此时 conda = 官方函数(覆盖了我们的代理)
    ↓
& conda @args  ← 调用的是官方函数
    ↓
用户再输入: conda activate xxx
    ↓
调用的是【官方 conda 函数】,不再是代理
    ↓
Invoke-CondaInit 永远不会再被调用