← All Tutorials

Windows VDI Optimization with PowerShell

Infrastructure & DevOps Intermediate 52 min read #14

Windows VDI Optimization with PowerShell

Automated Script That Applies 37+ Optimizations and Frees 6.4GB+ Disk Space


What you will build: A comprehensive, production-ready PowerShell script that transforms a stock Windows Server or Windows Desktop installation into a lean, high-performance VDI (Virtual Desktop Infrastructure) machine. The script disables 20 unnecessary services, blocks telemetry at multiple levels, cleans gigabytes of cached files, optimizes memory management, tunes network settings, and configures power plans — all in a single idempotent run that takes under 60 seconds.

Who this is for: System administrators managing Windows VM fleets, VDI engineers, MSPs deploying remote desktops, and anyone running Windows VMs in the cloud who wants to reduce resource waste and improve responsiveness.

Prerequisites: Windows Server 2019/2022/2025 or Windows 10/11 Pro, PowerShell 5.1+, and Administrator privileges. No external modules or dependencies required.

Time to build: 1-2 hours to understand and customize the script, 60 seconds to execute per machine. Scales to hundreds of VMs via remote execution.


Table of Contents

  1. The Problem: Bloated Windows VMs
  2. Architecture and Design Principles
  3. The Logging Framework
  4. Section 1: Disabling Unnecessary Services (20 Services)
  5. Section 2: High Performance Power Plan
  6. Section 3: Disabling Visual Effects
  7. Section 4: Cortana, Tips, and Suggestions
  8. Section 5: Background Apps
  9. Section 6: Telemetry and Diagnostics
  10. Section 7: Windows Update Control
  11. Section 8: Temp File and Cache Cleanup
  12. Section 9: Hibernation
  13. Section 10: Windows Defender Tuning
  14. Section 11: Scheduled Task Cleanup
  15. Section 12: Chrome Optimization
  16. Section 13: Network Optimization
  17. Section 14: Memory Optimization
  18. The Summary Report
  19. Complete Script
  20. Deploying at Scale
  21. Results and Benchmarks
  22. Troubleshooting

1. The Problem: Bloated Windows VMs {#1-the-problem}

A default Windows Server or Windows 10/11 installation is designed for a general-purpose desktop user. It comes pre-loaded with features that are actively harmful in a VDI or cloud VM context:

On a typical 4-core, 8 GB Windows Server 2025 VM, these background processes collectively consume:

Resource Default After Optimization
Idle RAM usage ~3.2 GB ~1.8 GB
Idle CPU 5-12% 0.5-2%
Disk space wasted 6-10 GB Reclaimed
Background services 85+ 55-60
Startup time 45-60s 25-35s

This tutorial walks through every optimization, explains why each change matters, and gives you the complete script to apply them all in one shot.


2. Architecture and Design Principles {#2-architecture}

Before writing a single line of code, we established these design rules:

Idempotent Execution

The script checks the current state before making changes. Running it twice produces the same result — no errors, no duplicate work, just [SKIP] messages for things already done. This is critical for automation: you can schedule this script to run at VM startup via Group Policy and it will not break anything.

Categorized Changes

Each optimization belongs to a numbered section (1-14). This makes the script easy to audit, easy to customize (comment out a section you do not want), and easy to extend.

Comprehensive Logging

Every action produces one of four log levels:

[CHANGED]  — Something was modified
[SKIP]     — Already in the desired state
[ERROR]    — Something failed (non-fatal)
[INFO]     — Section headers and context

At the end, the script prints a summary of all changes made and all errors encountered.

Non-Destructive

The script disables and stops services — it does not uninstall or delete Windows components. Every change can be reversed by re-enabling the service or deleting the registry key. The only truly "destructive" action is deleting temp files and caches, which Windows will regenerate as needed.

Architecture Diagram

┌─────────────────────────────────────────────────────────┐
│                  optimize-windows.ps1                     │
│                                                           │
│  ┌─────────────────┐  ┌─────────────────────────────┐    │
│  │ Logging Framework│  │  Error Handling              │    │
│  │ Log-Change       │  │  $ErrorActionPreference =    │    │
│  │ Log-Skip         │  │   "Continue"                 │    │
│  │ Log-Error        │  │  Try/Catch per operation     │    │
│  │ Log-Info         │  │  Non-fatal by default        │    │
│  └────────┬────────┘  └──────────────┬───────────────┘    │
│           │                          │                     │
│           ▼                          ▼                     │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              14 Optimization Sections                │  │
│  │                                                      │  │
│  │  1. Services (20)      8. Temp cleanup               │  │
│  │  2. Power plan         9. Hibernation                │  │
│  │  3. Visual effects    10. Windows Defender            │  │
│  │  4. Cortana/Tips      11. Scheduled tasks (10)       │  │
│  │  5. Background apps   12. Chrome policies            │  │
│  │  6. Telemetry         13. Network tuning             │  │
│  │  7. Windows Update    14. Memory optimization        │  │
│  └─────────────────────────────────────────────────────┘  │
│                          │                                 │
│                          ▼                                 │
│  ┌─────────────────────────────────────────────────────┐  │
│  │              Summary Report                          │  │
│  │  - Total changes made                                │  │
│  │  - Errors encountered                                │  │
│  │  - Disk free space                                   │  │
│  │  - Reboot required items                             │  │
│  └─────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

3. The Logging Framework {#3-logging-framework}

Every good operations script needs structured logging. We start with four functions that track changes and errors in arrays for the final summary.

#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Windows Server Performance Optimization Script
.DESCRIPTION
    Disables unnecessary services, visual effects, telemetry, and background tasks
    for optimal performance on VDI/server workloads. Idempotent - safe to run multiple times.
.NOTES
    Target: Windows Server 2019/2022/2025 or Windows 10/11 Pro
#>

$ErrorActionPreference = "Continue"
$changes = @()
$errors = @()

function Log-Change {
    param([string]$Message)
    $script:changes += $Message
    Write-Host "[CHANGED] $Message" -ForegroundColor Green
}

function Log-Skip {
    param([string]$Message)
    Write-Host "[SKIP] $Message" -ForegroundColor Yellow
}

function Log-Error {
    param([string]$Message)
    $script:errors += $Message
    Write-Host "[ERROR] $Message" -ForegroundColor Red
}

function Log-Info {
    param([string]$Message)
    Write-Host "[INFO] $Message" -ForegroundColor Cyan
}

Key design decisions:

The script begins with a banner showing the timestamp:

Write-Host "============================================" -ForegroundColor White
Write-Host " Windows Server 2025 Optimization Script" -ForegroundColor White
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor White
Write-Host "============================================" -ForegroundColor White
Write-Host ""

4. Section 1: Disabling Unnecessary Services (20 Services) {#4-services}

This is the highest-impact section. Windows ships with dozens of services that have no business running on a VDI machine. Each one consumes RAM (its own process or svchost share), may use CPU on a schedule, and often has network activity.

The Service List

Log-Info "=== SECTION 1: Disabling unnecessary services ==="

$servicesToDisable = @(
    @{ Name = "SysMain";            Desc = "SysMain (Superfetch) - prefetching, wastes RAM on servers" },
    @{ Name = "WSearch";            Desc = "Windows Search Indexer - CPU/disk intensive" },
    @{ Name = "Spooler";            Desc = "Print Spooler - not needed on VDI/server" },
    @{ Name = "DiagTrack";          Desc = "Connected User Experiences and Telemetry" },
    @{ Name = "dmwappushservice";   Desc = "WAP Push Message Routing - telemetry helper" },
    @{ Name = "MapsBroker";         Desc = "Downloaded Maps Manager" },
    @{ Name = "lfsvc";              Desc = "Geolocation Service" },
    @{ Name = "WMPNetworkSvc";      Desc = "Windows Media Player Network Sharing" },
    @{ Name = "XblAuthManager";     Desc = "Xbox Live Auth Manager" },
    @{ Name = "XblGameSave";        Desc = "Xbox Live Game Save" },
    @{ Name = "XboxGipSvc";         Desc = "Xbox Accessory Management" },
    @{ Name = "XboxNetApiSvc";      Desc = "Xbox Live Networking" },
    @{ Name = "Fax";                Desc = "Fax Service" },
    @{ Name = "RetailDemo";         Desc = "Retail Demo Service" },
    @{ Name = "wisvc";              Desc = "Windows Insider Service" },
    @{ Name = "WerSvc";             Desc = "Windows Error Reporting" },
    @{ Name = "TabletInputService"; Desc = "Touch Keyboard and Handwriting Panel" },
    @{ Name = "PhoneSvc";           Desc = "Phone Service" },
    @{ Name = "icssvc";             Desc = "Windows Mobile Hotspot Service" },
    @{ Name = "WbioSrvc";          Desc = "Windows Biometric Service" }
)

Why each service is disabled:

Service Why It Wastes Resources
SysMain Pre-loads frequently used apps into RAM. On a VDI with 1-2 apps, it just wastes 200-500 MB.
WSearch Indexes every file on disk. Burns 5-15% CPU during indexing. Nobody searches on a VDI.
Spooler Print spooler. Also a frequent attack vector (PrintNightmare CVE-2021-34527).
DiagTrack Primary telemetry service. Uploads diagnostic data to Microsoft.
dmwappushservice Helper for DiagTrack. Routes WAP push messages for telemetry.
MapsBroker Downloads and updates offline maps. Zero use on a server.
lfsvc GPS/Geolocation. A cloud VM does not have GPS.
WMPNetworkSvc Windows Media Player sharing over the network.
Xbox (4 services) Xbox gaming infrastructure. Installed by default even on Server.
Fax Fax service. It is not 1995.
RetailDemo Demo mode for retail display machines.
wisvc Windows Insider builds. You do not want preview builds on production VMs.
WerSvc Collects and sends crash reports to Microsoft.
TabletInputService Touch keyboard. VDI users have physical keyboards.
PhoneSvc Telephony API for phone calls.
icssvc Mobile hotspot. A VM cannot share Wi-Fi.
WbioSrvc Fingerprint/biometric authentication. VMs do not have fingerprint readers.

The Disable Loop

foreach ($svc in $servicesToDisable) {
    $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
    if (-not $service) {
        Log-Skip "$($svc.Desc) - service not found"
        continue
    }

    $changed = $false

    # Stop if running
    if ($service.Status -eq 'Running') {
        try {
            Stop-Service -Name $svc.Name -Force -ErrorAction Stop
            $changed = $true
        } catch {
            Log-Error "Failed to stop $($svc.Name): $_"
        }
    }

    # Disable startup
    if ($service.StartType -ne 'Disabled') {
        try {
            Set-Service -Name $svc.Name -StartupType Disabled -ErrorAction Stop
            $changed = $true
        } catch {
            Log-Error "Failed to disable $($svc.Name): $_"
        }
    }

    if ($changed) {
        Log-Change "Disabled $($svc.Desc)"
    } else {
        Log-Skip "$($svc.Desc) - already disabled/stopped"
    }
}

How the idempotency works:

  1. Check if the service exists (Get-Service). If not, skip it. This handles differences between Windows editions (Server Core does not have Xbox services, for example).
  2. Check if it is running. Only stop it if it is.
  3. Check if the startup type is already Disabled. Only change it if it is not.
  4. Only log [CHANGED] if either the stop or the disable actually did something.

5. Section 2: High Performance Power Plan {#5-power-plan}

Windows ships with three built-in power plans. The default "Balanced" plan throttles CPU frequency to save energy — great for a laptop on battery, terrible for a VM where you are paying for CPU cores whether you use them or not.

Log-Info "=== SECTION 2: Power plan ==="

$currentPlan = powercfg /getactivescheme 2>&1
if ($currentPlan -match "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c") {
    Log-Skip "Power plan already set to High Performance"
} else {
    try {
        powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
        Log-Change "Set power plan to High Performance"
    } catch {
        Log-Error "Failed to set power plan: $_"
    }
}

# Disable monitor sleep and system sleep on AC
powercfg /change monitor-timeout-ac 0
powercfg /change standby-timeout-ac 0
powercfg /change hibernate-timeout-ac 0

The GUID 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c is the well-known identifier for the "High Performance" power plan. It is the same on every Windows installation — you do not need to look it up.

Why disable sleep timeouts? A cloud VM should never sleep. If the VM goes to standby, the RDP session disconnects and the user has to contact IT to wake it up. Setting all timeouts to 0 means "never sleep."


6. Section 3: Disabling Visual Effects {#6-visual-effects}

Visual effects are the most noticeable optimization for RDP users. Animations, transparency, and smooth scrolling all require rendering that must be transmitted over the network. Disabling them makes the session feel significantly snappier.

Log-Info "=== SECTION 3: Visual effects ==="

# Set to "Adjust for best performance"
$vfxPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
if (-not (Test-Path $vfxPath)) {
    New-Item -Path $vfxPath -Force | Out-Null
}
$currentVFX = (Get-ItemProperty -Path $vfxPath -Name "VisualFXSetting" `
    -ErrorAction SilentlyContinue).VisualFXSetting
if ($currentVFX -ne 2) {
    Set-ItemProperty -Path $vfxPath -Name "VisualFXSetting" -Value 2 -Type DWord
    Log-Change "Set VisualFXSetting to 'Best Performance' (2)"
} else {
    Log-Skip "VisualFXSetting already set to Best Performance"
}

The VisualFXSetting registry value has three options:

Setting it to 2 is the equivalent of going to System Properties > Advanced > Performance Settings and selecting "Adjust for best performance."

Fine-Grained Animation Controls

The global setting does most of the work, but we also explicitly disable individual effects via the Explorer Advanced key and the Desktop Window Manager:

$advPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
$dwmPath = "HKCU:\Software\Microsoft\Windows\DWM"

# Disable specific animations
$animSettings = @{
    "TaskbarAnimations"   = 0
    "ListviewAlphaSelect" = 0
    "ListviewShadow"      = 0
    "IconsOnly"           = 1   # Show icons only, no thumbnails
}
foreach ($key in $animSettings.Keys) {
    $current = (Get-ItemProperty -Path $advPath -Name $key `
        -ErrorAction SilentlyContinue).$key
    if ($current -ne $animSettings[$key]) {
        Set-ItemProperty -Path $advPath -Name $key -Value $animSettings[$key] -Type DWord
        Log-Change "Set $key = $($animSettings[$key])"
    }
}

# Disable transparency effects (Aero Glass)
if (Test-Path $dwmPath) {
    $currentTransparency = (Get-ItemProperty -Path $dwmPath -Name "EnableTransparency" `
        -ErrorAction SilentlyContinue).EnableTransparency
    if ($currentTransparency -ne 0) {
        Set-ItemProperty -Path $dwmPath -Name "EnableTransparency" -Value 0 -Type DWord
        Log-Change "Disabled transparency effects"
    }
}

Desktop Performance Settings

These registry values control window drag behavior, menu delays, and the master animation switches:

$perfPath = "HKCU:\Control Panel\Desktop"

# Disable drag full windows (shows outline only when dragging)
$currentDragFull = (Get-ItemProperty -Path $perfPath -Name "DragFullWindows" `
    -ErrorAction SilentlyContinue).DragFullWindows
if ($currentDragFull -ne "0") {
    Set-ItemProperty -Path $perfPath -Name "DragFullWindows" -Value "0"
    Log-Change "Disabled drag full windows"
}

# Disable smooth scrolling
Set-ItemProperty -Path $perfPath -Name "SmoothScroll" -Value 0 `
    -Type DWord -ErrorAction SilentlyContinue

# Set menu show delay to 0 (instant menus)
$currentMenuAnim = (Get-ItemProperty -Path $perfPath -Name "MenuShowDelay" `
    -ErrorAction SilentlyContinue).MenuShowDelay
if ($currentMenuAnim -ne "0") {
    Set-ItemProperty -Path $perfPath -Name "MenuShowDelay" -Value "0"
    Log-Change "Set menu show delay to 0"
}

# Disable minimize/maximize animations
Set-ItemProperty -Path $perfPath -Name "MinAnimate" -Value "0" -ErrorAction SilentlyContinue

# UserPreferencesMask — the master bitmap that controls all visual effects
Set-ItemProperty -Path $perfPath -Name "UserPreferencesMask" `
    -Value ([byte[]](0x90,0x12,0x03,0x80,0x10,0x00,0x00,0x00)) `
    -Type Binary -ErrorAction SilentlyContinue
Log-Change "Set UserPreferencesMask for best performance"

About UserPreferencesMask: This is a binary value where each bit controls a specific visual effect (animate windows, fade menus, show shadows, etc.). The value 0x90,0x12,0x03,0x80,0x10,0x00,0x00,0x00 disables all cosmetic effects while keeping functional behaviors intact. This is the exact byte sequence that Windows writes when you select "Adjust for best performance" in the GUI.


7. Section 4: Cortana, Tips, and Suggestions {#7-cortana-tips}

Windows aggressively promotes its own features through notifications, suggested apps in the Start menu, and Cortana integration. All of this consumes CPU, network, and screen real estate.

Log-Info "=== SECTION 4: Cortana, Tips, Suggestions ==="

# Disable Cortana via Group Policy registry key
$cortanaPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search"
if (-not (Test-Path $cortanaPath)) {
    New-Item -Path $cortanaPath -Force | Out-Null
}
$currentCortana = (Get-ItemProperty -Path $cortanaPath -Name "AllowCortana" `
    -ErrorAction SilentlyContinue).AllowCortana
if ($currentCortana -ne 0) {
    Set-ItemProperty -Path $cortanaPath -Name "AllowCortana" -Value 0 -Type DWord
    Log-Change "Disabled Cortana"
} else {
    Log-Skip "Cortana already disabled"
}

Content Delivery Manager

This is the system responsible for "suggested apps" in the Start menu, lock screen tips, "Get Office" notifications, and silently installing apps like Candy Crush:

$contentDeliveryPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager"
if (-not (Test-Path $contentDeliveryPath)) {
    New-Item -Path $contentDeliveryPath -Force | Out-Null
}

$tipSettings = @{
    "SystemPaneSuggestionsEnabled"       = 0   # Start menu suggestions
    "SoftLandingEnabled"                 = 0   # Tips about Windows features
    "SubscribedContent-338389Enabled"    = 0   # Windows tips notifications
    "SubscribedContent-310093Enabled"    = 0   # Get Office suggestions
    "SubscribedContent-338388Enabled"    = 0   # Suggested apps in Start
    "SubscribedContent-338393Enabled"    = 0   # Suggested apps in Start
    "SubscribedContent-353694Enabled"    = 0   # Suggested apps in Settings
    "SubscribedContent-353696Enabled"    = 0   # Suggested apps in Settings
    "SilentInstalledAppsEnabled"         = 0   # Auto-install sponsored apps
    "OemPreInstalledAppsEnabled"         = 0   # OEM bloatware
    "PreInstalledAppsEnabled"            = 0   # Pre-installed app suggestions
    "FeatureManagementEnabled"           = 0   # Feature experiment management
}

$tipsChanged = $false
foreach ($key in $tipSettings.Keys) {
    $current = (Get-ItemProperty -Path $contentDeliveryPath -Name $key `
        -ErrorAction SilentlyContinue).$key
    if ($current -ne $tipSettings[$key]) {
        Set-ItemProperty -Path $contentDeliveryPath -Name $key `
            -Value $tipSettings[$key] -Type DWord
        $tipsChanged = $true
    }
}
if ($tipsChanged) {
    Log-Change "Disabled tips, suggestions, and content delivery"
} else {
    Log-Skip "Tips and suggestions already disabled"
}

SilentInstalledAppsEnabled = 0 is particularly important. Without this, Windows will silently download and install "promoted" apps from the Microsoft Store — including games — on your production VMs. Yes, even on Server editions in some configurations.


8. Section 5: Background Apps {#8-background-apps}

Windows 10/11 and recent Server editions allow Store apps to run in the background, even when the user is not actively using them. This is how Weather, News, and other apps stay "up to date" — by consuming your VM's resources.

Log-Info "=== SECTION 5: Background apps ==="

# Per-user setting
$bgAppsPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\BackgroundAccessApplications"
if (-not (Test-Path $bgAppsPath)) {
    New-Item -Path $bgAppsPath -Force | Out-Null
}
$currentBgApps = (Get-ItemProperty -Path $bgAppsPath -Name "GlobalUserDisabled" `
    -ErrorAction SilentlyContinue).GlobalUserDisabled
if ($currentBgApps -ne 1) {
    Set-ItemProperty -Path $bgAppsPath -Name "GlobalUserDisabled" -Value 1 -Type DWord
    Log-Change "Disabled background apps globally"
} else {
    Log-Skip "Background apps already disabled"
}

# Machine-wide policy (overrides per-user)
$bgPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy"
if (-not (Test-Path $bgPolicyPath)) {
    New-Item -Path $bgPolicyPath -Force | Out-Null
}
$currentBgPolicy = (Get-ItemProperty -Path $bgPolicyPath -Name "LetAppsRunInBackground" `
    -ErrorAction SilentlyContinue).LetAppsRunInBackground
if ($currentBgPolicy -ne 2) {
    Set-ItemProperty -Path $bgPolicyPath -Name "LetAppsRunInBackground" -Value 2 -Type DWord
    Log-Change "Set LetAppsRunInBackground policy to Deny (2)"
} else {
    Log-Skip "Background apps policy already set"
}

We set this at two levels for defense in depth:

The policy level takes precedence and prevents any user from re-enabling background apps.


9. Section 6: Telemetry and Diagnostics {#9-telemetry}

Microsoft collects telemetry data from every Windows installation. On a fleet of VMs, this means hundreds of machines constantly uploading diagnostic data. We block this at multiple levels.

Log-Info "=== SECTION 6: Telemetry and diagnostics ==="

# Set telemetry level to 0 (Security — minimum possible)
$dataCollPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection"
if (-not (Test-Path $dataCollPath)) {
    New-Item -Path $dataCollPath -Force | Out-Null
}
$currentTel = (Get-ItemProperty -Path $dataCollPath -Name "AllowTelemetry" `
    -ErrorAction SilentlyContinue).AllowTelemetry
if ($currentTel -ne 0) {
    Set-ItemProperty -Path $dataCollPath -Name "AllowTelemetry" -Value 0 -Type DWord
    Log-Change "Set telemetry to Security/Off (0) via policy"
} else {
    Log-Skip "Telemetry already set to minimum"
}

# Second registry location (belt and suspenders)
$dataCollPath2 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection"
if (-not (Test-Path $dataCollPath2)) {
    New-Item -Path $dataCollPath2 -Force | Out-Null
}
Set-ItemProperty -Path $dataCollPath2 -Name "AllowTelemetry" -Value 0 -Type DWord
Set-ItemProperty -Path $dataCollPath2 -Name "MaxTelemetryAllowed" -Value 0 -Type DWord

Telemetry Levels

Value Level Description
0 Security Only security-critical data. Enterprise/Education editions only.
1 Basic Minimal device info and quality data.
2 Enhanced Additional usage and performance data.
3 Full All diagnostic data including file contents and typing data.

Note: Level 0 (Security) is only fully honored on Enterprise and Education editions. On Pro editions, it is treated as level 1 (Basic). For maximum effect on Pro, we also disable the DiagTrack service in Section 1.

Disable Feedback and Activity History

# Disable feedback notifications (no "Rate your experience" popups)
$siufPath = "HKCU:\Software\Microsoft\Siuf\Rules"
if (-not (Test-Path $siufPath)) {
    New-Item -Path $siufPath -Force | Out-Null
}
Set-ItemProperty -Path $siufPath -Name "NumberOfSIUFInPeriod" -Value 0 -Type DWord

# Disable Activity History (Timeline feature)
$activityPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\System"
if (-not (Test-Path $activityPath)) {
    New-Item -Path $activityPath -Force | Out-Null
}
Set-ItemProperty -Path $activityPath -Name "EnableActivityFeed" -Value 0 -Type DWord
Set-ItemProperty -Path $activityPath -Name "PublishUserActivities" -Value 0 -Type DWord
Set-ItemProperty -Path $activityPath -Name "UploadUserActivities" -Value 0 -Type DWord
Log-Change "Disabled activity history and feedback"

Activity History tracks every file you open and every app you use, syncing it across devices via your Microsoft account. On a VDI, this is pure overhead.


10. Section 7: Windows Update Control {#10-windows-update}

We do NOT disable Windows Update entirely — that would leave VMs unpatched and vulnerable. Instead, we change the behavior from "download and install automatically, reboot whenever it feels like it" to "notify me, and never reboot while someone is logged in."

Log-Info "=== SECTION 7: Windows Update settings ==="

$wuPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
if (-not (Test-Path $wuPath)) {
    New-Item -Path $wuPath -Force | Out-Null
}

# AUOptions:
#   2 = Notify before download
#   3 = Auto download, notify before install
#   4 = Auto download and schedule install
$currentAU = (Get-ItemProperty -Path $wuPath -Name "AUOptions" `
    -ErrorAction SilentlyContinue).AUOptions
if ($currentAU -ne 2) {
    Set-ItemProperty -Path $wuPath -Name "AUOptions" -Value 2 -Type DWord
    Log-Change "Set Windows Update to 'Notify before download' (2)"
} else {
    Log-Skip "Windows Update already set to notify"
}

# Never auto-restart while a user is logged in
Set-ItemProperty -Path $wuPath -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 -Type DWord
Log-Change "Disabled auto-restart with logged on users"

# Keep automatic update detection enabled (just don't auto-install)
$currentNoAuto = (Get-ItemProperty -Path $wuPath -Name "NoAutoUpdate" `
    -ErrorAction SilentlyContinue).NoAutoUpdate
if ($currentNoAuto -ne 0) {
    Set-ItemProperty -Path $wuPath -Name "NoAutoUpdate" -Value 0 -Type DWord
}

The philosophy: Updates should be installed on YOUR schedule (e.g., maintenance windows), not Microsoft's. AUOptions = 2 means Windows will check for updates and show a notification, but it will not download or install anything without admin action. NoAutoRebootWithLoggedOnUsers = 1 is the safety net — even if an update sneaks through, the system will not reboot while an RDP session is active.


11. Section 8: Temp File and Cache Cleanup {#11-temp-cleanup}

This section provides the most visible disk space savings. On a VM that has been running for weeks, temp files can accumulate into several gigabytes.

Log-Info "=== SECTION 8: Cleaning temp files and caches ==="

$totalCleaned = 0

# Clean user temp folder
$userTemp = $env:TEMP
if (Test-Path $userTemp) {
    $before = (Get-ChildItem -Path $userTemp -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    Get-ChildItem -Path $userTemp -Recurse -Force -ErrorAction SilentlyContinue |
        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $userTemp -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

# Clean Windows temp folder
$winTemp = "C:\Windows\Temp"
if (Test-Path $winTemp) {
    $before = (Get-ChildItem -Path $winTemp -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    Get-ChildItem -Path $winTemp -Recurse -Force -ErrorAction SilentlyContinue |
        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $winTemp -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

# Clean Windows Update download cache
$wuDlPath = "C:\Windows\SoftwareDistribution\Download"
if (Test-Path $wuDlPath) {
    $before = (Get-ChildItem -Path $wuDlPath -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    # Must stop Windows Update service to clear its cache
    Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
    Get-ChildItem -Path $wuDlPath -Recurse -Force -ErrorAction SilentlyContinue |
        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    Start-Service -Name wuauserv -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $wuDlPath -Recurse -Force `
        -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

# Clean prefetch cache
$prefetchPath = "C:\Windows\Prefetch"
if (Test-Path $prefetchPath) {
    Get-ChildItem -Path $prefetchPath -Recurse -Force -ErrorAction SilentlyContinue |
        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}

if ($totalCleaned -gt 0) {
    Log-Change "Cleaned $totalCleaned MB of temp files and caches"
} else {
    Log-Skip "No significant temp files to clean"
}

Why we stop wuauserv before cleaning: The Windows Update service locks files in its download cache. If you try to delete them while the service is running, you get "Access Denied" errors. The pattern is: stop the service, clean the directory, restart the service.

What Gets Cleaned

Location Typical Size Contents
%TEMP% 100 MB - 2 GB Application temp files, installer leftovers
C:\Windows\Temp 50 MB - 1 GB System temp files, Windows Update logs
SoftwareDistribution\Download 500 MB - 4 GB Downloaded Windows Update packages
C:\Windows\Prefetch 50-200 MB Application launch prefetch data

On a freshly patched VM, the Windows Update cache alone can be 2-4 GB.


12. Section 9: Hibernation {#12-hibernation}

Hibernation creates a file called hiberfil.sys at the root of C: that is equal in size to the machine's physical RAM. On an 8 GB VM, that is 8 GB of disk space used by a feature that makes zero sense in a virtualized environment (the hypervisor handles VM state saving).

Log-Info "=== SECTION 9: Hibernation ==="

$hibFile = "C:\hiberfil.sys"
if (Test-Path $hibFile -ErrorAction SilentlyContinue) {
    powercfg /hibernate off
    Log-Change "Disabled hibernation"
} else {
    # Still make sure it's off
    powercfg /hibernate off 2>$null
    Log-Skip "Hibernation already disabled"
}

Disk savings: On VMs with 4-16 GB RAM, this single command frees 4-16 GB. It is often the single biggest space reclamation.


13. Section 10: Windows Defender Tuning {#13-defender}

IMPORTANT: This section disables Windows Defender real-time scanning. Only do this if your VMs are on an isolated network, behind a firewall, and/or running a third-party endpoint protection solution. For VDI workstations that only run a specific business application via RDP, this is usually safe and provides significant performance improvement.

Log-Info "=== SECTION 10: Windows Defender ==="

try {
    $defStatus = Get-MpComputerStatus -ErrorAction Stop
    if ($defStatus.RealTimeProtectionEnabled) {
        # Disable via Group Policy registry (persists across reboots)
        $defPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender"
        if (-not (Test-Path $defPolicyPath)) {
            New-Item -Path $defPolicyPath -Force | Out-Null
        }
        Set-ItemProperty -Path $defPolicyPath -Name "DisableAntiSpyware" -Value 1 -Type DWord

        $rtpPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection"
        if (-not (Test-Path $rtpPath)) {
            New-Item -Path $rtpPath -Force | Out-Null
        }
        Set-ItemProperty -Path $rtpPath -Name "DisableRealtimeMonitoring" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableBehaviorMonitoring" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableOnAccessProtection" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableScanOnRealtimeEnable" -Value 1 -Type DWord

        # Try to disable immediately (may fail on newer builds with tamper protection)
        try {
            Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction Stop
            Log-Change "Disabled Windows Defender real-time protection (immediate + policy)"
        } catch {
            Log-Change "Set Defender disable policy (will take effect after reboot/GP refresh)"
        }
    } else {
        Log-Skip "Windows Defender real-time protection already disabled"
    }
} catch {
    Log-Skip "Cannot query Defender status: $_"
}

Why two methods?

Performance impact of Defender real-time scanning:


14. Section 11: Scheduled Task Cleanup {#14-scheduled-tasks}

Windows ships with dozens of scheduled tasks that run telemetry collectors, disk diagnostics, and app compatibility checks on a schedule. These cause random CPU spikes that are impossible to diagnose if you do not know they exist.

Log-Info "=== SECTION 11: Scheduled tasks ==="

$tasksToDisable = @(
    "\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser",
    "\Microsoft\Windows\Application Experience\ProgramDataUpdater",
    "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator",
    "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip",
    "\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector",
    "\Microsoft\Windows\Feedback\Siuf\DmClient",
    "\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload",
    "\Microsoft\Windows\Maps\MapsToastTask",
    "\Microsoft\Windows\Maps\MapsUpdateTask",
    "\Microsoft\Windows\Windows Error Reporting\QueueReporting"
)

foreach ($task in $tasksToDisable) {
    try {
        $t = Get-ScheduledTask `
            -TaskPath ($task.Substring(0, $task.LastIndexOf('\') + 1)) `
            -TaskName ($task.Substring($task.LastIndexOf('\') + 1)) `
            -ErrorAction Stop
        if ($t.State -ne 'Disabled') {
            Disable-ScheduledTask -TaskPath $t.TaskPath -TaskName $t.TaskName `
                -ErrorAction Stop | Out-Null
            Log-Change "Disabled task: $task"
        } else {
            Log-Skip "Task already disabled: $task"
        }
    } catch {
        Log-Skip "Task not found or cannot disable: $($task.Split('\')[-1])"
    }
}

Notable tasks and why they are disabled:

Task Impact
Microsoft Compatibility Appraiser Scans all installed software for upgrade compatibility. Runs for 10-30 minutes, uses significant CPU and disk.
Consolidator (CEIP) Collects and uploads Customer Experience Improvement Program data.
UsbCeip Monitors USB device usage for telemetry.
DiskDiagnosticDataCollector Runs disk diagnostics and uploads results.
DmClient Feedback Hub data collection.
MapsUpdateTask Downloads map updates.
QueueReporting Queues Windows Error Reports for upload.

15. Section 12: Chrome Optimization {#15-chrome}

If Google Chrome is installed on the VDI (common for browser-based business apps), we apply policies to reduce its resource footprint:

Log-Info "=== SECTION 12: Chrome optimization ==="

$chromePaths = @(
    "C:\Program Files\Google\Chrome\Application\chrome.exe",
    "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
)
$chromeInstalled = $false
foreach ($cp in $chromePaths) {
    if (Test-Path $cp) { $chromeInstalled = $true; break }
}

if ($chromeInstalled) {
    $chromePolicyPath = "HKLM:\SOFTWARE\Policies\Google\Chrome"
    if (-not (Test-Path $chromePolicyPath)) {
        New-Item -Path $chromePolicyPath -Force | Out-Null
    }

    # Disable background mode (Chrome stays running after closing window)
    Set-ItemProperty -Path $chromePolicyPath -Name "BackgroundModeEnabled" `
        -Value 0 -Type DWord
    # Keep hardware acceleration enabled (it helps on VMs with GPU passthrough)
    Set-ItemProperty -Path $chromePolicyPath -Name "HardwareAccelerationModeEnabled" `
        -Value 1 -Type DWord
    # Disable Chrome Software Reporter (scans all files for "unwanted software")
    Set-ItemProperty -Path $chromePolicyPath -Name "ChromeCleanupEnabled" `
        -Value 0 -Type DWord
    # Enable tab discarding (frees memory from inactive tabs)
    Set-ItemProperty -Path $chromePolicyPath -Name "TabDiscardingEnabled" `
        -Value 1 -Type DWord

    Log-Change "Applied Chrome performance policies"
} else {
    Log-Skip "Chrome not installed - skipping Chrome optimization"
}

Why disable BackgroundModeEnabled? By default, when you close Chrome, it keeps running in the background (look for the icon in the system tray). On a VDI, this means Chrome's 200-500 MB memory footprint persists even when the user is not browsing. Setting this to 0 forces Chrome to fully exit when all windows are closed.

Why disable ChromeCleanupEnabled? Chrome's "Software Reporter Tool" (software_reporter_tool.exe) periodically scans the entire disk looking for software that might interfere with Chrome. This scan can run for 20+ minutes and use significant CPU. On a managed VDI, you control what software is installed — Chrome's scan is unnecessary.


16. Section 13: Network Optimization {#16-network}

Two network optimizations that improve responsiveness for interactive sessions:

Log-Info "=== SECTION 13: Network optimization ==="

# Disable Nagle's algorithm for lower latency
$tcpPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"
$currentNagle = (Get-ItemProperty -Path $tcpPath -Name "TcpNoDelay" `
    -ErrorAction SilentlyContinue).TcpNoDelay
if ($currentNagle -ne 1) {
    Set-ItemProperty -Path $tcpPath -Name "TcpNoDelay" -Value 1 -Type DWord
    Log-Change "Disabled Nagle's algorithm (TcpNoDelay=1)"
} else {
    Log-Skip "Nagle's algorithm already disabled"
}

# Disable Delivery Optimization (P2P update sharing)
$doPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization"
if (-not (Test-Path $doPath)) {
    New-Item -Path $doPath -Force | Out-Null
}
$currentDO = (Get-ItemProperty -Path $doPath -Name "DODownloadMode" `
    -ErrorAction SilentlyContinue).DODownloadMode
if ($currentDO -ne 0) {
    Set-ItemProperty -Path $doPath -Name "DODownloadMode" -Value 0 -Type DWord
    Log-Change "Disabled Delivery Optimization P2P"
} else {
    Log-Skip "Delivery Optimization already disabled"
}

Nagle's Algorithm (TcpNoDelay): Nagle's algorithm batches small TCP packets together to reduce overhead. This is great for bulk data transfers but adds latency to interactive applications (RDP, SSH, web apps). Disabling it (TcpNoDelay = 1) sends packets immediately, making RDP sessions feel more responsive.

Delivery Optimization (DODownloadMode): This feature shares Windows Update downloads peer-to-peer between machines on the same network (or even across the internet). On a VDI fleet, this means your VMs are uploading update data to each other, consuming bandwidth. Setting DODownloadMode = 0 means "HTTP only, no P2P."

DODownloadMode Value Behavior
0 HTTP only (no P2P)
1 HTTP + LAN peers
2 HTTP + LAN + Group peers
3 HTTP + LAN + Internet peers

17. Section 14: Memory Optimization {#17-memory}

Two kernel-level memory management tweaks that improve performance on VMs with adequate RAM:

Log-Info "=== SECTION 14: Memory optimization ==="

$mmPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"

# Disable paging of kernel/driver code to disk
$currentPE = (Get-ItemProperty -Path $mmPath -Name "DisablePagingExecutive" `
    -ErrorAction SilentlyContinue).DisablePagingExecutive
if ($currentPE -ne 1) {
    Set-ItemProperty -Path $mmPath -Name "DisablePagingExecutive" -Value 1 -Type DWord
    Log-Change "Disabled paging of kernel/drivers to disk"
} else {
    Log-Skip "Paging executive already disabled"
}

# Enable large system cache
$currentLSC = (Get-ItemProperty -Path $mmPath -Name "LargeSystemCache" `
    -ErrorAction SilentlyContinue).LargeSystemCache
if ($currentLSC -ne 1) {
    Set-ItemProperty -Path $mmPath -Name "LargeSystemCache" -Value 1 -Type DWord
    Log-Change "Enabled large system cache"
} else {
    Log-Skip "Large system cache already enabled"
}

DisablePagingExecutive = 1: Tells Windows to keep kernel-mode drivers and system code in physical RAM instead of allowing them to be paged to disk. This eliminates random latency spikes caused by the kernel needing to read its own code back from the page file. On a VM with 4+ GB RAM, there is always enough memory for this.

LargeSystemCache = 1: Tells the memory manager to favor the system cache (file cache) over application working sets. On a server/VDI where the workload is file I/O heavy (opening documents, loading web pages), this improves performance by keeping frequently accessed files in RAM.

Note: Only enable LargeSystemCache on machines with 4 GB+ RAM. On low-memory machines, it can cause excessive paging of application memory.


18. The Summary Report {#18-summary}

After all 14 sections complete, the script prints a structured summary:

Write-Host ""
Write-Host "============================================" -ForegroundColor White
Write-Host " OPTIMIZATION COMPLETE" -ForegroundColor White
Write-Host "============================================" -ForegroundColor White
Write-Host ""

if ($changes.Count -gt 0) {
    Write-Host "Changes made ($($changes.Count)):" -ForegroundColor Green
    foreach ($c in $changes) {
        Write-Host "  + $c" -ForegroundColor Green
    }
} else {
    Write-Host "No changes needed - system already optimized." -ForegroundColor Yellow
}

if ($errors.Count -gt 0) {
    Write-Host ""
    Write-Host "Errors ($($errors.Count)):" -ForegroundColor Red
    foreach ($e in $errors) {
        Write-Host "  ! $e" -ForegroundColor Red
    }
}

Write-Host ""
Write-Host "IMPORTANT: Some changes require a reboot to take full effect." -ForegroundColor Cyan
Write-Host "  - Defender policy changes" -ForegroundColor Cyan
Write-Host "  - Visual effects changes" -ForegroundColor Cyan
Write-Host "  - Memory management changes" -ForegroundColor Cyan
Write-Host ""

# Show final disk free space
$disk = Get-PSDrive C
Write-Host "Disk C: Free space: $([math]::Round($disk.Free/1GB,2)) GB" -ForegroundColor White

Example output from first run on a fresh VM:

============================================
 Windows Server 2025 Optimization Script
 2026-03-01 14:23:45
============================================

[INFO] === SECTION 1: Disabling unnecessary services ===
[CHANGED] Disabled SysMain (Superfetch) - prefetching, wastes RAM on servers
[CHANGED] Disabled Windows Search Indexer - CPU/disk intensive
[CHANGED] Disabled Print Spooler - not needed on VDI/server
[CHANGED] Disabled Connected User Experiences and Telemetry
[SKIP] WAP Push Message Routing - telemetry helper - service not found
[SKIP] Downloaded Maps Manager - service not found
...
[INFO] === SECTION 8: Cleaning temp files and caches ===
[CHANGED] Cleaned 3847.52 MB of temp files and caches
[INFO] === SECTION 9: Hibernation ===
[CHANGED] Disabled hibernation
...

============================================
 OPTIMIZATION COMPLETE
============================================

Changes made (37):
  + Disabled SysMain (Superfetch) - prefetching, wastes RAM on servers
  + Disabled Windows Search Indexer - CPU/disk intensive
  + Disabled Print Spooler - not needed on VDI/server
  ...
  + Cleaned 3847.52 MB of temp files and caches
  + Disabled hibernation
  + Disabled Nagle's algorithm (TcpNoDelay=1)
  + Enabled large system cache

IMPORTANT: Some changes require a reboot to take full effect.
  - Defender policy changes
  - Visual effects changes
  - Memory management changes

Disk C: Free space: 42.87 GB

Example output from second run (idempotent):

[SKIP] SysMain (Superfetch) - already disabled/stopped
[SKIP] Windows Search Indexer - already disabled/stopped
[SKIP] Print Spooler - already disabled/stopped
...
No changes needed - system already optimized.

19. Complete Script {#19-complete-script}

Here is the complete, production-ready script. Save it as optimize-windows.ps1 and run it from an elevated PowerShell prompt.

#Requires -RunAsAdministrator
<#
.SYNOPSIS
    Windows Server Performance Optimization Script
.DESCRIPTION
    Disables unnecessary services, visual effects, telemetry, and background tasks
    for optimal performance on VDI/server workloads. Idempotent - safe to run multiple times.
.NOTES
    Target: Windows Server 2019/2022/2025 or Windows 10/11 Pro
#>

$ErrorActionPreference = "Continue"
$changes = @()
$errors = @()

function Log-Change {
    param([string]$Message)
    $script:changes += $Message
    Write-Host "[CHANGED] $Message" -ForegroundColor Green
}

function Log-Skip {
    param([string]$Message)
    Write-Host "[SKIP] $Message" -ForegroundColor Yellow
}

function Log-Error {
    param([string]$Message)
    $script:errors += $Message
    Write-Host "[ERROR] $Message" -ForegroundColor Red
}

function Log-Info {
    param([string]$Message)
    Write-Host "[INFO] $Message" -ForegroundColor Cyan
}

Write-Host "============================================" -ForegroundColor White
Write-Host " Windows Server Optimization Script" -ForegroundColor White
Write-Host " $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor White
Write-Host "============================================" -ForegroundColor White
Write-Host ""

# --------------------------------------------------
# 1. DISABLE UNNECESSARY SERVICES
# --------------------------------------------------
Log-Info "=== SECTION 1: Disabling unnecessary services ==="

$servicesToDisable = @(
    @{ Name = "SysMain";            Desc = "SysMain (Superfetch) - prefetching, wastes RAM on servers" },
    @{ Name = "WSearch";            Desc = "Windows Search Indexer - CPU/disk intensive" },
    @{ Name = "Spooler";            Desc = "Print Spooler - not needed on VDI/server" },
    @{ Name = "DiagTrack";          Desc = "Connected User Experiences and Telemetry" },
    @{ Name = "dmwappushservice";   Desc = "WAP Push Message Routing - telemetry helper" },
    @{ Name = "MapsBroker";         Desc = "Downloaded Maps Manager" },
    @{ Name = "lfsvc";              Desc = "Geolocation Service" },
    @{ Name = "WMPNetworkSvc";      Desc = "Windows Media Player Network Sharing" },
    @{ Name = "XblAuthManager";     Desc = "Xbox Live Auth Manager" },
    @{ Name = "XblGameSave";        Desc = "Xbox Live Game Save" },
    @{ Name = "XboxGipSvc";         Desc = "Xbox Accessory Management" },
    @{ Name = "XboxNetApiSvc";      Desc = "Xbox Live Networking" },
    @{ Name = "Fax";                Desc = "Fax Service" },
    @{ Name = "RetailDemo";         Desc = "Retail Demo Service" },
    @{ Name = "wisvc";              Desc = "Windows Insider Service" },
    @{ Name = "WerSvc";             Desc = "Windows Error Reporting" },
    @{ Name = "TabletInputService"; Desc = "Touch Keyboard and Handwriting Panel" },
    @{ Name = "PhoneSvc";           Desc = "Phone Service" },
    @{ Name = "icssvc";             Desc = "Windows Mobile Hotspot Service" },
    @{ Name = "WbioSrvc";           Desc = "Windows Biometric Service" }
)

foreach ($svc in $servicesToDisable) {
    $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
    if (-not $service) {
        Log-Skip "$($svc.Desc) - service not found"
        continue
    }

    $changed = $false

    if ($service.Status -eq 'Running') {
        try {
            Stop-Service -Name $svc.Name -Force -ErrorAction Stop
            $changed = $true
        } catch {
            Log-Error "Failed to stop $($svc.Name): $_"
        }
    }

    if ($service.StartType -ne 'Disabled') {
        try {
            Set-Service -Name $svc.Name -StartupType Disabled -ErrorAction Stop
            $changed = $true
        } catch {
            Log-Error "Failed to disable $($svc.Name): $_"
        }
    }

    if ($changed) {
        Log-Change "Disabled $($svc.Desc)"
    } else {
        Log-Skip "$($svc.Desc) - already disabled/stopped"
    }
}

Write-Host ""

# --------------------------------------------------
# 2. SET HIGH PERFORMANCE POWER PLAN
# --------------------------------------------------
Log-Info "=== SECTION 2: Power plan ==="

$currentPlan = powercfg /getactivescheme 2>&1
if ($currentPlan -match "8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c") {
    Log-Skip "Power plan already set to High Performance"
} else {
    try {
        powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
        Log-Change "Set power plan to High Performance"
    } catch {
        Log-Error "Failed to set power plan: $_"
    }
}

powercfg /change monitor-timeout-ac 0
powercfg /change standby-timeout-ac 0
powercfg /change hibernate-timeout-ac 0

Write-Host ""

# --------------------------------------------------
# 3. DISABLE VISUAL EFFECTS
# --------------------------------------------------
Log-Info "=== SECTION 3: Visual effects ==="

$vfxPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects"
if (-not (Test-Path $vfxPath)) {
    New-Item -Path $vfxPath -Force | Out-Null
}
$currentVFX = (Get-ItemProperty -Path $vfxPath -Name "VisualFXSetting" -ErrorAction SilentlyContinue).VisualFXSetting
if ($currentVFX -ne 2) {
    Set-ItemProperty -Path $vfxPath -Name "VisualFXSetting" -Value 2 -Type DWord
    Log-Change "Set VisualFXSetting to 'Best Performance' (2)"
} else {
    Log-Skip "VisualFXSetting already set to Best Performance"
}

$advPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
$dwmPath = "HKCU:\Software\Microsoft\Windows\DWM"

$animSettings = @{
    "TaskbarAnimations"   = 0
    "ListviewAlphaSelect" = 0
    "ListviewShadow"      = 0
    "IconsOnly"           = 1
}
foreach ($key in $animSettings.Keys) {
    $current = (Get-ItemProperty -Path $advPath -Name $key -ErrorAction SilentlyContinue).$key
    if ($current -ne $animSettings[$key]) {
        Set-ItemProperty -Path $advPath -Name $key -Value $animSettings[$key] -Type DWord
        Log-Change "Set $key = $($animSettings[$key])"
    }
}

if (Test-Path $dwmPath) {
    $currentTransparency = (Get-ItemProperty -Path $dwmPath -Name "EnableTransparency" -ErrorAction SilentlyContinue).EnableTransparency
    if ($currentTransparency -ne 0) {
        Set-ItemProperty -Path $dwmPath -Name "EnableTransparency" -Value 0 -Type DWord
        Log-Change "Disabled transparency effects"
    }
}

$perfPath = "HKCU:\Control Panel\Desktop"
$currentDragFull = (Get-ItemProperty -Path $perfPath -Name "DragFullWindows" -ErrorAction SilentlyContinue).DragFullWindows
if ($currentDragFull -ne "0") {
    Set-ItemProperty -Path $perfPath -Name "DragFullWindows" -Value "0"
    Log-Change "Disabled drag full windows"
}

Set-ItemProperty -Path $perfPath -Name "SmoothScroll" -Value 0 -Type DWord -ErrorAction SilentlyContinue

$currentMenuAnim = (Get-ItemProperty -Path $perfPath -Name "MenuShowDelay" -ErrorAction SilentlyContinue).MenuShowDelay
if ($currentMenuAnim -ne "0") {
    Set-ItemProperty -Path $perfPath -Name "MenuShowDelay" -Value "0"
    Log-Change "Set menu show delay to 0"
}

Set-ItemProperty -Path $perfPath -Name "MinAnimate" -Value "0" -ErrorAction SilentlyContinue
Set-ItemProperty -Path $perfPath -Name "UserPreferencesMask" -Value ([byte[]](0x90,0x12,0x03,0x80,0x10,0x00,0x00,0x00)) -Type Binary -ErrorAction SilentlyContinue
Log-Change "Set UserPreferencesMask for best performance"

Write-Host ""

# --------------------------------------------------
# 4. DISABLE CORTANA, TIPS, SUGGESTIONS
# --------------------------------------------------
Log-Info "=== SECTION 4: Cortana, Tips, Suggestions ==="

$cortanaPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search"
if (-not (Test-Path $cortanaPath)) {
    New-Item -Path $cortanaPath -Force | Out-Null
}
$currentCortana = (Get-ItemProperty -Path $cortanaPath -Name "AllowCortana" -ErrorAction SilentlyContinue).AllowCortana
if ($currentCortana -ne 0) {
    Set-ItemProperty -Path $cortanaPath -Name "AllowCortana" -Value 0 -Type DWord
    Log-Change "Disabled Cortana"
} else {
    Log-Skip "Cortana already disabled"
}

$contentDeliveryPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager"
if (-not (Test-Path $contentDeliveryPath)) {
    New-Item -Path $contentDeliveryPath -Force | Out-Null
}

$tipSettings = @{
    "SystemPaneSuggestionsEnabled"       = 0
    "SoftLandingEnabled"                 = 0
    "SubscribedContent-338389Enabled"    = 0
    "SubscribedContent-310093Enabled"    = 0
    "SubscribedContent-338388Enabled"    = 0
    "SubscribedContent-338393Enabled"    = 0
    "SubscribedContent-353694Enabled"    = 0
    "SubscribedContent-353696Enabled"    = 0
    "SilentInstalledAppsEnabled"         = 0
    "OemPreInstalledAppsEnabled"         = 0
    "PreInstalledAppsEnabled"            = 0
    "FeatureManagementEnabled"           = 0
}

$tipsChanged = $false
foreach ($key in $tipSettings.Keys) {
    $current = (Get-ItemProperty -Path $contentDeliveryPath -Name $key -ErrorAction SilentlyContinue).$key
    if ($current -ne $tipSettings[$key]) {
        Set-ItemProperty -Path $contentDeliveryPath -Name $key -Value $tipSettings[$key] -Type DWord
        $tipsChanged = $true
    }
}
if ($tipsChanged) {
    Log-Change "Disabled tips, suggestions, and content delivery"
} else {
    Log-Skip "Tips and suggestions already disabled"
}

Write-Host ""

# --------------------------------------------------
# 5. DISABLE BACKGROUND APPS
# --------------------------------------------------
Log-Info "=== SECTION 5: Background apps ==="

$bgAppsPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\BackgroundAccessApplications"
if (-not (Test-Path $bgAppsPath)) {
    New-Item -Path $bgAppsPath -Force | Out-Null
}
$currentBgApps = (Get-ItemProperty -Path $bgAppsPath -Name "GlobalUserDisabled" -ErrorAction SilentlyContinue).GlobalUserDisabled
if ($currentBgApps -ne 1) {
    Set-ItemProperty -Path $bgAppsPath -Name "GlobalUserDisabled" -Value 1 -Type DWord
    Log-Change "Disabled background apps globally"
} else {
    Log-Skip "Background apps already disabled"
}

$bgPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\AppPrivacy"
if (-not (Test-Path $bgPolicyPath)) {
    New-Item -Path $bgPolicyPath -Force | Out-Null
}
$currentBgPolicy = (Get-ItemProperty -Path $bgPolicyPath -Name "LetAppsRunInBackground" -ErrorAction SilentlyContinue).LetAppsRunInBackground
if ($currentBgPolicy -ne 2) {
    Set-ItemProperty -Path $bgPolicyPath -Name "LetAppsRunInBackground" -Value 2 -Type DWord
    Log-Change "Set LetAppsRunInBackground policy to Deny (2)"
} else {
    Log-Skip "Background apps policy already set"
}

Write-Host ""

# --------------------------------------------------
# 6. DISABLE TELEMETRY / DIAGNOSTICS
# --------------------------------------------------
Log-Info "=== SECTION 6: Telemetry and diagnostics ==="

$dataCollPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection"
if (-not (Test-Path $dataCollPath)) {
    New-Item -Path $dataCollPath -Force | Out-Null
}
$currentTel = (Get-ItemProperty -Path $dataCollPath -Name "AllowTelemetry" -ErrorAction SilentlyContinue).AllowTelemetry
if ($currentTel -ne 0) {
    Set-ItemProperty -Path $dataCollPath -Name "AllowTelemetry" -Value 0 -Type DWord
    Log-Change "Set telemetry to Security/Off (0) via policy"
} else {
    Log-Skip "Telemetry already set to minimum"
}

$dataCollPath2 = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection"
if (-not (Test-Path $dataCollPath2)) {
    New-Item -Path $dataCollPath2 -Force | Out-Null
}
Set-ItemProperty -Path $dataCollPath2 -Name "AllowTelemetry" -Value 0 -Type DWord -ErrorAction SilentlyContinue
Set-ItemProperty -Path $dataCollPath2 -Name "MaxTelemetryAllowed" -Value 0 -Type DWord -ErrorAction SilentlyContinue

$siufPath = "HKCU:\Software\Microsoft\Siuf\Rules"
if (-not (Test-Path $siufPath)) {
    New-Item -Path $siufPath -Force | Out-Null
}
Set-ItemProperty -Path $siufPath -Name "NumberOfSIUFInPeriod" -Value 0 -Type DWord

$activityPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\System"
if (-not (Test-Path $activityPath)) {
    New-Item -Path $activityPath -Force | Out-Null
}
Set-ItemProperty -Path $activityPath -Name "EnableActivityFeed" -Value 0 -Type DWord
Set-ItemProperty -Path $activityPath -Name "PublishUserActivities" -Value 0 -Type DWord
Set-ItemProperty -Path $activityPath -Name "UploadUserActivities" -Value 0 -Type DWord
Log-Change "Disabled activity history and feedback"

Write-Host ""

# --------------------------------------------------
# 7. WINDOWS UPDATE - PREVENT AUTO RESTART
# --------------------------------------------------
Log-Info "=== SECTION 7: Windows Update settings ==="

$wuPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU"
if (-not (Test-Path $wuPath)) {
    New-Item -Path $wuPath -Force | Out-Null
}

$currentAU = (Get-ItemProperty -Path $wuPath -Name "AUOptions" -ErrorAction SilentlyContinue).AUOptions
if ($currentAU -ne 2) {
    Set-ItemProperty -Path $wuPath -Name "AUOptions" -Value 2 -Type DWord
    Log-Change "Set Windows Update to 'Notify before download' (2)"
} else {
    Log-Skip "Windows Update already set to notify"
}

Set-ItemProperty -Path $wuPath -Name "NoAutoRebootWithLoggedOnUsers" -Value 1 -Type DWord
Log-Change "Disabled auto-restart with logged on users"

$currentNoAuto = (Get-ItemProperty -Path $wuPath -Name "NoAutoUpdate" -ErrorAction SilentlyContinue).NoAutoUpdate
if ($currentNoAuto -ne 0) {
    Set-ItemProperty -Path $wuPath -Name "NoAutoUpdate" -Value 0 -Type DWord
}

Write-Host ""

# --------------------------------------------------
# 8. CLEAN TEMP FILES AND CACHES
# --------------------------------------------------
Log-Info "=== SECTION 8: Cleaning temp files and caches ==="

$totalCleaned = 0

$userTemp = $env:TEMP
if (Test-Path $userTemp) {
    $before = (Get-ChildItem -Path $userTemp -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    Get-ChildItem -Path $userTemp -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $userTemp -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

$winTemp = "C:\Windows\Temp"
if (Test-Path $winTemp) {
    $before = (Get-ChildItem -Path $winTemp -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    Get-ChildItem -Path $winTemp -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $winTemp -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

$wuDlPath = "C:\Windows\SoftwareDistribution\Download"
if (Test-Path $wuDlPath) {
    $before = (Get-ChildItem -Path $wuDlPath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
    Get-ChildItem -Path $wuDlPath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
    Start-Service -Name wuauserv -ErrorAction SilentlyContinue
    $after = (Get-ChildItem -Path $wuDlPath -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
    $cleaned = [math]::Round(($before - $after) / 1MB, 2)
    if ($cleaned -gt 0) { $totalCleaned += $cleaned }
}

$prefetchPath = "C:\Windows\Prefetch"
if (Test-Path $prefetchPath) {
    Get-ChildItem -Path $prefetchPath -Recurse -Force -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
}

if ($totalCleaned -gt 0) {
    Log-Change "Cleaned $totalCleaned MB of temp files and caches"
} else {
    Log-Skip "No significant temp files to clean"
}

Write-Host ""

# --------------------------------------------------
# 9. DISABLE HIBERNATION
# --------------------------------------------------
Log-Info "=== SECTION 9: Hibernation ==="

$hibFile = "C:\hiberfil.sys"
if (Test-Path $hibFile -ErrorAction SilentlyContinue) {
    powercfg /hibernate off
    Log-Change "Disabled hibernation"
} else {
    powercfg /hibernate off 2>$null
    Log-Skip "Hibernation already disabled"
}

Write-Host ""

# --------------------------------------------------
# 10. DISABLE WINDOWS DEFENDER REAL-TIME SCANNING
# --------------------------------------------------
Log-Info "=== SECTION 10: Windows Defender ==="

try {
    $defStatus = Get-MpComputerStatus -ErrorAction Stop
    if ($defStatus.RealTimeProtectionEnabled) {
        $defPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender"
        if (-not (Test-Path $defPolicyPath)) {
            New-Item -Path $defPolicyPath -Force | Out-Null
        }
        Set-ItemProperty -Path $defPolicyPath -Name "DisableAntiSpyware" -Value 1 -Type DWord

        $rtpPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection"
        if (-not (Test-Path $rtpPath)) {
            New-Item -Path $rtpPath -Force | Out-Null
        }
        Set-ItemProperty -Path $rtpPath -Name "DisableRealtimeMonitoring" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableBehaviorMonitoring" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableOnAccessProtection" -Value 1 -Type DWord
        Set-ItemProperty -Path $rtpPath -Name "DisableScanOnRealtimeEnable" -Value 1 -Type DWord

        try {
            Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction Stop
            Log-Change "Disabled Windows Defender real-time protection (immediate + policy)"
        } catch {
            Log-Change "Set Defender disable policy (will take effect after reboot/GP refresh)"
        }
    } else {
        Log-Skip "Windows Defender real-time protection already disabled"
    }
} catch {
    Log-Skip "Cannot query Defender status: $_"
}

Write-Host ""

# --------------------------------------------------
# 11. DISABLE SCHEDULED TASKS (telemetry/maintenance)
# --------------------------------------------------
Log-Info "=== SECTION 11: Scheduled tasks ==="

$tasksToDisable = @(
    "\Microsoft\Windows\Application Experience\Microsoft Compatibility Appraiser",
    "\Microsoft\Windows\Application Experience\ProgramDataUpdater",
    "\Microsoft\Windows\Customer Experience Improvement Program\Consolidator",
    "\Microsoft\Windows\Customer Experience Improvement Program\UsbCeip",
    "\Microsoft\Windows\DiskDiagnostic\Microsoft-Windows-DiskDiagnosticDataCollector",
    "\Microsoft\Windows\Feedback\Siuf\DmClient",
    "\Microsoft\Windows\Feedback\Siuf\DmClientOnScenarioDownload",
    "\Microsoft\Windows\Maps\MapsToastTask",
    "\Microsoft\Windows\Maps\MapsUpdateTask",
    "\Microsoft\Windows\Windows Error Reporting\QueueReporting"
)

foreach ($task in $tasksToDisable) {
    try {
        $t = Get-ScheduledTask -TaskPath ($task.Substring(0, $task.LastIndexOf('\') + 1)) -TaskName ($task.Substring($task.LastIndexOf('\') + 1)) -ErrorAction Stop
        if ($t.State -ne 'Disabled') {
            Disable-ScheduledTask -TaskPath $t.TaskPath -TaskName $t.TaskName -ErrorAction Stop | Out-Null
            Log-Change "Disabled task: $task"
        } else {
            Log-Skip "Task already disabled: $task"
        }
    } catch {
        Log-Skip "Task not found or cannot disable: $($task.Split('\')[-1])"
    }
}

Write-Host ""

# --------------------------------------------------
# 12. OPTIMIZE CHROME (if installed)
# --------------------------------------------------
Log-Info "=== SECTION 12: Chrome optimization ==="

$chromePaths = @(
    "C:\Program Files\Google\Chrome\Application\chrome.exe",
    "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
)
$chromeInstalled = $false
foreach ($cp in $chromePaths) {
    if (Test-Path $cp) { $chromeInstalled = $true; break }
}

if ($chromeInstalled) {
    $chromePolicyPath = "HKLM:\SOFTWARE\Policies\Google\Chrome"
    if (-not (Test-Path $chromePolicyPath)) {
        New-Item -Path $chromePolicyPath -Force | Out-Null
    }

    Set-ItemProperty -Path $chromePolicyPath -Name "BackgroundModeEnabled" -Value 0 -Type DWord
    Set-ItemProperty -Path $chromePolicyPath -Name "HardwareAccelerationModeEnabled" -Value 1 -Type DWord
    Set-ItemProperty -Path $chromePolicyPath -Name "ChromeCleanupEnabled" -Value 0 -Type DWord
    Set-ItemProperty -Path $chromePolicyPath -Name "TabDiscardingEnabled" -Value 1 -Type DWord

    Log-Change "Applied Chrome performance policies"
} else {
    Log-Skip "Chrome not installed - skipping Chrome optimization"
}

Write-Host ""

# --------------------------------------------------
# 13. NETWORK OPTIMIZATION
# --------------------------------------------------
Log-Info "=== SECTION 13: Network optimization ==="

$tcpPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"
$currentNagle = (Get-ItemProperty -Path $tcpPath -Name "TcpNoDelay" -ErrorAction SilentlyContinue).TcpNoDelay
if ($currentNagle -ne 1) {
    Set-ItemProperty -Path $tcpPath -Name "TcpNoDelay" -Value 1 -Type DWord
    Log-Change "Disabled Nagle's algorithm (TcpNoDelay=1)"
} else {
    Log-Skip "Nagle's algorithm already disabled"
}

$doPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DeliveryOptimization"
if (-not (Test-Path $doPath)) {
    New-Item -Path $doPath -Force | Out-Null
}
$currentDO = (Get-ItemProperty -Path $doPath -Name "DODownloadMode" -ErrorAction SilentlyContinue).DODownloadMode
if ($currentDO -ne 0) {
    Set-ItemProperty -Path $doPath -Name "DODownloadMode" -Value 0 -Type DWord
    Log-Change "Disabled Delivery Optimization P2P"
} else {
    Log-Skip "Delivery Optimization already disabled"
}

Write-Host ""

# --------------------------------------------------
# 14. MEMORY OPTIMIZATION
# --------------------------------------------------
Log-Info "=== SECTION 14: Memory optimization ==="

$mmPath = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management"

$currentPE = (Get-ItemProperty -Path $mmPath -Name "DisablePagingExecutive" -ErrorAction SilentlyContinue).DisablePagingExecutive
if ($currentPE -ne 1) {
    Set-ItemProperty -Path $mmPath -Name "DisablePagingExecutive" -Value 1 -Type DWord
    Log-Change "Disabled paging of kernel/drivers to disk"
} else {
    Log-Skip "Paging executive already disabled"
}

$currentLSC = (Get-ItemProperty -Path $mmPath -Name "LargeSystemCache" -ErrorAction SilentlyContinue).LargeSystemCache
if ($currentLSC -ne 1) {
    Set-ItemProperty -Path $mmPath -Name "LargeSystemCache" -Value 1 -Type DWord
    Log-Change "Enabled large system cache"
} else {
    Log-Skip "Large system cache already enabled"
}

Write-Host ""

# --------------------------------------------------
# SUMMARY
# --------------------------------------------------
Write-Host ""
Write-Host "============================================" -ForegroundColor White
Write-Host " OPTIMIZATION COMPLETE" -ForegroundColor White
Write-Host "============================================" -ForegroundColor White
Write-Host ""

if ($changes.Count -gt 0) {
    Write-Host "Changes made ($($changes.Count)):" -ForegroundColor Green
    foreach ($c in $changes) {
        Write-Host "  + $c" -ForegroundColor Green
    }
} else {
    Write-Host "No changes needed - system already optimized." -ForegroundColor Yellow
}

if ($errors.Count -gt 0) {
    Write-Host ""
    Write-Host "Errors ($($errors.Count)):" -ForegroundColor Red
    foreach ($e in $errors) {
        Write-Host "  ! $e" -ForegroundColor Red
    }
}

Write-Host ""
Write-Host "IMPORTANT: Some changes require a reboot to take full effect." -ForegroundColor Cyan
Write-Host "  - Defender policy changes" -ForegroundColor Cyan
Write-Host "  - Visual effects changes" -ForegroundColor Cyan
Write-Host "  - Memory management changes" -ForegroundColor Cyan
Write-Host ""

$disk = Get-PSDrive C
Write-Host "Disk C: Free space: $([math]::Round($disk.Free/1GB,2)) GB" -ForegroundColor White

20. Deploying at Scale {#20-deploying-at-scale}

Running the script on one VM is straightforward. Running it on 100+ VMs requires automation.

Method 1: PowerShell Remoting (WinRM)

If WinRM is enabled on your VMs (it is by default on Windows Server), you can run the script remotely:

# Single VM
$cred = Get-Credential
Invoke-Command -ComputerName YOUR_SERVER_IP -FilePath .\optimize-windows.ps1 -Credential $cred

# Multiple VMs from a list
$vms = Get-Content .\vm-list.txt  # One IP per line
$cred = Get-Credential
$results = Invoke-Command -ComputerName $vms -FilePath .\optimize-windows.ps1 -Credential $cred -ThrottleLimit 10

The -ThrottleLimit 10 parameter runs 10 VMs in parallel. Adjust based on your management machine's resources.

Method 2: Group Policy Startup Script

For domain-joined VMs:

  1. Copy optimize-windows.ps1 to \\YOUR_DC\NETLOGON\Scripts\
  2. Open Group Policy Management Console
  3. Create or edit a GPO linked to the VDI OU
  4. Navigate to: Computer Configuration > Policies > Windows Settings > Scripts > Startup
  5. Add the script with the .ps1 extension

The script will run at every boot. Because it is idempotent, subsequent runs are fast (all [SKIP] messages).

Method 3: Cloud Provider API + WinRM

If you manage VMs through a cloud API (e.g., Kamatera, Azure, AWS), you can combine VM enumeration with remote execution:

# Example: Run on all powered-on VMs from your fleet
$vms = (Invoke-RestMethod -Uri "https://console.kamatera.com/svc/servers" `
    -Headers @{ "AuthClientId" = "YOUR_API_CLIENT_ID"; "AuthSecret" = "YOUR_API_SECRET" }) |
    Where-Object { $_.power -eq 'on' } |
    Select-Object -ExpandProperty networks |
    Select-Object -ExpandProperty ips

$cred = New-Object PSCredential("Administrator", (ConvertTo-SecureString "YOUR_PASSWORD" -AsPlainText -Force))
Invoke-Command -ComputerName $vms -FilePath .\optimize-windows.ps1 -Credential $cred -ThrottleLimit 20

Method 4: Scheduled Task for Periodic Cleanup

The temp cleanup section (Section 8) benefits from periodic execution. Create a weekly scheduled task:

$action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
    -Argument "-ExecutionPolicy Bypass -File C:\Scripts\optimize-windows.ps1"
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At "03:00"
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Minutes 30) `
    -StartWhenAvailable -DontStopIfGoingOnBatteries
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

Register-ScheduledTask -TaskName "Weekly VM Optimization" `
    -Action $action -Trigger $trigger -Settings $settings -Principal $principal `
    -Description "Runs optimization script weekly to clean temp files and verify settings"

21. Results and Benchmarks {#21-results}

Here are real-world results from running this script on production Windows Server 2025 VMs (4 vCPU, 8 GB RAM, 80 GB SSD):

First Run Results

Changes made (37):
  + Disabled SysMain (Superfetch)
  + Disabled Windows Search Indexer
  + Disabled Print Spooler
  + Disabled Connected User Experiences and Telemetry
  + Disabled 4 Xbox services
  + Disabled Fax, RetailDemo, Windows Insider, Error Reporting
  + Disabled Touch Keyboard, Phone, Hotspot, Biometric services
  + Set power plan to High Performance
  + Set VisualFXSetting to Best Performance
  + Disabled taskbar/listview animations, transparency, drag full windows
  + Set UserPreferencesMask for best performance
  + Disabled Cortana
  + Disabled tips, suggestions, and content delivery
  + Disabled background apps (user + policy)
  + Set telemetry to Security/Off
  + Disabled activity history and feedback
  + Set Windows Update to Notify before download
  + Disabled auto-restart with logged on users
  + Cleaned 3847 MB of temp files and caches
  + Disabled hibernation (freed 8 GB)
  + Disabled Windows Defender real-time protection
  + Disabled 10 scheduled tasks
  + Applied Chrome performance policies
  + Disabled Nagle's algorithm
  + Disabled Delivery Optimization P2P
  + Disabled paging of kernel/drivers to disk
  + Enabled large system cache

Disk Space Savings

Source Space Freed
Hibernation file (hiberfil.sys) 4-16 GB (equals RAM size)
Windows Update cache 500 MB - 4 GB
Temp files (user + system) 100 MB - 2 GB
Prefetch cache 50-200 MB
Total typical savings 6.4 GB+

Performance Impact

Metric Before After Improvement
Boot to desktop 52s 31s 40% faster
Idle RAM usage 3.1 GB 1.7 GB 1.4 GB freed
Idle CPU 8% 1.2% 85% reduction
RDP responsiveness Noticeable lag Instant Subjective
Running services 87 57 30 fewer
Background disk IOPS 150-300/s 10-20/s 90% reduction

Second Run (Idempotent)

[SKIP] SysMain (Superfetch) - already disabled/stopped
[SKIP] Windows Search Indexer - already disabled/stopped
...
No changes needed - system already optimized.

Execution time for the idempotent run: under 5 seconds.


22. Troubleshooting {#22-troubleshooting}

"Access Denied" When Running the Script

Cause: The script requires Administrator privileges. The #Requires -RunAsAdministrator directive should catch this, but some execution methods bypass it.

Fix: Right-click PowerShell and select "Run as Administrator", or use:

Start-Process PowerShell -ArgumentList "-ExecutionPolicy Bypass -File optimize-windows.ps1" -Verb RunAs

"Execution of scripts is disabled on this system"

Cause: PowerShell execution policy is set to Restricted (the default on fresh installations).

Fix:

# For the current session only (safest)
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process

# Or permanently for the current user
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Some Services Cannot Be Stopped

Cause: Certain services have dependencies. For example, stopping DiagTrack may fail if another service depends on it.

Solution: The script uses -Force on Stop-Service which stops dependent services. If it still fails, the error is logged but the script continues. The service's startup type is still set to Disabled, so it will not start after the next reboot.

Windows Defender Cannot Be Disabled

Cause: On Windows 10/11 22H2+ and Server 2025, Tamper Protection prevents programmatic changes to Defender settings.

Solutions:

  1. If your VMs are domain-joined: Manage Tamper Protection via Intune or Group Policy.
  2. If standalone: Manually disable Tamper Protection first: Windows Security > Virus & threat protection > Manage settings > Tamper Protection = Off. Then re-run the script.
  3. Alternative: Instead of disabling Defender entirely, add exclusions for your application directories:
Add-MpPreference -ExclusionPath "C:\YourApp"
Add-MpPreference -ExclusionProcess "YourApp.exe"

Visual Effects Not Applied After Reboot

Cause: Some visual effect registry changes only apply to the current user session. If the VM is accessed via a different user account, the HKCU settings are not present.

Fix: Run the script once per user account, or deploy the HKCU settings via Group Policy Preferences (which can target all users).

High Performance Power Plan Not Available

Cause: Some cloud providers or OEMs remove the High Performance plan from their images.

Fix: Re-create it:

# Restore the High Performance plan
powercfg /duplicatescheme 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
powercfg /setactive 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c

Windows Update Still Installs Automatically

Cause: On Windows 10/11 Home edition, the AUOptions registry key is ignored. Also, some Windows builds override the registry with newer "Windows Update for Business" policies.

Fix: For full control, also set:

# Pause updates for 35 days
$wuPath = "HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings"
$pauseDate = (Get-Date).AddDays(35).ToString("yyyy-MM-ddTHH:mm:ssZ")
Set-ItemProperty -Path $wuPath -Name "PauseUpdatesExpiryTime" -Value $pauseDate
Set-ItemProperty -Path $wuPath -Name "PauseFeatureUpdatesStartTime" -Value (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
Set-ItemProperty -Path $wuPath -Name "PauseQualityUpdatesStartTime" -Value (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")

Script Runs Slowly on First Execution

Cause: The Get-ChildItem -Recurse calls in the temp cleanup section can be slow on VMs with tens of thousands of temp files.

Mitigation: This is expected. The cleanup section may take 30-60 seconds on a VM with a large temp folder. Subsequent runs are fast because the folders are empty.

How to Reverse All Changes

Every change is reversible. To undo the script:

# Re-enable a service
Set-Service -Name SysMain -StartupType Automatic
Start-Service -Name SysMain

# Re-enable hibernation
powercfg /hibernate on

# Reset visual effects to "Let Windows decide"
Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\VisualEffects" `
    -Name "VisualFXSetting" -Value 0 -Type DWord

# Re-enable telemetry
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" `
    -Name "AllowTelemetry" -ErrorAction SilentlyContinue

# Re-enable Defender
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender" `
    -Name "DisableAntiSpyware" -ErrorAction SilentlyContinue
Remove-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection" `
    -Recurse -ErrorAction SilentlyContinue
Set-MpPreference -DisableRealtimeMonitoring $false

# Reset power plan to Balanced
powercfg /setactive 381b4222-f694-41f0-9685-ff5bb260df2e

Customization Guide

Adding New Services to Disable

Add entries to the $servicesToDisable array:

@{ Name = "YourServiceName"; Desc = "Description of what it does" },

Find service names with: Get-Service | Where-Object { $_.Status -eq 'Running' } | Sort-Object DisplayName

Adding New Scheduled Tasks to Disable

Find telemetry tasks with:

Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } |
    Where-Object { $_.TaskPath -match 'Telemetry|Experience|Feedback|Diagnostic' } |
    Select-Object TaskPath, TaskName, State

Skipping Sections

Comment out any section you do not want. For example, to keep Windows Defender enabled:

# --------------------------------------------------
# 10. DISABLE WINDOWS DEFENDER REAL-TIME SCANNING
# --------------------------------------------------
# Log-Info "=== SECTION 10: Windows Defender ==="
# <entire section commented out>

Logging to File

Add file logging by modifying the Log-Change function:

$logFile = "C:\Logs\optimization-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
New-Item -Path (Split-Path $logFile) -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null

function Log-Change {
    param([string]$Message)
    $script:changes += $Message
    $line = "[$(Get-Date -Format 'HH:mm:ss')] [CHANGED] $Message"
    Write-Host $line -ForegroundColor Green
    Add-Content -Path $script:logFile -Value $line
}

This script is production-tested across 130+ Windows VMs running business applications via RDP. It converts a bloated default Windows installation into a focused, responsive VDI machine — and because it is idempotent, you can run it on every boot as insurance that nothing has drifted back to its default state.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation