Lazy Load PowerShell Module

Last updated at
Ref:
Speed Up PowerShell Startup, Without Sacrificing Functionality

Windows users are often in trouble with not having a convenient shell —

Well, the second one comes to me when I installed posh-git and it costs me 600~800ms to wait in starting every pwsh session! Try measure it by yourself:

Measure-Command { Import-Module posh-git }

Tip: use pwsh --NoProfile to start a powershell session without profile.

To lazy load a module, we need to find a way to do async works. It can’t be Start-Job in that jobs are running in different context and can not affect the main UI thread. The solution is that we create a new powershell script and run it in async.

If we need more ability to control multithread jobs, we may use runspace to manage them.

Runspace is the new (it’s old but I just knew it) way to do async jobs in powershell. In old days we do Start-Job, but jobs are running in another powershell processes. Read Beginning Use of PowerShell Runspaces if you want to know more.

Here is the final profile.ps1:

$LazyLoadProfile = [PowerShell]::Create()
[void]$LazyLoadProfile.AddScript(@'
    Import-Module posh-git
'@)
[void]$LazyLoadProfile.BeginInvoke()

$null = Register-ObjectEvent -InputObject $LazyLoadProfile -EventName InvocationStateChanged -Action {
    Import-Module posh-git
    $global:GitPromptSettings.DefaultPromptPrefix.Text = 'PS '
    $global:GitPromptSettings.DefaultPromptBeforeSuffix.Text = '`n'
    $LazyLoadProfile.Dispose()
}

Let’s dig into it.

Script Object

[PowerShell]::Create()

It creates a PowerShell object. What’s that mean? It sounds like we just created an empty a.ps1. That’s right, we can write scripts to that file —

[void]$LazyLoadProfile.AddScript(@'114514'@)

But we have not invoke it yet. I added [void] here to hide its result because AddScript returns something which will be printed to the screen.

To invoke it, call Invoke() or BeginInvoke(). The difference is the first one executes code in sync and the second one is async. Neither of them would affect our main UI thread.

Event Listener

PowerShell is nothing but another .NET language. Thus we are able to do something on some event happen. We can check what event we could see on a powershell object:

$LazyLoadProfile | Get-Member -MemberType Event

Which gives us some interesting event name:

Name
----
InvocationStateChanged

In the end, we added our event listener with Register-ObjectEvent.

Conclusion

To speed up your powershell / pwsh startup speed, refactor your profile.ps1 into this form:

$LazyLoadProfile = [PowerShell]::Create()
[void]$LazyLoadProfile.AddScript(@'
    # scripts in this string will run in async
'@)
[void]$LazyLoadProfile.BeginInvoke()

$null = Register-ObjectEvent -InputObject $LazyLoadProfile -EventName InvocationStateChanged -Action {
    # scripts in this block will run in main thread when the job is done
    # don't forget to dispose
    $LazyLoadProfile.Dispose()
}