惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
S
Secure Thoughts
C
Check Point Blog
D
DataBreaches.Net
V
V2EX
The GitHub Blog
The GitHub Blog
博客园 - Franky
博客园_首页
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Y
Y Combinator Blog
Last Week in AI
Last Week in AI
H
Help Net Security
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
宝玉的分享
宝玉的分享
A
About on SuperTechFans
月光博客
月光博客
Google DeepMind News
Google DeepMind News
小众软件
小众软件
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
F
Fortinet All Blogs
Blog — PlanetScale
Blog — PlanetScale
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
GbyAI
GbyAI
G
Google Developers Blog
P
Proofpoint News Feed
Recorded Future
Recorded Future
C
CXSECURITY Database RSS Feed - CXSecurity.com
MyScale Blog
MyScale Blog
T
Threatpost
V
Vulnerabilities – Threatpost
AI
AI
Cyberwarzone
Cyberwarzone
B
Blog RSS Feed
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 最新话题
L
LangChain Blog
Webroot Blog
Webroot Blog
AWS News Blog
AWS News Blog
云风的 BLOG
云风的 BLOG
W
WeLiveSecurity
Latest news
Latest news
U
Unit 42
C
CERT Recently Published Vulnerability Notes
Cisco Talos Blog
Cisco Talos Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Security @ Cisco Blogs
Forbes - Security
Forbes - Security

Cyberia

排印演进 - Cyberia EAC相关工具集合 - Cyberia 《C(金钱掌控)》观后感 - Cyberia C#在Windows UI开发中的衰落 - Cyberia 存储退潮 字体padding计算器 开源已死 如何写出优雅完整的音频元数据 自动化EAC音频抓取 起夜级家庭网络正在变得越来越重要 无JavaScript其实没有很重要 你不需要一个电纸书阅读器 文件分享的难题 为什么我不敢看都市 某科学的 SSH 算号指南 时代的辉夜姬 为什么 Pixi 是神 关于神椿的过去,当下以及未来,我的一点看法 Tahoe 的图标令人难评 The EVE of PUSHING LIMITS 在 Word 中通过制表符实现居中对齐等操作 编码之殇:前 Unicode 时代的 WAV 处理 sHELL 即地狱 WR1800K AX NAND 路由器刷入 Immortalwrt 一个 PWM 的世界 沉痛悼念复选框同志 字体焕新 独立博客自省问卷15题 答一答 解决 Visual Studio Code 内终端启动报错问题 (1) 简评《鱿鱼游戏》 Sword Art Online,以及它带来了什么 解决 Visual Studio Code 内终端启动报错问题 2024 年度总结 轻小说重度依赖 铃音之后 解决部分应用在 Windows 平台下端口被随机占用的问题 计算机科学中最困难的事:居中 宿舍场景下多人共用路由器下流量分流实践 Extra Decal Pre Processor 自动化处理 Amazon Music 文件 MD5 不一致问题 解决 MultiMC 无 Forge 版本的 Optifine 安装问题
优化Powershell的启动时间
Nullpinter · 2026-04-13 · via Cyberia

注意

这里的Powershell指pwsh7,部分新特性无法在5中使用,需要手动调整。

Powershell,有很多人都不喜欢、不理解,但是我挺喜欢的。不过Powershell最大的痛点其实是慢。当你使用各种命令行工具的默认补全命令时,你的启动时间可能会来到十几秒。所以有很多人都觉得这个速度太慢了。这里将要给出一种解决方案,能够成功的把启动时间压缩到500ms以内,并且不阻塞输入。

使用starship替换oh-my-posh#

不要再使用oh-my-posh了,这东西和oh-my-zsh一样都是性能杀手,非常卡顿。欢迎来用Starship,Rust神力还是很神力的。

禁止加载不必要的模块#

虽然我觉得大部分人应该没有这种问题,但是鉴于部分应用会自动安装那还是提一下,请务必检查Import-Module的调用,看看有没有不必要的模块被加载了。比如说Azure相关的模块,如果你不需要用到Azure相关的功能,那么就不要加载它们。

开始优化#

1 为什么慢#

大部分cli工具给出的补全命令都是及时输出eval的,比如:

(& uv generate-shell-completion powershell) | Out-String | Invoke-Expression

# 这个 uv 真的是太坏了

这会引入两个性能敏感点:Application调用和管道。

PowerShell scripts that leverage .NET directly and avoid the pipeline tend to be faster than idiomatic PowerShell. Idiomatic PowerShell uses cmdlets and PowerShell functions, often leveraging the pipeline, and resorting to .NET only when necessary.
——PowerShell scripting performance considerations

Powershell不是老旧的基于文本流的shell,powershell的基本设计是基于对象的,所以调用外部进程会引入编解码开销。并且,在运行外部进程的时候会阻塞用户输入。

同时管道越多,性能越差,这是所有shell都有的问题,管道会引入额外的开销。

因此,核心优化点就在于如何避免管道,如何避免调用外部进程。

2 OnIdle#

一些用过zsh-defer的人会对OnIdle比较熟悉,OnIdle在Powershell的触发时机是300ms没有用户输入,所以这可以被用来做后台初始化。一个典型的one-shot例子如下:

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

# Do something

} -MaxTriggerCount 1 -SupportEvent | Out-Null

在接下来的优化过程中,会大量使用OnIdle来做一些非必要的初始化,来达到加速启动的目的。

3 缓存与更新补全脚本#

如何做到不call外部Application就能获得其补全脚本呢?当然是缓存。所以引入了缓存与后台更新机制:

# update scripts

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

$baseDir = Split-Path -Parent $PROFILE

Start-ThreadJob -Name 'completion-cache-update' -ArgumentList $baseDir -ScriptBlock {

param($baseDir)

$ErrorActionPreference = 'Stop'

$locksDir = Join-Path $baseDir 'Locks'

$completionsDir = Join-Path $baseDir 'Completions'

New-Item -ItemType Directory -Force -Path $locksDir, $completionsDir | Out-Null

42 collapsed lines

function Update-CompletionScriptFile {

param(

[Parameter(Mandatory)][string]$CheckCommand,

[Parameter(Mandatory)][string[]]$CheckArgs,

[Parameter(Mandatory)][string]$GenerateCommand,

[Parameter(Mandatory)][string[]]$GenerateArgs,

[Parameter(Mandatory)][string]$OutputFileName

)

$target = Join-Path $completionsDir $OutputFileName

$lockFile = Join-Path $locksDir ($OutputFileName + '.version')

$current = (& $CheckCommand @CheckArgs 2>&1 | Out-String).Trim()

if (-not $current) { throw "Empty version output for $OutputFileName" }

$needsUpdate = $true

if ((Test-Path $lockFile -PathType Leaf) -and (Test-Path $target -PathType Leaf)) {

$prev = (Get-Content $lockFile -Raw).Trim()

if ($prev -eq $current) {

$needsUpdate = $false

}

}

if (-not $needsUpdate) { return }

$body = (& $GenerateCommand @GenerateArgs 2>&1 | Out-String)

if (-not $body.Trim()) { throw "Empty body for $OutputFileName" }

$header = @(

"# generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"

""

) -join [Environment]::NewLine

$tmpTarget = "$target.tmp"

$tmpLock = "$lockFile.tmp"

Set-Content -Path $tmpTarget -Value ($header + $body) -Encoding utf8

Set-Content -Path $tmpLock -Value $current -Encoding utf8

Move-Item -Path $tmpTarget -Destination $target -Force

Move-Item -Path $tmpLock -Destination $lockFile -Force

}

Update-CompletionScriptFile 'starship' @('--version') 'starship' @('init', 'powershell', '--print-full-init') 'starship.ps1'

Update-CompletionScriptFile 'zoxide' @('--version') 'zoxide' @('init', 'powershell') 'zoxide.ps1'

Update-CompletionScriptFile 'pixi' @('--version') 'pixi' @('completion', '--shell', 'powershell') 'pixi.ps1'

Update-CompletionScriptFile 'rustup' @('--version') 'rustup' @('completions', 'powershell') 'rustup.ps1'

Update-CompletionScriptFile 'uv' @('--version') 'uv' @('generate-shell-completion', 'powershell') 'uv.ps1'

Update-CompletionScriptFile 'uvx' @('--version') 'uvx' @('--generate-shell-completion', 'powershell') 'uvx.ps1'

Update-CompletionScriptFile 'sk' @('-V') 'sk' @('--shell', 'power-shell') 'skim.ps1'

} | Out-Null

} -MaxTriggerCount 1 -SupportEvent | Out-Null

这段使用OnIdle one-shot一次触发+ThreadJob后台运行,在后台主动更新缓存的脚本,避免了在启动时调用外部Application来获取补全脚本,同时不阻塞用户输入。

4 加载补全脚本#

然后补上加载补全脚本的代码,加载的时候直接执行缓存的脚本:

$baseDir = Split-Path -Parent $PROFILE

$locksDir = Join-Path $baseDir 'Locks'

$completionsDir = Join-Path $baseDir 'Completions'

New-Item -ItemType Directory -Force -Path $locksDir, $completionsDir | Out-Null

14 collapsed lines

function Import-CompletionScript {

param(

[Parameter(Mandatory)]

[string]$OutputFileName

)

$target = [System.IO.Path]::Combine($completionsDir, $OutputFileName)

if (-not [System.IO.File]::Exists($target)) {

return $false

}

. $target

return $true

}

Import-CompletionScript 'starship.ps1' | Out-Null

Import-CompletionScript 'zoxide.ps1' | Out-Null

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

Import-CompletionScript 'pixi.ps1' | Out-Null

Import-CompletionScript 'uv.ps1' | Out-Null

Import-CompletionScript 'uvx.ps1' | Out-Null

Import-CompletionScript 'skim.ps1' | Out-Null

Import-CompletionScript 'rustup.ps1' | Out-Null

Import-CompletionScript "$env:HOMEDRIVE$env:HOMEPATH\xmake\scripts\profile-win.ps1" | Out-Null

} -MaxTriggerCount 1 -SupportEvent | Out-Null

这里有两块,starshipzoxide这两个是会更改终端体验的,所以不能够放在OnIdle中加载,不然效果会非常奇怪。其他仅提供补全的都可以延迟加载,当然如果小而美的话也可以不延迟。

最终结果#

$baseDir = Split-Path -Parent $PROFILE

$locksDir = Join-Path $baseDir 'Locks'

$completionsDir = Join-Path $baseDir 'Completions'

New-Item -ItemType Directory -Force -Path $locksDir, $completionsDir | Out-Null

113 collapsed lines

function Import-CompletionScript {

param(

[Parameter(Mandatory)]

[string]$OutputFileName

)

$target = [System.IO.Path]::Combine($completionsDir, $OutputFileName)

if (-not [System.IO.File]::Exists($target)) {

return $false

}

. $target

return $true

}

Import-CompletionScript 'starship.ps1' | Out-Null

Import-CompletionScript 'zoxide.ps1' | Out-Null

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

Import-CompletionScript 'pixi.ps1' | Out-Null

Import-CompletionScript 'uv.ps1' | Out-Null

Import-CompletionScript 'uvx.ps1' | Out-Null

Import-CompletionScript 'skim.ps1' | Out-Null

Import-CompletionScript 'rustup.ps1' | Out-Null

Import-CompletionScript "$env:HOMEDRIVE$env:HOMEPATH\xmake\scripts\profile-win.ps1" | Out-Null

} -MaxTriggerCount 1 -SupportEvent | Out-Null

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

# 新时代的补全方法

# 不生成大量 shell scripts,而是动态向应用程序询问

# 但是目前还没有普及,只能说未来可期

Register-ArgumentCompleter -Native -CommandName winget -ScriptBlock {

param($wordToComplete, $commandAst, $cursorPosition)

[Console]::InputEncoding = [System.Text.UTF8Encoding]::new()

[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()

$OutputEncoding = [System.Text.UTF8Encoding]::new()

$word = $wordToComplete.Replace('"', '""')

$ast = $commandAst.ToString().Replace('"', '""')

winget complete --word="$word" --commandline "$ast" --position $cursorPosition |

ForEach-Object {

[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)

}

}

} -MaxTriggerCount 1 -SupportEvent | Out-Null

# update scripts

Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {

$baseDir = Split-Path -Parent $PROFILE

Start-ThreadJob -Name 'completion-cache-update' -ArgumentList $baseDir -ScriptBlock {

param($baseDir)

$ErrorActionPreference = 'Stop'

$locksDir = Join-Path $baseDir 'Locks'

$completionsDir = Join-Path $baseDir 'Completions'

New-Item -ItemType Directory -Force -Path $locksDir, $completionsDir | Out-Null

function Update-CompletionScriptFile {

param(

[Parameter(Mandatory)][string]$CheckCommand,

[Parameter(Mandatory)][string[]]$CheckArgs,

[Parameter(Mandatory)][string]$GenerateCommand,

[Parameter(Mandatory)][string[]]$GenerateArgs,

[Parameter(Mandatory)][string]$OutputFileName

)

$target = Join-Path $completionsDir $OutputFileName

$lockFile = Join-Path $locksDir ($OutputFileName + '.version')

$current = (& $CheckCommand @CheckArgs 2>&1 | Out-String).Trim()

if (-not $current) { throw "Empty version output for $OutputFileName" }

$needsUpdate = $true

if ((Test-Path $lockFile -PathType Leaf) -and (Test-Path $target -PathType Leaf)) {

$prev = (Get-Content $lockFile -Raw).Trim()

if ($prev -eq $current) {

$needsUpdate = $false

}

}

if (-not $needsUpdate) { return }

$body = (& $GenerateCommand @GenerateArgs 2>&1 | Out-String)

if (-not $body.Trim()) { throw "Empty body for $OutputFileName" }

$header = @(

"# generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"

""

) -join [Environment]::NewLine

$tmpTarget = "$target.tmp"

$tmpLock = "$lockFile.tmp"

Set-Content -Path $tmpTarget -Value ($header + $body) -Encoding utf8

Set-Content -Path $tmpLock -Value $current -Encoding utf8

Move-Item -Path $tmpTarget -Destination $target -Force

Move-Item -Path $tmpLock -Destination $lockFile -Force

}

Update-CompletionScriptFile 'starship' @('--version') 'starship' @('init', 'powershell', '--print-full-init') 'starship.ps1'

Update-CompletionScriptFile 'zoxide' @('--version') 'zoxide' @('init', 'powershell') 'zoxide.ps1'

Update-CompletionScriptFile 'pixi' @('--version') 'pixi' @('completion', '--shell', 'powershell') 'pixi.ps1'

Update-CompletionScriptFile 'rustup' @('--version') 'rustup' @('completions', 'powershell') 'rustup.ps1'

Update-CompletionScriptFile 'uv' @('--version') 'uv' @('generate-shell-completion', 'powershell') 'uv.ps1'

Update-CompletionScriptFile 'uvx' @('--version') 'uvx' @('--generate-shell-completion', 'powershell') 'uvx.ps1'

Update-CompletionScriptFile 'sk' @('-V') 'sk' @('--shell', 'power-shell') 'skim.ps1'

} | Out-Null

} -MaxTriggerCount 1 -SupportEvent | Out-Null

使用这种方式后,我再也没有见到过催命500ms+加载提示了。Make Powershell great again!