Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For element nodes with a single text child, directly set textContent #3939

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

andrewiggins
Copy link
Member

This PR adds a (hopefully) fast path for element VNodes with a single text child. Given how common this kind of tree structure (after all, UI is ultimately about fancy ways to display text lol), I hypothesize this should improve our rendering runtime.

Two significant internal changes that comes out of this are:

  1. Single text children no longer are represented as VNodes
    With this change, single text children no longer get a VNode created representing them. Hopefully this will help contribute to better runtime and memory performance by removing lots of allocations. But it does have implications for the VNode tree (which leads to the next point).

  2. Elements with a single text child have a null _children property
    Since single text childs no longer have a VNode created, the _children property of their parents is empty (aka null). Inspecting the tree VNode tree just using _children would then lead to an incomplete tree. We could fix this by adding the string to the _children array, but then that breaks the assumption the codebase has had for a while that _children is always Array<VNode | null>. But perhaps that is a better approach?

For now, I'm going to try this implementation and see if any performance improvements are observable. If so, then we can further pursue what the right thing do here is.

@github-actions
Copy link

github-actions bot commented Mar 17, 2023

📊 Tachometer Benchmark Results

Summary

duration

  • 02_replace1k: slower ❌ 9% - 13% (12.52ms - 17.44ms)
    preact-local vs preact-master
  • 03_update10th1k_x16: unsure 🔍 -3% - +4% (-1.33ms - +2.06ms)
    preact-local vs preact-master
  • 07_create10k: faster ✔ 0% - 2% (6.72ms - 24.43ms)
    preact-local vs preact-master
  • filter_list: unsure 🔍 -1% - +3% (-0.22ms - +0.57ms)
    preact-local vs preact-master
  • hydrate1k: slower ❌ 3% - 10% (4.87ms - 14.31ms)
    preact-local vs preact-master
  • many_updates: faster ✔ 16% - 22% (5.59ms - 8.00ms)
    preact-local vs preact-master
  • text_update: faster ✔ 1% - 11% (0.03ms - 0.39ms)
    preact-local vs preact-master
  • todo: faster ✔ 10% - 12% (5.21ms - 6.39ms)
    preact-local vs preact-master

usedJSHeapSize

  • 02_replace1k: faster ✔ 3% - 10% (0.11ms - 0.39ms)
    preact-local vs preact-master
  • 03_update10th1k_x16: faster ✔ 5% - 5% (0.18ms - 0.19ms)
    preact-local vs preact-master
  • 07_create10k: faster ✔ 7% - 7% (1.81ms - 1.87ms)
    preact-local vs preact-master
  • filter_list: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-master
  • hydrate1k: unsure 🔍 -8% - +1% (-1.05ms - +0.08ms)
    preact-local vs preact-master
  • many_updates: faster ✔ 36% - 36% (1.66ms - 1.66ms)
    preact-local vs preact-master
  • text_update: faster ✔ 0% - 1% (0.00ms - 0.00ms)
    preact-local vs preact-master
  • todo: faster ✔ 0% - 1% (0.00ms - 0.01ms)
    preact-local vs preact-master

Results

02_replace1k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master132.24ms - 136.21ms-faster ✔
8% - 12%
12.52ms - 17.44ms
faster ✔
12% - 15%
18.02ms - 22.63ms
preact-local147.76ms - 150.65msslower ❌
9% - 13%
12.52ms - 17.44ms
-faster ✔
2% - 5%
3.48ms - 7.21ms
preact-hooks153.38ms - 155.72msslower ❌
13% - 17%
18.02ms - 22.63ms
slower ❌
2% - 5%
3.48ms - 7.21ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master3.47ms - 3.74ms-slower ❌
3% - 12%
0.11ms - 0.39ms
slower ❌
1% - 10%
0.05ms - 0.35ms
preact-local3.35ms - 3.37msfaster ✔
3% - 10%
0.11ms - 0.39ms
-unsure 🔍
-3% - +0%
-0.11ms - +0.02ms
preact-hooks3.34ms - 3.47msfaster ✔
2% - 10%
0.05ms - 0.35ms
unsure 🔍
-1% - +3%
-0.02ms - +0.11ms
-

run-warmup-0

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master44.05ms - 46.06ms-unsure 🔍
-2% - +5%
-0.94ms - +2.11ms
unsure 🔍
-3% - +3%
-1.55ms - +1.20ms
preact-local43.32ms - 45.61msunsure 🔍
-5% - +2%
-2.11ms - +0.94ms
-unsure 🔍
-5% - +2%
-2.24ms - +0.71ms
preact-hooks44.29ms - 46.16msunsure 🔍
-3% - +3%
-1.20ms - +1.55ms
unsure 🔍
-2% - +5%
-0.71ms - +2.24ms
-

run-warmup-1

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master66.34ms - 68.57ms-slower ❌
0% - 5%
0.20ms - 3.11ms
unsure 🔍
-3% - +2%
-1.76ms - +1.26ms
preact-local64.87ms - 66.73msfaster ✔
0% - 5%
0.20ms - 3.11ms
-faster ✔
1% - 5%
0.53ms - 3.28ms
preact-hooks66.69ms - 68.72msunsure 🔍
-2% - +3%
-1.26ms - +1.76ms
slower ❌
1% - 5%
0.53ms - 3.28ms
-

run-warmup-2

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master45.04ms - 48.53ms-unsure 🔍
-7% - +1%
-3.58ms - +0.36ms
faster ✔
0% - 8%
0.18ms - 3.99ms
preact-local47.48ms - 49.30msunsure 🔍
-1% - +8%
-0.36ms - +3.58ms
-unsure 🔍
-3% - +1%
-1.67ms - +0.71ms
preact-hooks48.11ms - 49.64msslower ❌
0% - 9%
0.18ms - 3.99ms
unsure 🔍
-1% - +3%
-0.71ms - +1.67ms
-

run-warmup-3

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master56.61ms - 60.70ms-slower ❌
23% - 34%
10.74ms - 15.50ms
slower ❌
24% - 33%
10.85ms - 15.05ms
preact-local44.32ms - 46.76msfaster ✔
19% - 26%
10.74ms - 15.50ms
-unsure 🔍
-3% - +2%
-1.48ms - +1.14ms
preact-hooks45.23ms - 46.19msfaster ✔
19% - 25%
10.85ms - 15.05ms
unsure 🔍
-3% - +3%
-1.14ms - +1.48ms
-

run-warmup-4

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master58.06ms - 60.48ms-slower ❌
24% - 32%
11.43ms - 14.68ms
slower ❌
29% - 37%
13.16ms - 16.31ms
preact-local45.13ms - 47.30msfaster ✔
20% - 24%
11.43ms - 14.68ms
-slower ❌
0% - 7%
0.20ms - 3.16ms
preact-hooks43.53ms - 45.54msfaster ✔
23% - 27%
13.16ms - 16.31ms
faster ✔
0% - 7%
0.20ms - 3.16ms
-

run-final

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master38.26ms - 42.11ms-faster ✔
24% - 32%
13.56ms - 17.93ms
faster ✔
30% - 37%
18.33ms - 22.90ms
preact-local54.90ms - 56.97msslower ❌
32% - 46%
13.56ms - 17.93ms
-faster ✔
5% - 11%
3.26ms - 6.47ms
preact-hooks59.57ms - 62.03msslower ❌
43% - 59%
18.33ms - 22.90ms
slower ❌
6% - 12%
3.26ms - 6.47ms
-
03_update10th1k_x16

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master47.09ms - 49.44ms-unsure 🔍
-4% - +3%
-2.06ms - +1.33ms
unsure 🔍
-5% - +3%
-2.22ms - +1.45ms
preact-local47.41ms - 49.85msunsure 🔍
-3% - +4%
-1.33ms - +2.06ms
-unsure 🔍
-4% - +4%
-1.88ms - +1.84ms
preact-hooks47.24ms - 50.06msunsure 🔍
-3% - +5%
-1.45ms - +2.22ms
unsure 🔍
-4% - +4%
-1.84ms - +1.88ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master3.44ms - 3.45ms-slower ❌
5% - 6%
0.18ms - 0.19ms
slower ❌
5% - 5%
0.16ms - 0.17ms
preact-local3.26ms - 3.27msfaster ✔
5% - 5%
0.18ms - 0.19ms
-faster ✔
0% - 1%
0.01ms - 0.02ms
preact-hooks3.28ms - 3.29msfaster ✔
5% - 5%
0.16ms - 0.17ms
slower ❌
0% - 1%
0.01ms - 0.02ms
-
07_create10k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1380.29ms - 1393.74ms-slower ❌
0% - 2%
6.72ms - 24.43ms
slower ❌
0% - 2%
3.28ms - 20.96ms
preact-local1365.68ms - 1377.20msfaster ✔
0% - 2%
6.72ms - 24.43ms
-unsure 🔍
-1% - +0%
-11.59ms - +4.67ms
preact-hooks1369.16ms - 1380.64msfaster ✔
0% - 2%
3.28ms - 20.96ms
unsure 🔍
-0% - +1%
-4.67ms - +11.59ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master25.41ms - 25.46ms-slower ❌
8% - 8%
1.81ms - 1.87ms
slower ❌
8% - 8%
1.80ms - 1.87ms
preact-local23.57ms - 23.62msfaster ✔
7% - 7%
1.81ms - 1.87ms
-unsure 🔍
-0% - +0%
-0.04ms - +0.03ms
preact-hooks23.58ms - 23.62msfaster ✔
7% - 7%
1.80ms - 1.87ms
unsure 🔍
-0% - +0%
-0.03ms - +0.04ms
-
filter_list

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master22.15ms - 22.71ms-unsure 🔍
-3% - +1%
-0.57ms - +0.22ms
unsure 🔍
-1% - +2%
-0.28ms - +0.44ms
preact-local22.33ms - 22.88msunsure 🔍
-1% - +3%
-0.22ms - +0.57ms
-unsure 🔍
-0% - +3%
-0.10ms - +0.60ms
preact-hooks22.13ms - 22.58msunsure 🔍
-2% - +1%
-0.44ms - +0.28ms
unsure 🔍
-3% - +0%
-0.60ms - +0.10ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1.58ms - 1.58ms-unsure 🔍
-0% - -0%
-0.00ms - -0.00ms
faster ✔
1% - 1%
0.02ms - 0.02ms
preact-local1.58ms - 1.58msunsure 🔍
+0% - +0%
+0.00ms - +0.00ms
-faster ✔
1% - 1%
0.02ms - 0.02ms
preact-hooks1.61ms - 1.61msslower ❌
1% - 1%
0.02ms - 0.02ms
slower ❌
1% - 1%
0.02ms - 0.02ms
-
hydrate1k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master148.14ms - 154.49ms-faster ✔
3% - 9%
4.87ms - 14.31ms
faster ✔
4% - 10%
6.69ms - 15.88ms
preact-local157.42ms - 164.39msslower ❌
3% - 10%
4.87ms - 14.31ms
-unsure 🔍
-4% - +2%
-6.51ms - +3.12ms
preact-hooks159.28ms - 165.92msslower ❌
4% - 11%
6.69ms - 15.88ms
unsure 🔍
-2% - +4%
-3.12ms - +6.51ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master12.82ms - 13.65ms-unsure 🔍
-1% - +8%
-0.08ms - +1.05ms
unsure 🔍
-3% - +6%
-0.38ms - +0.72ms
preact-local12.37ms - 13.13msunsure 🔍
-8% - +1%
-1.05ms - +0.08ms
-unsure 🔍
-6% - +2%
-0.83ms - +0.21ms
preact-hooks12.71ms - 13.42msunsure 🔍
-5% - +3%
-0.72ms - +0.38ms
unsure 🔍
-2% - +7%
-0.21ms - +0.83ms
-
many_updates

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master35.45ms - 37.16ms-slower ❌
18% - 28%
5.59ms - 8.00ms
slower ❌
17% - 26%
5.33ms - 7.57ms
preact-local28.66ms - 30.36msfaster ✔
16% - 22%
5.59ms - 8.00ms
-unsure 🔍
-5% - +3%
-1.47ms - +0.77ms
preact-hooks29.13ms - 30.58msfaster ✔
15% - 21%
5.33ms - 7.57ms
unsure 🔍
-3% - +5%
-0.77ms - +1.47ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master4.62ms - 4.63ms-slower ❌
56% - 56%
1.66ms - 1.66ms
slower ❌
55% - 55%
1.63ms - 1.64ms
preact-local2.97ms - 2.97msfaster ✔
36% - 36%
1.66ms - 1.66ms
-faster ✔
1% - 1%
0.02ms - 0.02ms
preact-hooks2.99ms - 2.99msfaster ✔
35% - 35%
1.63ms - 1.64ms
slower ❌
1% - 1%
0.02ms - 0.02ms
-
text_update

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master3.47ms - 3.71ms-slower ❌
1% - 12%
0.03ms - 0.39ms
unsure 🔍
-4% - +4%
-0.16ms - +0.16ms
preact-local3.25ms - 3.51msfaster ✔
1% - 11%
0.03ms - 0.39ms
-faster ✔
1% - 10%
0.04ms - 0.37ms
preact-hooks3.49ms - 3.69msunsure 🔍
-4% - +4%
-0.16ms - +0.16ms
slower ❌
1% - 11%
0.04ms - 0.37ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.81ms - 0.81ms-slower ❌
0% - 1%
0.00ms - 0.00ms
faster ✔
1% - 2%
0.01ms - 0.01ms
preact-local0.81ms - 0.81msfaster ✔
0% - 1%
0.00ms - 0.00ms
-faster ✔
2% - 2%
0.02ms - 0.02ms
preact-hooks0.82ms - 0.83msslower ❌
1% - 2%
0.01ms - 0.01ms
slower ❌
2% - 2%
0.02ms - 0.02ms
-
todo

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master53.55ms - 54.45ms-slower ❌
11% - 13%
5.21ms - 6.39ms
slower ❌
8% - 10%
3.90ms - 5.13ms
preact-local47.82ms - 48.59msfaster ✔
10% - 12%
5.21ms - 6.39ms
-faster ✔
1% - 4%
0.71ms - 1.85ms
preact-hooks49.06ms - 49.90msfaster ✔
7% - 9%
3.90ms - 5.13ms
slower ❌
1% - 4%
0.71ms - 1.85ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1.07ms - 1.07ms-slower ❌
0% - 1%
0.00ms - 0.01ms
faster ✔
2% - 2%
0.02ms - 0.03ms
preact-local1.06ms - 1.07msfaster ✔
0% - 1%
0.00ms - 0.01ms
-faster ✔
2% - 3%
0.02ms - 0.03ms
preact-hooks1.09ms - 1.09msslower ❌
2% - 2%
0.02ms - 0.03ms
slower ❌
2% - 3%
0.02ms - 0.03ms
-

tachometer-reporter-action v2 for Benchmarks

@coveralls
Copy link

coveralls commented Mar 17, 2023

Coverage Status

Coverage: 99.701% (+0.003%) from 99.699% when pulling 2226d4c on skip-append-for-element-text-child into e97da39 on master.

@github-actions
Copy link

github-actions bot commented Mar 17, 2023

Size Change: +374 B (0%)

Total Size: 54.7 kB

Filename Size Change
dist/preact.js 4.27 kB +60 B (1%)
dist/preact.min.js 4.3 kB +63 B (1%)
dist/preact.min.module.js 4.3 kB +62 B (1%)
dist/preact.min.umd.js 4.33 kB +61 B (1%)
dist/preact.module.js 4.29 kB +59 B (1%)
dist/preact.umd.js 4.34 kB +69 B (1%)
ℹ️ View Unchanged
Filename Size Change
compat/dist/compat.js 3.91 kB 0 B
compat/dist/compat.module.js 3.84 kB 0 B
compat/dist/compat.umd.js 3.98 kB 0 B
debug/dist/debug.js 3 kB 0 B
debug/dist/debug.module.js 3.01 kB 0 B
debug/dist/debug.umd.js 3.08 kB 0 B
devtools/dist/devtools.js 232 B 0 B
devtools/dist/devtools.module.js 240 B 0 B
devtools/dist/devtools.umd.js 315 B 0 B
hooks/dist/hooks.js 1.53 kB 0 B
hooks/dist/hooks.module.js 1.56 kB 0 B
hooks/dist/hooks.umd.js 1.61 kB 0 B
jsx-runtime/dist/jsxRuntime.js 360 B 0 B
jsx-runtime/dist/jsxRuntime.module.js 326 B 0 B
jsx-runtime/dist/jsxRuntime.umd.js 441 B 0 B
test-utils/dist/testUtils.js 442 B 0 B
test-utils/dist/testUtils.module.js 444 B 0 B
test-utils/dist/testUtils.umd.js 526 B 0 B

compressed-size-action

Comment on lines -1674 to -1677
'<div>.appendChild(#text)',
'<div>1.insertBefore(<div>3, #text)',
'<div>.appendChild(#text)',
'<div>31.insertBefore(<div>4, #text)',
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the test changes here are just removing calls to appendChild(#text). But this test case invokes something more interesting I think it worth examining.

Here, we are transitioning from a tree like:

<div>1</div>
<div>2</div>

to

<div class="wrapper">
  <div>3</div>
  <div>4</div>
</div>

Our diff matches <div>1 with the new div.wrapper. So it sees a div with a single text child (1) transitioning to a div with multiple children <div>3 and <div>4. So with this update, instead of creating and inserting the new children before the old children, it removes the text node and appends the new children to the now empty div.

Comment on lines -1689 to -1695
[
'<div>34.insertBefore(#text, <div>3)',
'<div>4.remove()',
'<div>3.remove()',
'<div>.appendChild(#text)',
'<div>1.appendChild(<div>2)'
],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, we are transition from a tree like:

<div class="wrapper">
  <div>3</div>
  <div>4</div>
</div>

to

<div>1</div>
<div>2</div>

Our diff matches the old div.wrapper with the new <div>1 node. So it sees a div with multiple children transitioning to a div with a single text child. To handle this, it clears away the old children by setting the textContent of div.wrapper to the new single text child. That change is why you no longer see the calls to .remove(). Also since we set textContent to the new text child, you don't see the relevant insertBefore call of the text node. In one operation we unmount old children and set new children.

We do still call unmount on old children so any lifecycle methods are called, but no DOM is removed in those calls since setting textContent will take care of that.

@andrewiggins
Copy link
Member Author

I think the regression on 02_replace1k is due to changes in how V8 is optimizing the code. Check out the trace logs below:
preact-master:
image

preact-local:
Screenshot 2023-03-17 at 4 38 16 PM

The differences in colors under the red underlined area show that V8's optimization heuristics are treating each of these differently. I don't entirely understand the difference but running this benchmark locally and comparing the average duration of all the runs (warmups + final test run) shows that there isn't really a significant difference between on average between the two:
image

@andrewiggins andrewiggins force-pushed the skip-append-for-element-text-child branch from 5242ac0 to 2a3bba9 Compare March 24, 2023 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants