From 027315124aebdc77070084a64cad6148c9d892cb Mon Sep 17 00:00:00 2001 From: jdecroock Date: Fri, 24 Oct 2025 17:33:37 +0200 Subject: [PATCH] Experiment: add moveBefore rather than insertBefor when available --- src/constants.js | 3 +++ src/diff/children.js | 24 +++++++++++++++---- test/_util/logCall.js | 8 +++++++ test/browser/fragments.test.jsx | 4 ++++ test/browser/keys.test.jsx | 3 +++ .../lifecycles/shouldComponentUpdate.test.jsx | 4 ++++ test/browser/render.test.jsx | 4 ++++ 7 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/constants.js b/src/constants.js index 1ad82b208e..194e88231a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -33,3 +33,6 @@ export const EMPTY_OBJ = /** @type {any} */ ({}); export const EMPTY_ARR = []; export const MATHML_TOKEN_ELEMENTS = /(mi|mn|mo|ms$|mte|msp)/; + +export const HAS_MOVE_BEFORE_SUPPORT = + typeof window !== 'undefined' && 'moveBefore' in Element.prototype; diff --git a/src/diff/children.js b/src/diff/children.js index d920dce452..0004b6c79f 100644 --- a/src/diff/children.js +++ b/src/diff/children.js @@ -6,7 +6,8 @@ import { INSERT_VNODE, MATCHED, UNDEFINED, - NULL + NULL, + HAS_MOVE_BEFORE_SUPPORT } from '../constants'; import { isArray } from '../util'; import { getDomSibling } from '../component'; @@ -127,7 +128,13 @@ export function diffChildren( let shouldPlace = childVNode._flags & INSERT_VNODE; if (shouldPlace || oldVNode._children === childVNode._children) { - oldDom = insert(childVNode, oldDom, parentDom, shouldPlace); + oldDom = insert( + childVNode, + oldDom, + parentDom, + shouldPlace, + oldVNode == NULL || oldVNode._original == NULL + ); // When a matched VNode is physically moved via INSERT_VNODE, its old // _dom pointer becomes a stale positional reference. Clear it so that @@ -344,9 +351,10 @@ function constructNewChildrenArray( * @param {PreactElement} oldDom * @param {PreactElement} parentDom * @param {number} shouldPlace + * @param {boolean} isMounting * @returns {PreactElement} */ -function insert(parentVNode, oldDom, parentDom, shouldPlace) { +function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) { // Note: VNodes in nested suspended trees may be missing _children. if (typeof parentVNode.type == 'function') { let children = parentVNode._children; @@ -357,7 +365,7 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) { // children's _parent pointer to point to the newVNode (parentVNode // here). children[i]._parent = parentVNode; - oldDom = insert(children[i], oldDom, parentDom, shouldPlace); + oldDom = insert(children[i], oldDom, parentDom, shouldPlace, false); } } @@ -367,7 +375,13 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) { if (oldDom && parentVNode.type && !oldDom.parentNode) { oldDom = getDomSibling(parentVNode); } - parentDom.insertBefore(parentVNode._dom, oldDom || NULL); + + if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) { + // @ts-expect-error This isn't added to TypeScript lib.d.ts yet + parentDom.moveBefore(parentVNode._dom, oldDom); + } else { + parentDom.insertBefore(parentVNode._dom, oldDom || NULL); + } } oldDom = parentVNode._dom; } diff --git a/test/_util/logCall.js b/test/_util/logCall.js index 3fb094ca78..64fd3141b9 100644 --- a/test/_util/logCall.js +++ b/test/_util/logCall.js @@ -39,6 +39,14 @@ export function logCall(obj, method) { } break; } + case 'moveBefore': { + if (args[1] === null && args.length === 2) { + operation = `${serialize(this)}.appendChild(${serialize(args[0])})`; + } else { + operation = `${serialize(this)}.insertBefore(${c})`; + } + break; + } default: { operation = `${serialize(this)}.${String(method)}(${c})`; break; diff --git a/test/browser/fragments.test.jsx b/test/browser/fragments.test.jsx index ff3cbdd538..0f8cd6ed1d 100644 --- a/test/browser/fragments.test.jsx +++ b/test/browser/fragments.test.jsx @@ -38,9 +38,12 @@ describe('Fragment', () => { let resetAppendChild; let resetRemove; let resetRemoveText; + let resetMoveBefore; beforeAll(() => { resetInsertBefore = logCall(Element.prototype, 'insertBefore'); + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetAppendChild = logCall(Element.prototype, 'appendChild'); resetRemove = logCall(Element.prototype, 'remove'); resetRemoveText = logCall(Text.prototype, 'remove'); @@ -57,6 +60,7 @@ describe('Fragment', () => { }); afterAll(() => { + resetMoveBefore(); resetInsertBefore(); resetAppendChild(); resetRemove(); diff --git a/test/browser/keys.test.jsx b/test/browser/keys.test.jsx index 5734b3ab81..54a672da8a 100644 --- a/test/browser/keys.test.jsx +++ b/test/browser/keys.test.jsx @@ -71,6 +71,7 @@ describe('keys', () => { values.splice(to, 0, value); } + let resetMoveBefore; let resetAppendChild; let resetInsertBefore; let resetRemoveChild; @@ -78,6 +79,7 @@ describe('keys', () => { let resetRemoveText; beforeAll(() => { + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'removeChild'); @@ -91,6 +93,7 @@ describe('keys', () => { resetRemoveChild(); resetRemove(); resetRemoveText(); + resetMoveBefore(); }); beforeEach(() => { diff --git a/test/browser/lifecycles/shouldComponentUpdate.test.jsx b/test/browser/lifecycles/shouldComponentUpdate.test.jsx index 3ecd7e8ad3..2d8618b277 100644 --- a/test/browser/lifecycles/shouldComponentUpdate.test.jsx +++ b/test/browser/lifecycles/shouldComponentUpdate.test.jsx @@ -15,10 +15,13 @@ describe('Lifecycle methods', () => { // expect(getLog()).to.deep.equal(expectedOperations, message); // } let resetInsertBefore; + let resetMoveBefore; let resetRemoveChild; let resetRemove; beforeAll(() => { + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); resetRemoveChild = logCall(Element.prototype, 'appendChild'); resetRemove = logCall(Element.prototype, 'removeChild'); @@ -28,6 +31,7 @@ describe('Lifecycle methods', () => { resetInsertBefore(); resetRemoveChild(); resetRemove(); + resetMoveBefore(); }); beforeEach(() => { diff --git a/test/browser/render.test.jsx b/test/browser/render.test.jsx index 91e7074f8f..b89c9a288e 100644 --- a/test/browser/render.test.jsx +++ b/test/browser/render.test.jsx @@ -30,6 +30,7 @@ describe('render()', () => { let resetInsertBefore; let resetRemoveText; let resetRemove; + let resetMoveBefore; beforeEach(() => { scratch = setupScratch(); @@ -43,12 +44,15 @@ describe('render()', () => { beforeAll(() => { resetAppendChild = logCall(Element.prototype, 'appendChild'); resetInsertBefore = logCall(Element.prototype, 'insertBefore'); + // @ts-expect-error + resetMoveBefore = logCall(Element.prototype, 'moveBefore'); resetRemoveText = logCall(Text.prototype, 'remove'); resetRemove = logCall(Element.prototype, 'remove'); }); afterAll(() => { resetAppendChild(); + resetMoveBefore(); resetInsertBefore(); resetRemoveText(); resetRemove();