From 88bdfcbece60e417275d1dc0bf6e9f2b112ffddb Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Fri, 10 Apr 2026 11:01:15 +0000
Subject: [PATCH 1/2] perf: compile regex instances to module-level singletons
Five locations were creating a new uncompiled Regex on every call:
- PageContentList.mkPageContentMenu: new Regex per page rendered
- Formatting.fs (search index): Regex.Replace (static, uncompiled) per HTML page
- HtmlFormatting.formatAnchor: Regex.Matches per heading processed
- Menu.snakeCase: Regex.Replace (static, uncompiled) per menu item
- LlmsTxt (collapseBlankLines, normaliseTitle): two Regex.Replace per page entry
Each is now a module-level let binding with RegexOptions.Compiled, so the
pattern is compiled once at startup and reused for the lifetime of the
process.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
RELEASE_NOTES.md | 3 +++
src/FSharp.Formatting.Common/Menu.fs | 5 ++++-
src/FSharp.Formatting.Common/PageContentList.fs | 9 +++++----
src/FSharp.Formatting.Literate/Formatting.fs | 5 ++++-
src/FSharp.Formatting.Markdown/HtmlFormatting.fs | 5 ++++-
src/fsdocs-tool/BuildCommand.fs | 11 +++++++++--
6 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 33214cbc4..3b4c0b50e 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,9 @@
## [Unreleased]
+### Changed
+* Compile `Regex` instances to module-level singletons (with `RegexOptions.Compiled`) in `PageContentList`, `HtmlFormatting`, `Formatting`, `Menu`, and `LlmsTxt`. Previously a new, uncompiled `Regex` was constructed on every call (once per page heading, once per HTML page, once per menu item, once per llms.txt entry), incurring repeated JIT overhead. The patterns are now compiled once at module load and reused across all calls.
+
## [22.0.0] - 2026-04-03
### Fixed
diff --git a/src/FSharp.Formatting.Common/Menu.fs b/src/FSharp.Formatting.Common/Menu.fs
index b788de4a0..508b0453b 100644
--- a/src/FSharp.Formatting.Common/Menu.fs
+++ b/src/FSharp.Formatting.Common/Menu.fs
@@ -12,8 +12,11 @@ type MenuItem =
IsActive: bool }
/// Converts a display string to a snake_case HTML id attribute value
+let private snakeCaseRegex =
+ System.Text.RegularExpressions.Regex("[A-Z]", System.Text.RegularExpressions.RegexOptions.Compiled)
+
let private snakeCase (v: string) =
- System.Text.RegularExpressions.Regex.Replace(v, "[A-Z]", "$0").Replace(" ", "_").ToLower()
+ snakeCaseRegex.Replace(v, "$0").Replace(" ", "_").ToLower()
/// Renders an HTML navigation menu for the given header and items using template files in `input`
let createMenu (input: string) (isCategoryActive: bool) (header: string) (items: MenuItem list) : string =
diff --git a/src/FSharp.Formatting.Common/PageContentList.fs b/src/FSharp.Formatting.Common/PageContentList.fs
index 5300132be..fb92d31e7 100644
--- a/src/FSharp.Formatting.Common/PageContentList.fs
+++ b/src/FSharp.Formatting.Common/PageContentList.fs
@@ -13,10 +13,11 @@ let EmptyContent = "
"
/// We process the html to collect the table of content.
/// We can't use the doc.MarkdownDocument because we cannot easily get the generated id values.
/// It is safer to parse the html.
-let mkPageContentMenu (html: string) =
- let headingLinkPattern = "]*href=\"([^\"]+)\">([^<]+)"
- let regex = Regex(headingLinkPattern)
+// Compiled once at module load; reused across all pages.
+let private headingLinkRegex = Regex("]*href=\"([^\"]+)\">([^<]+)", RegexOptions.Compiled)
+
+let mkPageContentMenu (html: string) =
let extractHeadingLinks (matchItem: Match) =
let level = int matchItem.Groups.[1].Value
@@ -26,7 +27,7 @@ let mkPageContentMenu (html: string) =
linkText, li [ Class $"level-%i{level}" ] [ a [ Href href ] [ !!linkText ] ]
let headingTexts, listItems =
- regex.Matches(html)
+ headingLinkRegex.Matches(html)
|> Seq.cast
|> Seq.map extractHeadingLinks
|> Seq.toList
diff --git a/src/FSharp.Formatting.Literate/Formatting.fs b/src/FSharp.Formatting.Literate/Formatting.fs
index 3bc583e81..65355c6be 100644
--- a/src/FSharp.Formatting.Literate/Formatting.fs
+++ b/src/FSharp.Formatting.Literate/Formatting.fs
@@ -13,6 +13,9 @@ open FSharp.Formatting.Templating
/// substitution key–value pairs used by the templating engine to populate page templates.
module internal Formatting =
+ // Compiled once at module load; reused for every HTML page's search-index text extraction.
+ let private htmlTagRegex = Regex("<.*?>", RegexOptions.Compiled ||| RegexOptions.Singleline)
+
/// Format document with the specified output kind
let format (doc: MarkdownDocument) generateAnchors outputKind substitutions crefResolver mdlinkResolver =
match outputKind with
@@ -290,7 +293,7 @@ module internal Formatting =
(match ctx.OutputKind with
| OutputKind.Html ->
// Strip the html tags
- let fullText = Regex.Replace(formattedDocument, "<.*?>", "")
+ let fullText = htmlTagRegex.Replace(formattedDocument, "")
Some(IndexText(fullText, headingTexts))
| _ -> None)
diff --git a/src/FSharp.Formatting.Markdown/HtmlFormatting.fs b/src/FSharp.Formatting.Markdown/HtmlFormatting.fs
index 11cf1c58f..0a2f56d25 100644
--- a/src/FSharp.Formatting.Markdown/HtmlFormatting.fs
+++ b/src/FSharp.Formatting.Markdown/HtmlFormatting.fs
@@ -129,9 +129,12 @@ let rec internal formatSpan (ctx: FormattingContext) span =
and internal formatSpans ctx = List.iter (formatSpan ctx)
/// generate anchor name from Markdown text
+// Compiled once at module load; reused for every heading anchor generated.
+let private wordRegex = Regex(@"\w+", RegexOptions.Compiled)
+
let internal formatAnchor (ctx: FormattingContext) (spans: MarkdownSpans) =
let extractWords (text: string) =
- Regex.Matches(text, @"\w+") |> Seq.cast |> Seq.map (fun m -> m.Value)
+ wordRegex.Matches(text) |> Seq.cast |> Seq.map (fun m -> m.Value)
let rec gather (span: MarkdownSpan) : string seq =
seq {
diff --git a/src/fsdocs-tool/BuildCommand.fs b/src/fsdocs-tool/BuildCommand.fs
index b5abd1db8..7732e025f 100644
--- a/src/fsdocs-tool/BuildCommand.fs
+++ b/src/fsdocs-tool/BuildCommand.fs
@@ -1319,6 +1319,13 @@ module Serve =
/// Helpers for generating llms.txt and llms-full.txt content.
module internal LlmsTxt =
+ // Compiled once at module load; reused across all llms.txt page entries.
+ let private multipleNewlinesRegex =
+ System.Text.RegularExpressions.Regex(@"\n{3,}", System.Text.RegularExpressions.RegexOptions.Compiled)
+
+ let private whitespaceRunRegex =
+ System.Text.RegularExpressions.Regex(@"\s+", System.Text.RegularExpressions.RegexOptions.Compiled)
+
/// Decode HTML entities (e.g. " → ", > → >) in a string.
let private decodeHtml (s: string) = System.Net.WebUtility.HtmlDecode(s)
@@ -1338,11 +1345,11 @@ module internal LlmsTxt =
/// Collapse three or more consecutive newlines into at most two.
let private collapseBlankLines (s: string) =
- System.Text.RegularExpressions.Regex.Replace(s, @"\n{3,}", "\n\n")
+ multipleNewlinesRegex.Replace(s, "\n\n")
/// Normalise a title: trim and collapse internal whitespace/newlines to a single space.
let private normaliseTitle (s: string) =
- System.Text.RegularExpressions.Regex.Replace(s.Trim(), @"\s+", " ")
+ whitespaceRunRegex.Replace(s.Trim(), " ")
/// Decode HTML entities and remove --eval noise from content.
let private cleanContent (s: string) =
From 65aebd6fa6454300b5340a6c046565d0e66e7a17 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Fri, 10 Apr 2026 11:01:18 +0000
Subject: [PATCH 2/2] ci: trigger checks