Contact Us
Optimize Server 2025 UI Performance

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?

How I'm sure my co-workers see me these days.


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 think we know where this is going.

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.

Oddly Relevant...

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.

My Project Management Approach.

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.

Don't learn this too late.

<# 
.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 -IncludeAllProfiles it 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!