4kb Markdown Files Benchmark

Time to build 4096 markdown files

Last built at 2026-03-16T03:58:38 running @ 2445 Mhz

build.with.powershell.fast (0.93s)
Build Script

param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/PowerShellFast"
)


$mdPipelineBuilder = [Markdig.MarkdownPipelineBuilder]::new()
$mdPipeline = [Markdig.MarkdownExtensions]::UsePipeTables($mdPipelineBuilder).Build()

foreach ($fullPath in [IO.Directory]::EnumerateFileSystemEntries($InputPath)) {
    $fileHtml = [Markdig.Markdown]::ToHtml(
        [IO.File]::ReadAllText($fullPath), $mdPipeline
    )
    
    $fileName = @($fullPath -split '[\\/]')[-1]
    
    $directoryPath = $OutputPath, ($fileName -replace '\.md$') -join '/'

    if (-not [IO.Directory]::Exists($directoryPath)) {
        $null = [IO.Directory]::CreateDirectory($directoryPath)
    }

    $fileOutputPath = $OutputPath, ($fileName -replace '\.md$'), 'index.html' -join '/'
    
    [IO.File]::WriteAllText($fileOutputPath, $fileHtml)    
}

build.with.powershell.New-Item (2.73s)
Build Script

param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/PowerShellSlowest"
)


foreach ($file in Get-ChildItem -Path $InputPath -File) {
    
    $fileHtml = (ConvertFrom-Markdown -LiteralPath $file.FullName).Html
    
    $fileName = $file.Name
        
    $fileOutputPath = 
        Join-Path $OutputPath ($fileName -replace '\.md$') | 
            Join-Path -ChildPath ('index.html')

    New-Item -ItemType File -Path $fileOutputPath -Value $fileHtml -Force    
}

build.with.powershell.ConvertFrom-Markdown (2.76s)
Build Script

param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/PowerShellConvertFromMarkdown"
)


foreach ($file in Get-ChildItem -Path $InputPath -File) {
    
    $fileHtml = (ConvertFrom-Markdown -LiteralPath $file.FullName).Html
    
    $fileName = $file.Name
    
    $directoryPath = $OutputPath, ($fileName -replace '\.md$') -join '/'

    if (-not [IO.Directory]::Exists($directoryPath)) {
        $null = [IO.Directory]::CreateDirectory($directoryPath)
    }

    $fileOutputPath = $OutputPath, ($fileName -replace '\.md$'), 'index.html' -join '/'
    
    [IO.File]::WriteAllText($fileOutputPath, $fileHtml)    
}

build.with.powershell.Foreach-Object (3.16s)
Build Script

param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/PowerShellForeachObject"
)


Get-ChildItem -Path $InputPath -File |
    Foreach-Object {
        $in = $_
        New-Item -ItemType File -Path (
            Join-Path $OutputPath ($in.Name -replace '\.md$') | 
            Join-Path -ChildPath ./index.html
        ) -Value (
            ($in | 
                ConvertFrom-Markdown | 
                Select-Object -ExpandProperty html) 
        ) -Force
    }

build.with.11ty (6.27s)
Build Script

<#
.SYNOPSIS
    Builds 4kb markdown files with 11ty
.DESCRIPTION
    Builds 4kb markdown files with 11ty.
    
#>
Push-Location $PSScriptRoot

npx @11ty/eleventy --input=./TestMarkdown/*.md --output ./11ty/

Pop-Location


build.with.powershell.markx (7.85s)
Build Script

param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/PowerShellMarkX"
)


foreach ($fullPath in [IO.Directory]::EnumerateFileSystemEntries($InputPath)) {
    $markx = markx ([IO.File]::ReadAllText($fullPath))
    
    $fileName = @($fullPath -split '[\\/]')[-1]
    
    $directoryPath = $OutputPath, ($fileName -replace '\.md$') -join '/'

    if (-not [IO.Directory]::Exists($directoryPath)) {
        $null = [IO.Directory]::CreateDirectory($directoryPath)
    }

    $fileOutputPath = $OutputPath, ($fileName -replace '\.md$'), 'index.html' -join '/'
    
    [IO.File]::WriteAllText($fileOutputPath, $markx.html)    
}

build.with.astro (79.71s)
Build Script

<#
.SYNOPSIS
    Builds 4kb markdown files with astro
.DESCRIPTION
    Builds 4kb markdown files with astro.    
#>
param(
[Alias('Input')]
[string]
$InputPath = "$psScriptRoot/TestMarkdown",

[string]
$OutputPath = "$psScriptRoot/astro",

[string]
$AstroProjectRoot = "$psScriptRoot/astrodev"
)


if (-not (Test-path $AstroProjectRoot)) {
    $null = New-Item -ItemType Directory -Path $AstroProjectRoot -Force
}

Push-Location $AstroProjectRoot

@"
// @ts-check
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig($(
    [ordered]@{
        outDir = $OutputPath
    } | ConvertTo-Json
));
"@ > ./astro.config.mjs

npm run build

Pop-Location


The Numbers

TechniqueTimeRelativeSpeed
build.with.powershell.fast00:00:00.92569380.01161305955741971
build.with.powershell.New-Item00:00:02.72978640.03424585110350133
build.with.powershell.ConvertFrom-Markdown00:00:02.76264610.034658084234088296
build.with.powershell.Foreach-Object00:00:03.16003790.03964346346103161
build.with.11ty00:00:06.26664390.07861661041247217
build.with.powershell.markx00:00:07.84691290.0984414790825919
build.with.astro00:01:19.71144861
View Source

<#
.SYNOPSIS
    Builds 4kb Markdown files
.DESCRIPTION
    Builds 4kb Markdown files N different ways, in order to test performance.
.NOTES
    In the interests of fair play, any prerequisities should be installed before builds are timed.    
#>
param(
[uri]
$BuildTimeHistoryUrl = "https://4kb.powershellweb.com/history.json",

[string]$GitHubRepository = $env:GITHUB_REPOSITORY
)

# Make sure we're in the right place.
Push-Location $PSScriptRoot

$initStart = [DateTime]::Now

#region Install Prereqs
if ($env:GITHUB_WORKFLOW) {
    # Install 11ty to reduce 11ty build time
    $null = sudo npm install -g '@11ty/eleventy'    

    #region Astro Initialization
    # Install Astro to reduce astro build time
    $null = sudo npm install -g 'astro'

    $astroDevRoot = Join-Path $PSScriptRoot "astrodev"
    New-Item -ItemType File -Path (
        Join-Path $astroDevRoot package.json
    ) -Force -Value (    
        [Ordered]@{
            name='astro-test'
            type='module'
            version='0.0.1'
            scripts = [Ordered]@{
                "build" = "astro build"
            }
            dependencies = @{
                "astro" = 'latest'
            }
        } | ConvertTo-Json 
    )

    Push-Location $astroDevRoot

    npm install | Out-Host

    $pagesRoot = Join-Path $astroDevRoot "src/pages"
    if (-not (Test-Path $pagesRoot)) {
        New-Item -ItemType Directory -Path $pagesRoot
    }

    Copy-Item ../TestMarkdown/* $pagesRoot  

    Pop-Location

    #endregion Astro Initialization

    Install-Module MarkX -Force

    Import-Module MarkX -Global
}
$initEnd = [DateTime]::Now
#endregion Install Prereqs

#region Get Clock Speed
$cpuSpeed = 
    if ($executionContext.SessionState.PSVariable.Get('IsLinux').Value) {
        Get-Content /proc/cpuinfo -Raw -ErrorAction SilentlyContinue | 
            Select-String "(?<Unit>Mhz|MIPS)\s+\:\s+(?<Value>[\d\.]+)" | 
            Select-Object -First 1 -ExpandProperty Matches |
            ForEach-Object {
                $_.Groups["Value"].Value -as [int]
            }
    } elseif ($executionContext.SessionState.PSVariable.Get('IsMacOS').Value) {
        (sysctl -n hw.cpufrequency) / 1e6 -as [int]
    } else {
        $getCimInstance = $ExecutionContext.SessionState.InvokeCommand.GetCommand('Get-CimInstance','Cmdlet')
        if ($getCimInstance) {
            & $getCimInstance -Class Win32_Processor |
                Select-Object -ExpandProperty MaxClockSpeed
        }
    }
#endregion

$mySelf = $MyInvocation.MyCommand.ScriptBlock
$StartTime = [DateTime]::Now

& {
    foreach ($file in Get-ChildItem -filter build.with.*.ps1) {
        try {
            $techniqueName = $file.Name -replace '\.build\.with' -replace '\.ps1$'
            $script = (Get-Command $file.FullName -CommandType ExternalScript).ScriptBlock
            $time = Measure-Command { . $file.FullName } 
            [PSCustomObject]@{
                Technique = $techniqueName
                Time = $time
                Script = $script
            }
        } catch {
            Write-Warning "$file encountered an error: $_"
        }
        
    }
} | Tee-Object -Variable buildTimes


$buildTimes = $buildTimes | Sort-Object Time

foreach ($buildTime in $buildTimes) {
    $relativeSpeed = $buildTime.Time.TotalMilliseconds / $buildTimes[-1].Time.TotalMilliseconds
    Add-Member NoteProperty -InputObject $buildTime -Name RelativeSpeed $relativeSpeed -Force
    Add-Member NoteProperty -InputObject $buildTime -Name DateTime $StartTime -Force
}

$history = @(try {
    Invoke-RestMethod -Uri $BuildTimeHistoryUrl -ErrorAction Ignore
} catch {}) -ne $null

$buildTimes | ConvertTo-Html -Title BuildTimes > ./times.html


$descriptionMessage = @(foreach ($buildtime in $buildTimes) {    
    ($buildTime.Technique, $buildTime.Time -join ':')    
}) -join [Environment]::NewLine

@(
    "<html>"
    
    "<head>"    
    
    "<title>4kb Markdown Files</title>"
    
    "<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1.0' />"

    "<meta name='og:title' content='4kb Markdown Files Benchmark' />"
    
    "<meta name='og:description' content='
4kb Markdown Files Benchmark. 
The fastest framework is no framework.
$([Web.HttpUtility]::HtmlAttributeEncode($descriptionMessage))
' />"

    "<meta name='article:published_time' content='$($StartTime.ToString('s'))' />"

    "<style>"
    
    "
    
    body { height: 100vh; max-width: 100vw; margin:0 } 
    
    svg { height: 5%; }
    h1,h2, h3,h4 { text-align: center; }
    .techniqueSummary { font-size: 2rem; }

    "
    "</style>"
    "</head>"
    "<body>"
    "<h1>4kb Markdown Files Benchmark</h1>"
    "<h2>Time to build 4096 markdown files</h2>"    
    "<h3>Last built at $([DateTime]::UtcNow.ToString("s")) running @ $cpuSpeed Mhz</h3>"
    if ($GitHubRepository) {
        "<h4><a href='https://github.com/$GitHubRepository/'><button>Github Repo</button></a></h4>"
        
        "<h4>"
        "<a href='https://github.com/$GitHubRepository/actions/workflows/deploy.yml'>"
            "<img src='https://github.com/$GitHubRepository/actions/workflows/deploy.yml/badge.svg'></img>"
        "</a>"        
    "</h4>"
    }
    
    # Make a really basic heatmap (yes, this is that easy)
    # We want the fastest item to be the most green, so start by getting its relative speed
    $fastestRelativeSpeed = $buildTimes[0].RelativeSpeed
    foreach ($buildTime in $buildTimes) {
        # We only neeed two colors for the heatmap: green and red.
        # Green is calculated by:
        $green = [byte][Math]::Floor(
            # Starting at one
            (1 -             
                # Subtracting the relative speed
                $buildTime.RelativeSpeed + 
                # and adding the fastest relative speed.
                $fastestRelativeSpeed                
            ) * 255  # (multiply by 255 and floor it to make a byte)
        )
        # Red is even easier, it's just the relative speed as a byte
        $red = [byte][Math]::Floor(
            $buildTime.RelativeSpeed * 255  
        )

        # Use a bit of C# string formatting to make a color (blue is always blank)
        $color = "#{0:x2}{1:x2}00" -f $red, $green

        "<details open>"
            "<summary class='techniqueSummary'>$($buildTime.Technique) ($([Math]::Round(
                $buildTime.Time.TotalSeconds, 2
            ))s)</summary>"
            "<details>"
            "<summary>Build Script</summary>"
            "<pre><code class='langauge-PowerShell'>"
            [Web.HttpUtility]::HtmlEncode($buildTime.Script)
            "</code></pre>"
            "</details>"
            "<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'>"
                "<rect x='0%' width='1%' height='100%' fill='$color' stroke='currentColor'>"
                    "<animate attributeName='width' from='1%' to='100%' dur='$($buildTime.Time.TotalSeconds)s' fill='freeze' />"
                "</rect>"
            "</svg>"
        "</details>"
    }
    
    "<h3>The Numbers</h3>"
    $buildTimes | 
        Select-Object Technique, Time, RelativeSpeed | 
        ConvertTo-Html -Fragment
    "<details>"
        "<summary>View Source</summary>"
        "<pre><code class='language-PowerShell'>"
        [Web.HttpUtility]::HtmlEncode("$mySelf")
        "</code></pre>"
    "</details>"
    "</body>"
    "</html>"
) > ./index.html

foreach ($buildTime in $buildTimes) {
    Add-Member NoteProperty -InputObject $buildTime -Name Time "$($buildTime.Time)" -Force
}

<#
$history = @(try {
    Invoke-RestMethod -Uri $BuildTimeHistoryUrl -ErrorAction Ignore
} catch {}) -ne $null

$history += $buildTimes | 
    Select-Object Technique, Time, RelativeSpeed, DateTime

ConvertTo-Json -InputObject $history > ./history.json -Depth 4
#>

Remove-Item -Recurse -Force ./astrodev

Pop-Location

# Set our last exit code to 0
$LASTEXITCODE = 0
exit 0