Skip to content

Commit 51771f7

Browse files
authored
Inline diffProps function (#4200)
Inline `diffProps` to take advantage of the props loop to read values and use later in the diff. This avoids duplicate megamorphic reads for props like `dangerouslySetInnerHTML`, `children`, `value`, and `checked` since we can reuse reading the value in the props loop for handling elsewhere. * Inline diffProps (-11 B) * Inline dangerouslySetInnerHtml handling (-1 B) Removes a guaranteed megamorphic access in diffElementNodes * Golf dangerouslySetInnerHtml handling (-4 B) * Pull of new children while looping through props (+2 B) * Read prop value once at start of props loop (+0 B) * Use prop loop for handling `value` and `checked` (-20 B) * Add types to new variables * Clean up comments * Fix benchmark name * Add run tracking to 03_update10th1k * Capture input value and checked props in first props loop (+17 B) * Golf diffElementNodes (-7 B)
1 parent b4a1cc2 commit 51771f7

File tree

4 files changed

+88
-81
lines changed

4 files changed

+88
-81
lines changed

.github/workflows/single-bench.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
type: choice
99
options:
1010
- 02_replace1k
11-
- 03_update10k
11+
- 03_update10th1k_x16
1212
- 07_create10k
1313
- filter_list
1414
- hydrate1k

benches/src/03_update10th1k_x16.html

+7-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
afterFrameAsync,
2828
getRowLinkSel,
2929
testElement,
30-
testElementTextContains
30+
testElementTextContains,
31+
markRunStart,
32+
markRunEnd
3133
} from './util.js';
3234
import * as framework from 'framework';
3335
import { render } from '../src/keyed-children/index.js';
@@ -53,17 +55,21 @@
5355
testElement(getRowLinkSel(1000));
5456

5557
for (let i = 0; i < 3; i++) {
58+
markRunStart(`warmup-${i}`);
5659
update();
60+
await markRunEnd(`warmup-${i}`);
5761

5862
await afterFrameAsync();
5963
testElementTextContains(getRowLinkSel(991), repeat(' !!!', i + 1));
6064
}
6165
}
6266

6367
async function run() {
68+
markRunStart('final');
6469
performance.mark('start');
6570
update();
6671

72+
await markRunEnd('final');
6773
await afterFrameAsync();
6874
testElementTextContains(getRowLinkSel(991), repeat(' !!!', 3 + 1));
6975
performance.mark('stop');

src/diff/index.js

+79-47
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { BaseComponent, getDomSibling } from '../component';
88
import { Fragment } from '../create-element';
99
import { diffChildren } from './children';
10-
import { diffProps, setProperty } from './props';
10+
import { setProperty } from './props';
1111
import { assign, isArray, removeNode, slice } from '../util';
1212
import options from '../options';
1313

@@ -364,24 +364,33 @@ function diffElementNodes(
364364
let newProps = newVNode.props;
365365
let nodeType = /** @type {string} */ (newVNode.type);
366366
/** @type {any} */
367-
let i = 0;
367+
let i;
368+
/** @type {{ __html?: string }} */
369+
let newHtml;
370+
/** @type {{ __html?: string }} */
371+
let oldHtml;
372+
/** @type {ComponentChildren} */
373+
let newChildren;
374+
let value;
375+
let inputValue;
376+
let checked;
368377

369378
// Tracks entering and exiting SVG namespace when descending through the tree.
370379
if (nodeType === 'svg') isSvg = true;
371380

372381
if (excessDomChildren != null) {
373-
for (; i < excessDomChildren.length; i++) {
374-
const child = excessDomChildren[i];
382+
for (i = 0; i < excessDomChildren.length; i++) {
383+
value = excessDomChildren[i];
375384

376385
// if newVNode matches an element in excessDomChildren or the `dom`
377386
// argument matches an element in excessDomChildren, remove it from
378387
// excessDomChildren so it isn't later removed in diffChildren
379388
if (
380-
child &&
381-
'setAttribute' in child === !!nodeType &&
382-
(nodeType ? child.localName === nodeType : child.nodeType === 3)
389+
value &&
390+
'setAttribute' in value === !!nodeType &&
391+
(nodeType ? value.localName === nodeType : value.nodeType === 3)
383392
) {
384-
dom = child;
393+
dom = value;
385394
excessDomChildren[i] = null;
386395
break;
387396
}
@@ -401,7 +410,8 @@ function diffElementNodes(
401410

402411
// we created a new parent, so none of the previously attached children can be reused:
403412
excessDomChildren = null;
404-
// we are creating a new node, so we can assume this is a new subtree (in case we are hydrating), this deopts the hydrate
413+
// we are creating a new node, so we can assume this is a new subtree (in
414+
// case we are hydrating), this deopts the hydrate
405415
isHydrating = false;
406416
}
407417

@@ -416,43 +426,67 @@ function diffElementNodes(
416426

417427
oldProps = oldVNode.props || EMPTY_OBJ;
418428

419-
let oldHtml = oldProps.dangerouslySetInnerHTML;
420-
let newHtml = newProps.dangerouslySetInnerHTML;
421-
422-
// During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
423-
// @TODO we should warn in debug mode when props don't match here.
424-
if (!isHydrating) {
425-
// But, if we are in a situation where we are using existing DOM (e.g. replaceNode)
426-
// we should read the existing DOM attributes to diff them
427-
if (excessDomChildren != null) {
428-
oldProps = {};
429-
for (i = 0; i < dom.attributes.length; i++) {
430-
oldProps[dom.attributes[i].name] = dom.attributes[i].value;
431-
}
429+
// If we are in a situation where we are not hydrating but are using
430+
// existing DOM (e.g. replaceNode) we should read the existing DOM
431+
// attributes to diff them
432+
if (!isHydrating && excessDomChildren != null) {
433+
oldProps = {};
434+
for (i = 0; i < dom.attributes.length; i++) {
435+
value = dom.attributes[i];
436+
oldProps[value.name] = value.value;
432437
}
438+
}
433439

434-
if (newHtml || oldHtml) {
435-
// Avoid re-applying the same '__html' if it did not changed between re-render
436-
if (
437-
!newHtml ||
438-
((!oldHtml || newHtml.__html != oldHtml.__html) &&
439-
newHtml.__html !== dom.innerHTML)
440-
) {
441-
dom.innerHTML = (newHtml && newHtml.__html) || '';
442-
}
440+
for (i in oldProps) {
441+
value = oldProps[i];
442+
if (i == 'children') {
443+
} else if (i == 'dangerouslySetInnerHTML') {
444+
oldHtml = value;
445+
} else if (i !== 'key' && !(i in newProps)) {
446+
setProperty(dom, i, null, value, isSvg);
443447
}
444448
}
445449

446-
diffProps(dom, newProps, oldProps, isSvg, isHydrating);
450+
// During hydration, props are not diffed at all (including dangerouslySetInnerHTML)
451+
// @TODO we should warn in debug mode when props don't match here.
452+
for (i in newProps) {
453+
value = newProps[i];
454+
if (i == 'children') {
455+
newChildren = value;
456+
} else if (i == 'dangerouslySetInnerHTML') {
457+
newHtml = value;
458+
} else if (i == 'value') {
459+
inputValue = value;
460+
} else if (i == 'checked') {
461+
checked = value;
462+
} else if (
463+
i !== 'key' &&
464+
(!isHydrating || typeof value == 'function') &&
465+
oldProps[i] !== value
466+
) {
467+
setProperty(dom, i, value, oldProps[i], isSvg);
468+
}
469+
}
447470

448471
// If the new vnode didn't have dangerouslySetInnerHTML, diff its children
449472
if (newHtml) {
473+
// Avoid re-applying the same '__html' if it did not changed between re-render
474+
if (
475+
!isHydrating &&
476+
(!oldHtml ||
477+
(newHtml.__html !== oldHtml.__html &&
478+
newHtml.__html !== dom.innerHTML))
479+
) {
480+
dom.innerHTML = newHtml.__html;
481+
}
482+
450483
newVNode._children = [];
451484
} else {
452-
i = newVNode.props.children;
485+
if (oldHtml) dom.innerHTML = '';
486+
453487
diffChildren(
454488
dom,
455-
isArray(i) ? i : [i],
489+
isArray(newChildren) ? newChildren : [newChildren],
456490
newVNode,
457491
oldVNode,
458492
globalContext,
@@ -474,30 +508,28 @@ function diffElementNodes(
474508
}
475509
}
476510

477-
// (as above, don't diff props during hydration)
511+
// As above, don't diff props during hydration
478512
if (!isHydrating) {
513+
i = 'value';
479514
if (
480-
'value' in newProps &&
481-
(i = newProps.value) !== undefined &&
515+
inputValue !== undefined &&
482516
// #2756 For the <progress>-element the initial value is 0,
483517
// despite the attribute not being present. When the attribute
484518
// is missing the progress bar is treated as indeterminate.
485519
// To fix that we'll always update it when it is 0 for progress elements
486-
(i !== dom.value ||
487-
(nodeType === 'progress' && !i) ||
520+
(inputValue !== dom[i] ||
521+
(nodeType === 'progress' && !inputValue) ||
488522
// This is only for IE 11 to fix <select> value not being updated.
489523
// To avoid a stale select value we need to set the option.value
490524
// again, which triggers IE11 to re-evaluate the select value
491-
(nodeType === 'option' && i !== oldProps.value))
525+
(nodeType === 'option' && inputValue !== oldProps[i]))
492526
) {
493-
setProperty(dom, 'value', i, oldProps.value, false);
527+
setProperty(dom, i, inputValue, oldProps[i], false);
494528
}
495-
if (
496-
'checked' in newProps &&
497-
(i = newProps.checked) !== undefined &&
498-
i !== dom.checked
499-
) {
500-
setProperty(dom, 'checked', i, oldProps.checked, false);
529+
530+
i = 'checked';
531+
if (checked !== undefined && checked !== dom[i]) {
532+
setProperty(dom, i, checked, oldProps[i], false);
501533
}
502534
}
503535
}

src/diff/props.js

+1-32
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,6 @@
11
import { IS_NON_DIMENSIONAL } from '../constants';
22
import options from '../options';
33

4-
/**
5-
* Diff the old and new properties of a VNode and apply changes to the DOM node
6-
* @param {PreactElement} dom The DOM node to apply changes to
7-
* @param {object} newProps The new props
8-
* @param {object} oldProps The old props
9-
* @param {boolean} isSvg Whether or not this node is an SVG node
10-
* @param {boolean} hydrate Whether or not we are in hydration mode
11-
*/
12-
export function diffProps(dom, newProps, oldProps, isSvg, hydrate) {
13-
let i;
14-
15-
for (i in oldProps) {
16-
if (i !== 'children' && i !== 'key' && !(i in newProps)) {
17-
setProperty(dom, i, null, oldProps[i], isSvg);
18-
}
19-
}
20-
21-
for (i in newProps) {
22-
if (
23-
(!hydrate || typeof newProps[i] == 'function') &&
24-
i !== 'children' &&
25-
i !== 'key' &&
26-
i !== 'value' &&
27-
i !== 'checked' &&
28-
oldProps[i] !== newProps[i]
29-
) {
30-
setProperty(dom, i, newProps[i], oldProps[i], isSvg);
31-
}
32-
}
33-
}
34-
354
function setStyle(style, key, value) {
365
if (key[0] === '-') {
376
style.setProperty(key, value == null ? '' : value);
@@ -104,7 +73,7 @@ export function setProperty(dom, name, value, oldValue, isSvg) {
10473
const handler = useCapture ? eventProxyCapture : eventProxy;
10574
dom.removeEventListener(name, handler, useCapture);
10675
}
107-
} else if (name !== 'dangerouslySetInnerHTML') {
76+
} else {
10877
if (isSvg) {
10978
// Normalize incorrect prop usage for SVG:
11079
// - xlink:href / xlinkHref --> href (xlink:href was removed from SVG and isn't needed)

0 commit comments

Comments
 (0)