Skip to content

Commit cf73a9c

Browse files
authored
Merge pull request #1768 from fsprojects/repo-assist/perf-htmlnode-serialize-2026-04-29-3c3cf8d8a964a909
[Repo Assist] perf: HtmlNode.serialize avoids per-indent string alloc; isVoidElement as module-level constant
2 parents af42ebe + cc28a88 commit cf73a9c

2 files changed

Lines changed: 44 additions & 29 deletions

File tree

RELEASE_NOTES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Release Notes
22

3-
## 8.1.12 - Apr 28 2026
3+
## 8.1.13 - May 08 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+
7+
## 8.1.12 - Apr 29 2026
48

59
- Fix: `JsonValue.WriteTo` now always uses `CultureInfo.InvariantCulture` when serializing `Number` (decimal) values, preventing invalid JSON output (e.g. `1,5` instead of `1.5`) when called with a `TextWriter` configured with a non-English culture.
610
- Perf: `JsonValue.WriteTo` no longer allocates an intermediate `System.String(' ', n)` per indentation level; spaces are written directly to the writer.

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)