Optimizing your $Profile - PowerShell Team
Skip to main content
Search<br>Search
No results
Cancel
Steve Lee
Principal Software Engineer Manager
Optimizing your $Profile
Your PowerShell Profile allows you to customize<br>your PowerShell session and runs at startup.<br>Complex profiles can cause a significant delay in the startup of PowerShell as it is a script that needs to be executed before the prompt first<br>shows up.
Spoiler: I’ll show how I got my profile loading time from 1465 ms to 217 ms!
It’s important to note that many of the optmizations I cover here are micro-optimizations.<br>They won’t have significant overall impact and, in general, may not be the best way to write your scripts.<br>In the case of a profile, I’m optimizing for speed and, in some cases, making the script slightly harder to read as a tradeoff.<br>For production scripts used in automation, the micro performance improvement may not be worthwhile as maintainability of the script<br>may be more important.
Using GitHub to store my profile.ps1
As part of working on PowerShell on GitHub, I have a macBook Pro, a Windows desktop, and also a Linux desktop I use frequently.<br>I like to keep my PowerShell environment customizations in sync across these devices without having to manually update the profile should I make changes.<br>The solution I decided upon was to publish my profile as a GitHub Gist.
NOTE: that the current version on GitHub contains all the optimizations covered in this blog post, you can look at the revisions on GitHub<br>to see how my profile has changed over time.<br>I would NOT recommend using my profile directly as I may make changes that break you or are not applicable to your daily usage of PowerShell.<br>Use it more as an example.
The key parts of the code that does this is a ThreadJob that makes a REST call to GitHub<br>to retrieve the latest version of my profile and compare with the current version.<br>Creating the ThreadJob does take time, but because I’m making a networking call which can lead to variable execution time, it’s a worthwhile<br>tradeoff.<br>Also, because that scriptblock is running as a separate thread from the main startup thread, I don’t have to worry about performance optimizations<br>of that ThreadJob scriptblock.
My profile contains a # Version x.y.z comment near the top of the script and this version string is used to determine if the one on GitHub<br>is newer than the one that’s currently running.<br>A regular expression is used to parse this from the profile.<br>If the ThreadJob determines there is a newer version, it saves that version string to a file that I can easily check at the start of my profile.<br>At the start of my profile, I check the current loaded version against this file and will prompt to install the latest version if there is one.<br>This means that on startup, I won’t know that a newer version exists until the next startup, but it’s a worthwhile tradeoff.
Getting a baseline for pwsh
Before we start making changes, we want to get a baseline so we know what impact, if any, our changes are affecting the startup performance.<br>I wanted to separate the startup time of pwsh itself from the execution of my profile.<br>I got an average startup, in milliseconds, for pwsh starting up without loading any profile.
Here I use the Measure-Command cmdlet to make this easy.<br>However, I do a loop of 100 times to make sure any variance of my computer are accounted for and calculate the average time:
$p = 0<br>1..100 | ForEach-Object {<br>Write-Progress -Id 1 -Activity 'pwsh' -PercentComplete $_<br>$p += (Measure-Command {<br>pwsh -noprofile -command 1<br>}).TotalMilliseconds<br>Write-Progress -id 1 -Activity 'profile' -Completed<br>$p = $p/100<br>$p
I’m using the variable $p to store the result as I want to subtract that time from my profile measurements.<br>Since running this 100 times can take some time, I like to know how far it’s progressed, so I’m using Write-Progress as a visual indicator of<br>how many more need to be run.<br>Since the writing of progress is not in the scriptblock used by Measure-Command, it has no impact on the measured time.<br>pwsh -noprofile -command 1 will ensure that when PowerShell starts, it doesn’t load my profile, and the command 1 simply has PowerShell<br>emit the number 1 and exit.
For my baseline, I got a time of 1176 ms for the startup of pwsh.
Getting a baseline for my profile
To get the average start time of my profile, the script is quite similar:
$a = 0<br>1..100 | ForEach-Object {<br>Write-Progress -Id 1 -Activity 'profile' -PercentComplete $_<br>$a += (Measure-Command {<br>pwsh -command 1<br>}).TotalMilliseconds<br>Write-Progress -id 1 -activity 'profile' -Completed<br>$a/100 - $p
The only major difference here is not using -noprofile so that my profile is loaded and also subtracting the startup time of pwsh $p from the result.<br>I got a time of 1465 ms for the startup of my profile.
Measure-Script
Mathias Jessen published a great profiling tool for scripts called PSProfiler<br>that I decided to use against my profile to see which lines were taking...