Skip to content

Commit 1854e24

Browse files
authored
perf(jsx/dom): improve performance (#3288)
Fixes #3264 * fix(test): fix test name * refactor(jsx/dom): assign type and ref to node object directly. `Object.defineProperties` is too slow * fix(jsx/hooks): `useReducer` returns the same function object no matter how many times it is called. * refactor(jsx/dom): remove redundant property "s", "shadow virtual dom children" is not used * refactor(jsx/dom): use for-loop instead of recursion for findInsertBefore * perf(jsx/dom): improve performance of `getEventSpec` * perf(jsx/dom): improve performance of `toAttributeName` * perf(jsx/dom): for the same event handler, do nothing. * perf(jsx/dom): reduce `container.nodeName` access * perf(jsx/dom): remove `skipProps` and compare `key` with `'children'` directly * perf(jsx/dom): use `for-in` instead of `Object.entries` * perf(jsx/dom): if `defaultProps` does not exist, do not create a new `props` object. * perf(jsx/dom): if `callbacks` is empty, do not call `requestAnimationFrame` * perf(jsx/dom): optimize `build` function * perf(jsx/dom): skip `push` call if target list is empty. * perf(jsx/dom): optimize `apply` function * refactor(jsx/dom): remove unused type import * refactor(jsx/dom): remove redundant code `applyNode` * test(jsx/dom): add tests * refactor(jsx/dom): tweaks `delete node.vR` timing
1 parent e6459e7 commit 1854e24

File tree

9 files changed

+272
-156
lines changed

9 files changed

+272
-156
lines changed

src/jsx/dom/context.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Child } from '../base'
22
import { DOM_ERROR_HANDLER } from '../constants'
33
import type { Context } from '../context'
44
import { globalContexts } from '../context'
5-
import { newJSXNode, setInternalTagFlag } from './utils'
5+
import { setInternalTagFlag } from './utils'
66

77
export const createContextProviderFunction =
88
<T>(values: T[]): Function =>
@@ -33,7 +33,7 @@ export const createContextProviderFunction =
3333
}),
3434
props: {},
3535
})
36-
const res = newJSXNode({ tag: '', props })
36+
const res = { tag: '', props, type: '' }
3737
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3838
;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
3939
values.pop()

src/jsx/dom/index.test.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,16 @@ describe('DOM', () => {
736736
})
737737
})
738738

739+
describe('dangerouslySetInnerHTML', () => {
740+
it('string', () => {
741+
const App = () => {
742+
return <div dangerouslySetInnerHTML={{ __html: '<p>Hello</p>' }} />
743+
}
744+
render(<App />, root)
745+
expect(root.innerHTML).toBe('<div><p>Hello</p></div>')
746+
})
747+
})
748+
739749
describe('Event', () => {
740750
it('bubbling phase', async () => {
741751
const clicked: string[] = []
@@ -884,6 +894,13 @@ describe('DOM', () => {
884894
render(<App />, root)
885895
expect(addEventListenerSpy).not.toHaveBeenCalled()
886896
})
897+
898+
it('invalid event handler value', async () => {
899+
const App = () => {
900+
return <div onClick={1 as unknown as () => void}></div>
901+
}
902+
expect(() => render(<App />, root)).toThrow()
903+
})
887904
})
888905

889906
it('simple Counter', async () => {
@@ -1874,6 +1891,16 @@ describe('DOM', () => {
18741891
await Promise.resolve()
18751892
expect(root.innerHTML).toBe('<div><p>1</p></div>')
18761893
})
1894+
1895+
it('title', async () => {
1896+
const App = () => {
1897+
return <div>{createElement('title', {}, 'Hello')}</div>
1898+
}
1899+
const app = <App />
1900+
render(app, root)
1901+
expect(document.head.innerHTML).toBe('<title>Hello</title>')
1902+
expect(root.innerHTML).toBe('<div></div>')
1903+
})
18771904
})
18781905

18791906
describe('dom-specific createElement', () => {
@@ -1889,6 +1916,16 @@ describe('DOM', () => {
18891916
await Promise.resolve()
18901917
expect(root.innerHTML).toBe('<div><p>1</p></div>')
18911918
})
1919+
1920+
it('title', async () => {
1921+
const App = () => {
1922+
return <div>{createElementForDom('title', {}, 'Hello')}</div>
1923+
}
1924+
const app = <App />
1925+
render(app, root)
1926+
expect(document.head.innerHTML).toBe('<title>Hello</title>')
1927+
expect(root.innerHTML).toBe('<div></div>')
1928+
})
18921929
})
18931930

18941931
describe('cloneElement', () => {

src/jsx/dom/intrinsic-element/components.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ describe('intrinsic element', () => {
524524
)
525525
})
526526

527-
it('should be ordered by precedence attribute', () => {
527+
it('should ignore precedence attribute', () => {
528528
const App = () => {
529529
return (
530530
<div>

src/jsx/dom/intrinsic-element/components.ts

+35-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Props } from '../../base'
22
import type { FC, JSXNode, PropsWithChildren, RefObject } from '../../types'
3-
import { newJSXNode } from '../utils'
43
import { createPortal, getNameSpaceContext } from '../render'
54
import type { PreserveNodeType } from '../render'
65
import { useContext } from '../../context'
@@ -58,10 +57,12 @@ const documentMetadataTag = (
5857
supportBlocking: boolean
5958
) => {
6059
if (props?.itemProp) {
61-
return newJSXNode({
60+
return {
6261
tag,
6362
props,
64-
})
63+
type: tag,
64+
ref: props.ref,
65+
}
6566
}
6667

6768
const head = document.head
@@ -192,13 +193,15 @@ const documentMetadataTag = (
192193
}
193194
}
194195

195-
const jsxNode = newJSXNode({
196+
const jsxNode = {
196197
tag,
198+
type: tag,
197199
props: {
198200
...restProps,
199201
ref,
200202
},
201-
}) as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }
203+
ref,
204+
} as unknown as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }
202205

203206
jsxNode.p = preserveNodeType // preserve for unmounting
204207
if (element) {
@@ -215,30 +218,37 @@ export const title: FC<PropsWithChildren> = (props) => {
215218
const nameSpaceContext = getNameSpaceContext()
216219
const ns = nameSpaceContext && useContext(nameSpaceContext)
217220
if (ns?.endsWith('svg')) {
218-
return newJSXNode({
221+
return {
219222
tag: 'title',
220223
props,
221-
})
224+
type: 'title',
225+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
226+
ref: (props as any).ref,
227+
} as unknown as JSXNode
222228
}
223229
return documentMetadataTag('title', props, undefined, false, false)
224230
}
225231

226232
export const script: FC<PropsWithChildren<IntrinsicElements['script']>> = (props) => {
227233
if (!props || ['src', 'async'].some((k) => !props[k])) {
228-
return newJSXNode({
234+
return {
229235
tag: 'script',
230236
props,
231-
})
237+
type: 'script',
238+
ref: props.ref,
239+
} as unknown as JSXNode
232240
}
233241
return documentMetadataTag('script', props, 1, false, true)
234242
}
235243

236244
export const style: FC<PropsWithChildren<IntrinsicElements['style']>> = (props) => {
237245
if (!props || !['href', 'precedence'].every((k) => k in props)) {
238-
return newJSXNode({
246+
return {
239247
tag: 'style',
240248
props,
241-
})
249+
type: 'style',
250+
ref: props.ref,
251+
} as unknown as JSXNode
242252
}
243253
props['data-href'] = props.href
244254
delete props.href
@@ -251,10 +261,12 @@ export const link: FC<PropsWithChildren<IntrinsicElements['link']>> = (props) =>
251261
['onLoad', 'onError'].some((k) => k in props) ||
252262
(props.rel === 'stylesheet' && (!('precedence' in props) || 'disabled' in props))
253263
) {
254-
return newJSXNode({
264+
return {
255265
tag: 'link',
256266
props,
257-
})
267+
type: 'link',
268+
ref: props.ref,
269+
} as unknown as JSXNode
258270
}
259271
return documentMetadataTag('link', props, 1, 'precedence' in props, true)
260272
}
@@ -309,7 +321,7 @@ export const form: FC<
309321

310322
const [data, isDirty] = state
311323
state[1] = false
312-
return newJSXNode({
324+
return {
313325
tag: FormContext as unknown as Function,
314326
props: {
315327
value: {
@@ -318,17 +330,19 @@ export const form: FC<
318330
method: data ? 'post' : null,
319331
action: data ? action : null,
320332
},
321-
children: newJSXNode({
333+
children: {
322334
tag: 'form',
323335
props: {
324336
...restProps,
325337
ref,
326338
},
327-
}),
339+
type: 'form',
340+
ref,
341+
},
328342
},
329343
f: isDirty,
330344
// eslint-disable-next-line @typescript-eslint/no-explicit-any
331-
} as any) as any
345+
} as any
332346
}
333347

334348
const formActionableElement = (
@@ -357,11 +371,13 @@ const formActionableElement = (
357371
})
358372
}
359373

360-
return newJSXNode({
374+
return {
361375
tag,
362376
props,
377+
type: tag,
378+
ref: props.ref,
363379
// eslint-disable-next-line @typescript-eslint/no-explicit-any
364-
}) as any
380+
} as any
365381
}
366382

367383
export const input: FC<PropsWithChildren<IntrinsicElements['input']>> = (props) =>

src/jsx/dom/jsx-dev-runtime.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@
44
*/
55

66
import type { JSXNode, Props } from '../base'
7-
import { newJSXNode } from './utils'
87
import * as intrinsicElementTags from './intrinsic-element/components'
98

109
export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => {
11-
return newJSXNode({
12-
tag:
13-
(typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) ||
14-
tag,
10+
if (typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) {
11+
tag = intrinsicElementTags[tag as keyof typeof intrinsicElementTags]
12+
}
13+
return {
14+
tag,
15+
type: tag,
1516
props,
1617
key,
17-
})
18+
ref: props.ref,
19+
} as JSXNode
1820
}
1921

2022
export const Fragment = (props: Record<string, unknown>): JSXNode => jsxDEV('', props, undefined)

0 commit comments

Comments
 (0)