Skip to content

Commit c26ae20

Browse files
perf: HtmlNode.serialize avoids per-indent string alloc; isVoidElement as module-level constant; HtmlDocument uses single StringBuilder
- Move void-element set to a module-level [<AutoOpen>] private module so it is computed once at startup instead of being re-created on every HtmlNode.ToString() call. - Replace `String(' ', indentation + plus) |> append` in the serialize `newLine` helper with `sb.Append(' ', indentation + plus)` which writes spaces directly into the StringBuilder without allocating an intermediate string. - Rewrite HtmlDocument.ToString() to use a single StringBuilder rather than `List.map … |> String.Concat`, avoiding the intermediate list allocation. 2980 tests pass (net8.0, -p:NuGetAudit=false). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3b3f825 commit c26ae20

2 files changed

Lines changed: 43 additions & 28 deletions

File tree

RELEASE_NOTES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Release Notes
22

3+
## 8.1.12 - Apr 29 2026
4+
5+
- Performance: `HtmlNode.serialize` no longer allocates a temporary `string` on each newline/indentation step; uses `StringBuilder.Append(char, int)` overload directly. `isVoidElement` set is now computed once at module initialisation instead of being re-created on every `HtmlNode.ToString()` call. `HtmlDocument.ToString()` uses a single `StringBuilder` for the whole document instead of `List.map … |> String.Concat`.
6+
37
## 8.1.11 - Apr 22 2026
48

59
- Code: `HtmlParser` `EmitTag` removes dead code in the `else` branch (the expression `x.HasFormattedParent || x.IsFormattedTag` was always equivalent to `x.HasFormattedParent` since `x.IsFormattedTag` is always `false` in that branch). Uses `name` directly to avoid re-computing `CurrentTagName()` for formatted/script tag checks. Also removes redundant `.ToLowerInvariant()` calls in `IsFormattedTag` and `IsScriptTag` since tag names are already lowercased at read time.

src/FSharp.Data.Html.Core/HtmlNode.fs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ open System.Text
77

88
// --------------------------------------------------------------------------------------
99

10+
[<AutoOpen>]
11+
module private HtmlNodeHelpers =
12+
// Void elements per the HTML spec — stored as a module-level constant so the Set is not
13+
// re-created on every HtmlNode.ToString() call.
14+
let private htmlVoidElementSet =
15+
Set.ofArray
16+
[| "area"
17+
"base"
18+
"br"
19+
"col"
20+
"command"
21+
"embed"
22+
"hr"
23+
"img"
24+
"input"
25+
"keygen"
26+
"link"
27+
"meta"
28+
"param"
29+
"source"
30+
"track"
31+
"wbr" |]
32+
33+
let isVoidElement name = Set.contains name htmlVoidElementSet
34+
35+
// --------------------------------------------------------------------------------------
36+
1037
/// <summary>Represents an HTML attribute. The name is always normalized to lowercase</summary>
1138
/// <namespacedoc>
1239
/// <summary>Contains the primary types for the FSharp.Data package.</summary>
@@ -92,28 +119,6 @@ type HtmlNode =
92119
static member NewCData content = HtmlCData(content)
93120

94121
override x.ToString() =
95-
let isVoidElement =
96-
let set =
97-
[| "area"
98-
"base"
99-
"br"
100-
"col"
101-
"command"
102-
"embed"
103-
"hr"
104-
"img"
105-
"input"
106-
"keygen"
107-
"link"
108-
"meta"
109-
"param"
110-
"source"
111-
"track"
112-
"wbr" |]
113-
|> Set.ofArray
114-
115-
fun name -> Set.contains name set
116-
117122
let rec serialize (sb: StringBuilder) indentation canAddNewLine insidePre html =
118123
let append (str: string) = sb.Append str |> ignore
119124

@@ -124,7 +129,7 @@ type HtmlNode =
124129

125130
let newLine plus =
126131
sb.AppendLine() |> ignore
127-
String(' ', indentation + plus) |> append
132+
sb.Append(' ', indentation + plus) |> ignore
128133

129134
match html with
130135
| HtmlElement(name, attributes, elements) ->
@@ -224,11 +229,17 @@ type HtmlDocument =
224229
override x.ToString() =
225230
match x with
226231
| HtmlDocument(docType, elements) ->
227-
(if String.IsNullOrEmpty docType then
228-
""
229-
else
230-
"<!DOCTYPE " + docType + ">" + Environment.NewLine)
231-
+ (elements |> List.map (fun x -> x.ToString()) |> String.Concat)
232+
let sb = StringBuilder()
233+
234+
if not (String.IsNullOrEmpty docType) then
235+
sb.Append("<!DOCTYPE ") |> ignore
236+
sb.Append(docType) |> ignore
237+
sb.AppendLine(">") |> ignore
238+
239+
for element in elements do
240+
sb.Append(element.ToString()) |> ignore
241+
242+
sb.ToString()
232243

233244
/// <exclude />
234245
[<EditorBrowsableAttribute(EditorBrowsableState.Never)>]

0 commit comments

Comments
 (0)