I get it, in 2025 you're supposed to have unlimited CPU, RAM and other compute resources because you're in "the cloud". Resources are infinite, so why optimize your UI on your windows server when you can just throw more compute at it?

For those of us who choose to manage our own infrastructure, compute comes as a tangible, physical cost. But even if it didn't, why spend money and resources on things you won't need? Not only does running less processes on your server improve performance, it's a good idea from a security perspective.

I setup a 2025 Server in my homelab to test it out, and like all of my experiences with newer Windows OS, I found the UI appalling. Not just the looks, but the usability. Searching for anything takes more than 10 seconds between the index and the web search, and *ANY* action feels delayed and sluggish.

Now, for context, these servers are running 2 vCPU's with 4GB of RAM Each on a 10GbE SSD Storage back plane. Not the largest amount, but nothing that a minimalist server should be struggling with. I also made sure to fully patch, reboot multiple times, install virtualization drivers and let the servers "Breathe" while .net and other processes finish their first run.
After waiting several days with no change in behavior, it annoyed me enough to do something about it. I fired up my local AI and ran through several large prompts, before I settled on a powershell script that tuned the UI down without modifying the system too much. I will say that these results are comparing the same server against itself, and this is also on a bog-standard 2025 Server VM, with no extras like AV or other system add-on's installed.

Here's the Before and After:
Before the Script:
Background Background: 32
Windows Processes: 85

After the script:
Background Background: 25
Windows Processes: 77

That's almost a 10% Reduction in RAM and 13% reduction in active processes! May not seem like a lot, but on a smaller VM, it can be the difference between needing to increase compute (and cost), or keeping it within a manageable range.
For those interested in the script and a breakdown of how it works, keep reading! (This section is generated by AI and modified by me).
Also, as always, test things like code from the internet in a test environment to ensure it will work for you before applying to any production servers. I am not responsible if you cause an outage because you Copy/Paste things from a blog into your production environment without checking it first.

<#
.SYNOPSIS
Windows Server 2025 GUI performance tuner: disables non-required services and dials down UI animations/effects.
.DESCRIPTION
- Backs up service start types and relevant UI registry keys (HKCU).
- Applies conservative baseline (Manual/Disabled on commonly unnecessary services).
- Optionally applies a slightly more aggressive set (-Aggressive).
- Sets "Adjust for best performance" visual effects for current user, or for all loaded profiles + Default User when -IncludeAllProfiles is used.
- Revert supported via -Revert.
.NOTES
Run in elevated PowerShell. Reboot/logoff recommended after applying.
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param(
[switch]$Aggressive,
[switch]$IncludeAllProfiles,
[string]$BackupPath = "C:\PerfTuningBackup",
[switch]$Revert
)
function Assert-Admin {
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw "Please run this script from an elevated (Administrator) PowerShell session."
}
}
function Ensure-Path([string]$Path) {
if (-not (Test-Path -LiteralPath $Path)) {
New-Item -Path $Path -ItemType Directory -Force | Out-Null
}
}
function Export-Registry {
param(
[Parameter(Mandatory)][string]$RegPath,
[Parameter(Mandatory)][string]$OutFile
)
$parent = Split-Path -Parent $OutFile
Ensure-Path $parent
& reg.exe export $RegPath $OutFile /y | Out-Null
}
function Get-ServiceSafe {
param([string]$Name)
try { Get-Service -Name $Name -ErrorAction Stop } catch { $null }
}
function Save-ServiceStartTypes {
param([string[]]$ServiceNames, [string]$OutFile)
$data = foreach ($n in $ServiceNames) {
$svc = Get-ServiceSafe -Name $n
if ($svc) {
$wmi = Get-CimInstance -ClassName Win32_Service -Filter "Name='$n'"
[pscustomobject]@{
Name = $n
StartMode = $wmi.StartMode # Auto, Manual, Disabled
State = $wmi.State # Running, Stopped
}
}
}
$data | ConvertTo-Json -Depth 3 | Set-Content -Path $OutFile -Encoding UTF8
}
function Restore-ServiceStartTypes {
param([string]$InFile)
if (-not (Test-Path $InFile)) { throw "Backup file not found: $InFile" }
$items = Get-Content $InFile -Raw | ConvertFrom-Json
foreach ($i in $items) {
$name = $i.Name
$mode = $i.StartMode
if ($PSCmdlet.ShouldProcess($name, "Set StartMode -> $mode")) {
try {
$svc = Get-CimInstance -ClassName Win32_Service -Filter "Name='$name'"
if ($null -ne $svc) {
Invoke-CimMethod -InputObject $svc -MethodName ChangeStartMode -Arguments @{ StartMode = $mode } | Out-Null
if ($i.State -eq 'Running' -and (Get-ServiceSafe $name).Status -ne 'Running') {
Start-Service -Name $name -ErrorAction SilentlyContinue
}
if ($i.State -ne 'Running' -and (Get-ServiceSafe $name).Status -eq 'Running') {
Stop-Service -Name $name -Force -ErrorAction SilentlyContinue
}
}
} catch {
Write-Warning "Failed to restore $($name): $($_.Exception.Message)"
}
}
}
}
function Set-ServiceStart {
param(
[Parameter(Mandatory)][string]$Name,
[ValidateSet('Automatic','Manual','Disabled')][string]$StartMode,
[switch]$StopIfRunning
)
$svc = Get-ServiceSafe -Name $Name
if (-not $svc) { return }
try {
$cim = Get-CimInstance -ClassName Win32_Service -Filter "Name='$Name'"
if ($cim) {
if ($PSCmdlet.ShouldProcess($Name, "ChangeStartMode -> $StartMode")) {
Invoke-CimMethod -InputObject $cim -MethodName ChangeStartMode -Arguments @{ StartMode = $StartMode } | Out-Null
}
if ($StopIfRunning -and $svc.Status -eq 'Running') {
if ($PSCmdlet.ShouldProcess($Name, "Stop-Service")) {
Stop-Service -Name $Name -Force -ErrorAction SilentlyContinue
}
}
}
} catch {
Write-Warning "Could not change $($Name): $($_.Exception.Message)"
}
}
function Set-HighPerformancePowerPlan {
Write-Verbose "Enabling High Performance (or Ultimate) power plan"
try {
# Try Ultimate Performance; else fall back to High performance
$ultimate = 'e9a42b02-d5df-448d-aa00-03f14749eb61'
$plans = (powercfg -list) 2>$null
if ($plans -match $ultimate) {
powercfg -setactive $ultimate | Out-Null
} else {
powercfg -duplicatescheme $ultimate 2>$null
powercfg -setactive $ultimate 2>$null
if ($LASTEXITCODE -ne 0) {
$high = (powercfg -list) -match 'High performance'
if ($high) {
$guid = ($high | Select-String -Pattern 'GUID:\s+([a-f0-9-]+)' -AllMatches).Matches[0].Groups[1].Value
powercfg -setactive $guid | Out-Null
}
}
}
} catch {
Write-Warning "Unable to set power plan: $($_.Exception.Message)"
}
}
function Set-VisualEffectsBestPerformanceForHive {
param(
[Parameter(Mandatory)][Microsoft.Win32.RegistryKey]$Root
)
# Absolute registry paths for UI performance tweaks
$veKey = "Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
$advKey = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
$dwmKey = "Software\Microsoft\Windows\DWM"
$deskKey = "Control Panel\Desktop"
$themePers = "Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
$rkVE = $Root.CreateSubKey($veKey, $true)
$rkAdv = $Root.CreateSubKey($advKey, $true)
$rkDWM = $Root.CreateSubKey($dwmKey, $true)
$rkDesk = $Root.CreateSubKey($deskKey, $true)
$rkPer = $Root.CreateSubKey($themePers, $true)
# 2 = Adjust for best performance
$rkVE.SetValue('VisualFXSetting', 2, 'DWord')
# Disable common animations/shadows/transparency
$rkAdv.SetValue('TaskbarAnimations', 0, 'DWord')
$rkAdv.SetValue('ListviewAlphaSelect', 0, 'DWord')
$rkAdv.SetValue('ListviewShadow', 0, 'DWord')
$rkAdv.SetValue('IconsOnly', 0, 'DWord')
$rkDWM.SetValue('EnableAeroPeek', 0, 'DWord')
$rkDWM.SetValue('EnableWindowColorization', 0, 'DWord')
$rkPer.SetValue('EnableTransparency', 0, 'DWord')
# Desktop tweaks
$rkDesk.SetValue('DragFullWindows', '0', 'String')
$rkDesk.SetValue('MenuShowDelay', '0', 'String')
$rkDesk.SetValue('FontSmoothing', '0', 'String')
$rkDesk.SetValue('UserPreferencesMask', ([byte[]](0x90,0x12,0x03,0x80,0x10,0x00,0x00,0x00)), 'Binary')
$rkDesk.SetValue('MinAnimate', '0', 'String')
$rkVE.Close(); $rkAdv.Close(); $rkDWM.Close(); $rkDesk.Close(); $rkPer.Close()
}
function Apply-VisualEffects {
param([switch]$AllProfiles)
Write-Verbose "Applying 'best performance' visual effects..."
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$hkcuDump = Join-Path $BackupPath "HKCU-UI-$timestamp.reg"
Export-Registry -RegPath "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer" -OutFile $hkcuDump
Export-Registry -RegPath "HKCU\Control Panel\Desktop" -OutFile (Join-Path $BackupPath "HKCU-Desktop-$timestamp.reg")
Export-Registry -RegPath "HKCU\Software\Microsoft\Windows\DWM" -OutFile (Join-Path $BackupPath "HKCU-DWM-$timestamp.reg")
Export-Registry -RegPath "HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize" -OutFile (Join-Path $BackupPath "HKCU-Personalize-$timestamp.reg")
# Current user
$cu = [Microsoft.Win32.Registry]::CurrentUser
Set-VisualEffectsBestPerformanceForHive -Root $cu
if ($AllProfiles) {
# For each loaded user hive under HKU (ignore .DEFAULT and *_Classes)
$hku = [Microsoft.Win32.Registry]::Users
foreach ($sid in $hku.GetSubKeyNames() | Where-Object { $_ -notmatch '^\.DEFAULT$' -and $_ -notmatch '_Classes$' }) {
try {
$root = $hku.OpenSubKey($sid, $true)
if ($null -ne $root) {
Set-VisualEffectsBestPerformanceForHive -Root $root
$root.Close()
}
} catch {
Write-Warning "Could not update UI settings for HKU\$sid : $($_.Exception.Message)"
}
}
# Default User (for new profiles)
$defNtuser = "C:\Users\Default\NTUSER.DAT"
if (Test-Path $defNtuser) {
$mount = "HKU\__DEFAULT__"
& reg.exe load $mount $defNtuser | Out-Null
try {
$root = [Microsoft.Win32.Registry]::Users.OpenSubKey("__DEFAULT__", $true)
Set-VisualEffectsBestPerformanceForHive -Root $root
$root.Close()
} finally {
& reg.exe unload $mount | Out-Null
}
}
}
# Best-effort refresh of Explorer/DWM for current session
try { Stop-Process -Name explorer -Force -ErrorAction SilentlyContinue } catch {}
}
function Apply-ServiceTuning {
# Baseline set (conservative). Adjust for your roles.
$baseline = @(
@{ Name='WSearch'; Mode='Disabled'; Stop=$true } # Windows Search indexing
@{ Name='SysMain'; Mode='Disabled'; Stop=$true } # Superfetch/Prefetcher
@{ Name='DiagTrack'; Mode='Disabled'; Stop=$true } # Telemetry
@{ Name='WerSvc'; Mode='Manual'; Stop=$true } # Error Reporting
@{ Name='MapsBroker'; Mode='Disabled'; Stop=$true } # Offline maps
@{ Name='DoSvc'; Mode='Manual'; Stop=$true } # Delivery Optimization (Manual keeps on-demand)
@{ Name='Fax'; Mode='Disabled'; Stop=$true } # Fax
@{ Name='Spooler'; Mode='Manual'; Stop=$false } # Print Spooler (Manual safer)
@{ Name='RemoteRegistry'; Mode='Disabled'; Stop=$true } # Harden + tiny perf gain
@{ Name='SSDPSRV'; Mode='Disabled'; Stop=$true } # SSDP Discovery
@{ Name='upnphost'; Mode='Disabled'; Stop=$true } # UPnP
@{ Name='WMPNetworkSvc'; Mode='Disabled'; Stop=$true } # Media sharing (if present)
@{ Name='TrkWks'; Mode='Manual'; Stop=$false } # Link Tracking
@{ Name='ShellHWDetection'; Mode='Manual'; Stop=$false } # Portable device detection
)
# Optional extras (disable when you’re sure they’re not needed)
$more = @(
@{ Name='bthserv'; Mode='Disabled'; Stop=$true } # Bluetooth
@{ Name='tiledatamodelsvc'; Mode='Disabled'; Stop=$true } # Legacy tile cache
@{ Name='RetailDemo'; Mode='Disabled'; Stop=$true } # Retail demo
@{ Name='OneSyncSvc'; Mode='Disabled'; Stop=$true } # Old sync service (if present)
@{ Name='TabletInputService'; Mode='Disabled'; Stop=$true } # Touch/ink (older builds)
@{ Name='TextInputManagementService'; Mode='Disabled'; Stop=$true } # Touch/ink (newer builds)
)
$serviceList = if ($Aggressive) { $baseline + $more } else { $baseline }
# Backup current start types
$svcBackupFile = Join-Path $BackupPath "service-startmodes-$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
Save-ServiceStartTypes -ServiceNames ($serviceList.Name) -OutFile $svcBackupFile
# Apply changes
foreach ($item in $serviceList) {
Set-ServiceStart -Name $item.Name -StartMode $item.Mode -StopIfRunning:$($item.Stop)
}
}
# ------------------------ MAIN ------------------------
try {
Assert-Admin
Ensure-Path $BackupPath
if ($Revert) {
$svc = Get-ChildItem $BackupPath -Filter 'service-startmodes-*.json' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $svc) { throw "No service backup found in $BackupPath." }
Write-Host "Reverting service start types from $($svc.FullName)..." -ForegroundColor Yellow
Restore-ServiceStartTypes -InFile $svc.FullName
$regFiles = Get-ChildItem $BackupPath -Filter 'HKCU-*.reg' | Sort-Object LastWriteTime -Descending
if ($regFiles) {
Write-Host "Restoring latest HKCU UI registry backups..." -ForegroundColor Yellow
foreach ($rf in $regFiles | Select-Object -First 4) { & reg.exe import $rf.FullName | Out-Null }
} else {
Write-Warning "No HKCU registry backups found to restore UI settings."
}
Write-Host "Revert complete. Consider logging off/on or rebooting." -ForegroundColor Green
return
}
Write-Host "Applying Windows Server 2025 performance tuning..." -ForegroundColor Cyan
Set-HighPerformancePowerPlan
Apply-ServiceTuning
Apply-VisualEffects -AllProfiles:$IncludeAllProfiles
Write-Host "`nDone. A reboot is recommended to fully apply UI changes." -ForegroundColor Green
Write-Host "Backups saved in: $BackupPath" -ForegroundColor DarkGreen
} catch {
Write-Error $_.Exception.Message
exit 1
}
1. Admin Check
Assert-Admin
Ensures the script is running as Administrator; required for modifying services, registry, and power plans.
2. Backup Utilities
Functions like Save-ServiceStartTypes and Export-Registry capture the current state of services and UI settings. Backups are stored in C:\PerfTuningBackup by default.
3. Service Tuning
Apply-ServiceTuning
Disables or sets to Manual services that aren’t typically needed on a server:
- WSearch (Windows Search) – Stops indexing, saves CPU/IO.
- SysMain (Superfetch) – Prefetching not useful on servers.
- DiagTrack (Telemetry) – Usage data collection.
- WerSvc (Error Reporting) – Set to Manual; only triggers if needed.
- MapsBroker, Fax, SSDP, UPnP, Media Sharing – Consumer-oriented services.
- RemoteRegistry – Security hardening plus minor performance benefit.
- Print Spooler – Set to Manual unless printing is required.
With -Aggressive, it also disables:
- Bluetooth (bthserv)
- Tablet Input/Ink services
- Retail Demo
- Legacy Sync services
Each change is reversible using -Revert.
4. Power Plan Optimization
Set-HighPerformancePowerPlan
Forces the system to use Ultimate Performance (if available) or High Performance mode. Prevents CPU throttling and ensures consistent responsiveness.
5. Visual Effects
Apply-VisualEffects
Sets “Adjust for best performance” for the GUI:
- Disables taskbar animations, shadows, transparency.
- Turns off font smoothing and full window drag previews.
- Reduces menu show delay for instant response.
- Applies to current user by default, but with
-IncludeAllProfilesit also applies to all existing profiles and the Default User template.
6. Reversion Logic
.\Tune-Server2025Performance.ps1 -Revert
Imports the saved backups, restores service start types, and re-applies registry settings. This ensures you can experiment safely.
Happy tuning!
