Skip to content

Commit 2bac269

Browse files
committed
Imrpove performance for createElement on IReactProperty seq
1 parent e366fd9 commit 2bac269

6 files changed

Lines changed: 306 additions & 20 deletions

File tree

src/Feliz/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## 3.0.0-rc.3 - 2025-09-18
11+
12+
### Changed
13+
14+
- Improved performance for createElement used for `seq<IReactProperty>`
15+
1016
## 3.0.0-rc.2 - 2025-09-18
1117

1218
### Fixed

src/Feliz/Html.fs

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,28 @@ module HtmlHelper =
1313
ReactLegacy.createElement(tag, children = children)
1414

1515
/// Iterates once, returns (firstMatch, restWithoutMatch).
16-
let tryExtractFirst (predicate: 'T -> bool) (xs: 'T list) : ('T option * 'T list) =
17-
let rec loop acc = function
18-
| [] -> None, List.rev acc
19-
| x::xs when predicate x ->
20-
Some x, List.rev acc @ xs
21-
| x::xs ->
22-
loop (x::acc) xs
23-
loop [] xs
16+
let extractByKeyFast (key: string) (arr: seq<(string * obj)>) : (string * obj)[] * (string * obj) option =
17+
Fable.Core.JsInterop.emitJsStatement (arr, key) """const arrNext_ = Array.from($0);
18+
for (let i = 0; i < arrNext_.length; i++) {
19+
if (arrNext_[i][0] === $1) {
20+
const item = arrNext_[i];
21+
arrNext_[i] = arrNext_[arrNext_.length - 1]; // overwrite with last
22+
arrNext_.pop(); // remove last
23+
return [arrNext_, item]; // extracted tuple
24+
}
25+
}
26+
return [arrNext_, null]; // return full arr and None option for children"""
2427

2528
let createElement (name: string) (props: IReactProperty list) : ReactElement =
26-
match unbox<(string*obj) list> props |> tryExtractFirst (fun (key, _) -> key = "children") with
27-
| Some (_, children), props when not (Interop.isString children) && Interop.isIterable children ->
29+
30+
match unbox<(string*obj) list> props |> extractByKeyFast "children" with
31+
| props, Some (_, children) ->
2832
ReactLegacy.createElement(
2933
name,
3034
Fable.Core.JsInterop.createObj props,
3135
unbox<ReactElement list> children
3236
)
33-
| Some (_, children), props ->
34-
ReactLegacy.createElement(
35-
name,
36-
Fable.Core.JsInterop.createObj props,
37-
unbox<ReactElement> children
38-
)
39-
| None, props ->
37+
| props, None ->
4038
ReactLegacy.createElement(
4139
name,
4240
Fable.Core.JsInterop.createObj props

src/Feliz/Properties.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1793,11 +1793,11 @@ type prop =
17931793
static member inline testId(value: string) = PropHelper.mkAttr "data-testid" value
17941794

17951795
/// Defines the text content of the element. Alias for `children [ Html.text value ]`
1796-
static member inline text (value: float) = PropHelper.mkAttr "children" value
1796+
static member inline text (value: float) = PropHelper.mkAttr "children" [value]
17971797
/// Defines the text content of the element. Alias for `children [ Html.text value ]`
1798-
static member inline text (value: int) = PropHelper.mkAttr "children" value
1798+
static member inline text (value: int) = PropHelper.mkAttr "children" [value]
17991799
/// Defines the text content of the element. Alias for `children [ Html.text value ]`
1800-
static member inline text (value: string) = PropHelper.mkAttr "children" value
1800+
static member inline text (value: string) = PropHelper.mkAttr "children" [value]
18011801

18021802
/// Defines the text content of the element. Alias for `children [ Html.text (sprintf ...) ]`
18031803
static member inline textf fmt = Printf.kprintf prop.text fmt

tests/Feliz/Basic.test.fs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,184 @@ open Fable.Core
88
open Vitest
99
open Basic
1010

11+
describe "Simple createElement calls" <| fun _ ->
12+
13+
test "Html.div children" <| fun _ ->
14+
let render =
15+
RTL.render(
16+
Html.div [
17+
Html.h1 "Hello, World!"
18+
Html.p "This is a simple div component."
19+
]
20+
)
21+
22+
let h1 = render.getByText "Hello, World!"
23+
expect(h1).toBeInTheDocument()
24+
let p = render.getByText "This is a simple div component."
25+
expect(p).toBeInTheDocument()
26+
27+
test "JSX.div children" <| fun _ ->
28+
let render =
29+
RTL.render(
30+
JSX.Html.div [
31+
JSX.Html.h1 "Hello, World!"
32+
JSX.Html.p "This is a simple div component."
33+
]
34+
)
35+
36+
let h1 = render.getByText "Hello, World!"
37+
expect(h1).toBeInTheDocument()
38+
let p = render.getByText "This is a simple div component."
39+
expect(p).toBeInTheDocument()
40+
41+
test "Html.div child" <| fun _ ->
42+
let render =
43+
RTL.render(
44+
Html.div (Html.h1 "Hello, World!")
45+
)
46+
47+
let h1 = render.getByText "Hello, World!"
48+
expect(h1).toBeInTheDocument()
49+
50+
test "JSX.Html.div child" <| fun _ ->
51+
let render =
52+
RTL.render(
53+
JSX.Html.div (JSX.Html.h1 "Hello, World!")
54+
)
55+
56+
let h1 = render.getByText "Hello, World!"
57+
expect(h1).toBeInTheDocument()
58+
59+
test "Html.div [prop.children children]" <| fun _ ->
60+
let render =
61+
RTL.render(
62+
Html.div [
63+
prop.testId "simpleDiv"
64+
prop.children [
65+
Html.h1 "Hello, World!"
66+
Html.p "This is a simple div component."
67+
]
68+
]
69+
)
70+
71+
let h1 = render.getByText "Hello, World!"
72+
expect(h1).toBeInTheDocument()
73+
let p = render.getByText "This is a simple div component."
74+
expect(p).toBeInTheDocument()
75+
76+
test "JSX.Html.div [prop.children children]" <| fun _ ->
77+
let render =
78+
RTL.render(
79+
JSX.Html.div [
80+
prop.testId "simpleDiv"
81+
prop.children [
82+
JSX.Html.h1 "Hello, World!"
83+
JSX.Html.p "This is a simple div component."
84+
]
85+
]
86+
)
87+
88+
let h1 = render.getByText "Hello, World!"
89+
expect(h1).toBeInTheDocument()
90+
let p = render.getByText "This is a simple div component."
91+
expect(p).toBeInTheDocument()
92+
93+
test "Html.div [prop.children child]" <| fun _ ->
94+
let render =
95+
RTL.render(
96+
Html.div [
97+
prop.testId "simpleDiv"
98+
prop.children (Html.h1 "Hello, World!")
99+
]
100+
)
101+
102+
let h1 = render.getByText "Hello, World!"
103+
expect(h1).toBeInTheDocument()
104+
105+
test "JSX.Html.div [prop.children child]" <| fun _ ->
106+
let render =
107+
RTL.render(
108+
JSX.Html.div [
109+
prop.testId "simpleDiv"
110+
prop.children (JSX.Html.h1 "Hello, World!")
111+
]
112+
)
113+
114+
let h1 = render.getByText "Hello, World!"
115+
expect(h1).toBeInTheDocument()
116+
117+
test "Html.div [prop.text string]" <| fun _ ->
118+
let render =
119+
RTL.render(
120+
Html.div [
121+
prop.testId "simpleDiv"
122+
prop.text "Hello, World!"
123+
]
124+
)
125+
126+
let text = render.getByText "Hello, World!"
127+
expect(text).toBeInTheDocument()
128+
129+
test "JSX.Html.div [prop.text string]" <| fun _ ->
130+
let render =
131+
RTL.render(
132+
JSX.Html.div [
133+
prop.testId "simpleDiv"
134+
prop.text "Hello, World!"
135+
]
136+
)
137+
138+
let text = render.getByText "Hello, World!"
139+
expect(text).toBeInTheDocument()
140+
141+
test "Html.div [prop.text int]" <| fun _ ->
142+
let render =
143+
RTL.render(
144+
Html.div [
145+
prop.testId "simpleDiv"
146+
prop.text 42
147+
]
148+
)
149+
150+
let text = render.getByText "42"
151+
expect(text).toBeInTheDocument()
152+
153+
test "JSX.Html.div [prop.text int]" <| fun _ ->
154+
let render =
155+
RTL.render(
156+
JSX.Html.div [
157+
prop.testId "simpleDiv"
158+
prop.text 42
159+
]
160+
)
161+
162+
let text = render.getByText "42"
163+
expect(text).toBeInTheDocument()
164+
165+
test "Html.div [prop.text float]" <| fun _ ->
166+
let render =
167+
RTL.render(
168+
Html.div [
169+
prop.testId "simpleDiv"
170+
prop.text 42.42
171+
]
172+
)
173+
174+
let text = render.getByText "42.42"
175+
expect(text).toBeInTheDocument()
176+
177+
test "JSX.Html.div [prop.text float]" <| fun _ ->
178+
let render =
179+
RTL.render(
180+
JSX.Html.div [
181+
prop.testId "simpleDiv"
182+
prop.text 42.42
183+
]
184+
)
185+
186+
let text = render.getByText "42.42"
187+
expect(text).toBeInTheDocument()
188+
11189
describe "Basic Tests" <| fun _ ->
12190

13191
test "Html elements can be rendered" <| fun _ ->
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
module ExtractChildrenTests
2+
3+
open Feliz
4+
open System
5+
open Vitest
6+
7+
describe "HtmlHelper.createElement" <| fun _ ->
8+
9+
test "extract from seq" <| fun _ ->
10+
let child = [ Html.span []; Html.div [] ]
11+
let props =
12+
seq {
13+
"id", box "my-div"
14+
"children", box child
15+
"className", box "container"
16+
"data-test", box true
17+
"style", box {| color = "red" |}
18+
"onClick", box (fun _ -> ())
19+
"tabIndex", box 0
20+
}
21+
let extractedProps, childOption = HtmlHelper.extractByKeyFast "children" props
22+
expect(child).toBeTruthy()
23+
let key, child =
24+
match childOption with
25+
| Some kvp -> kvp
26+
| None -> failwith "Expected to find 'children' key"
27+
expect(key).toBe("children")
28+
expect(child).toEqual(child)
29+
let propsContainChildren: bool = extractedProps |> Array.exists (fun (k, _) -> k = "children")
30+
expect(propsContainChildren).toBeFalsy()
31+
expect(extractedProps.Length).toBe((props |> Seq.length) - 1)
32+
33+
test "extract from list" <| fun _ ->
34+
let child = [ Html.span []; Html.div [] ]
35+
let props =
36+
[
37+
"id", box "my-div"
38+
"children", box child
39+
"className", box "container"
40+
"data-test", box true
41+
"style", box {| color = "red" |}
42+
"onClick", box (fun _ -> ())
43+
"tabIndex", box 0
44+
]
45+
let extractedProps, childOption = HtmlHelper.extractByKeyFast "children" props
46+
expect(child).toBeTruthy()
47+
let key, child =
48+
match childOption with
49+
| Some kvp -> kvp
50+
| None -> failwith "Expected to find 'children' key"
51+
expect(key).toBe("children")
52+
expect(child).toEqual(child)
53+
let propsContainChildren: bool = extractedProps |> Array.exists (fun (k, _) -> k = "children")
54+
expect(propsContainChildren).toBeFalsy()
55+
expect(extractedProps.Length).toBe((props |> Seq.length) - 1)
56+
57+
test "extract from array" <| fun _ ->
58+
let child = [ Html.span []; Html.div [] ]
59+
let props =
60+
[|
61+
"id", box "my-div"
62+
"children", box child
63+
"className", box "container"
64+
"data-test", box true
65+
"style", box {| color = "red" |}
66+
"onClick", box (fun _ -> ())
67+
"tabIndex", box 0
68+
|]
69+
let extractedProps, childOption = HtmlHelper.extractByKeyFast "children" props
70+
expect(child).toBeTruthy()
71+
let key, child =
72+
match childOption with
73+
| Some kvp -> kvp
74+
| None -> failwith "Expected to find 'children' key"
75+
expect(key).toBe("children")
76+
expect(child).toEqual(child)
77+
let propsContainChildren: bool = extractedProps |> Array.exists (fun (k, _) -> k = "children")
78+
expect(propsContainChildren).toBeFalsy()
79+
expect(extractedProps.Length).toBe((props |> Seq.length) - 1)
80+
81+
test "extract from ResizeArray" <| fun _ ->
82+
let child = [ Html.span []; Html.div [] ]
83+
let props =
84+
ResizeArray [
85+
"id", box "my-div"
86+
"children", box child
87+
"className", box "container"
88+
"data-test", box true
89+
"style", box {| color = "red" |}
90+
"onClick", box (fun _ -> ())
91+
"tabIndex", box 0
92+
]
93+
let extractedProps, childOption = HtmlHelper.extractByKeyFast "children" props
94+
expect(child).toBeTruthy()
95+
let key, child =
96+
match childOption with
97+
| Some kvp -> kvp
98+
| None -> failwith "Expected to find 'children' key"
99+
expect(key).toBe("children")
100+
expect(child).toEqual(child)
101+
let propsContainChildren: bool = extractedProps |> Array.exists (fun (k, _) -> k = "children")
102+
expect(propsContainChildren).toBeFalsy()
103+
expect(extractedProps.Length).toBe((props |> Seq.length) - 1)

tests/Feliz/Feliz.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
</PropertyGroup>
66
<ItemGroup>
77
<Content Include="package.json" />
8+
<Compile Include="ExtractChildren.test.fs" />
89
<Compile Include="CodeSplitting.fs" />
910
<Compile Include="PropHelpers.test.fs" />
1011
<Compile Include="Legacy.fs" />

0 commit comments

Comments
 (0)