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

perf(jsx/dom): improve performance #3288

Merged
merged 21 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a32bfb1
fix(test): fix test name
usualoma Aug 17, 2024
7fd73ef
refactor(jsx/dom): assign type and ref to node object directly. `Obje…
usualoma Aug 17, 2024
03178cb
fix(jsx/hooks): `useReducer` returns the same function object no matt…
usualoma Aug 17, 2024
967c2e6
refactor(jsx/dom): remove redundant property "s", "shadow virtual dom…
usualoma Aug 17, 2024
0dadfbc
refactor(jsx/dom): use for-loop instead of recursion for findInsertBe…
usualoma Aug 17, 2024
424934d
perf(jsx/dom): improve performance of `getEventSpec`
usualoma Aug 17, 2024
bd832d9
perf(jsx/dom): improve performance of `toAttributeName`
usualoma Aug 17, 2024
b140bd6
perf(jsx/dom): for the same event handler, do nothing.
usualoma Aug 17, 2024
bc3f6cb
perf(jsx/dom): reduce `container.nodeName` access
usualoma Aug 17, 2024
541d0a7
perf(jsx/dom): remove `skipProps` and compare `key` with `'children'`…
usualoma Aug 17, 2024
6e7d7b6
perf(jsx/dom): use `for-in` instead of `Object.entries`
usualoma Aug 17, 2024
7b4031e
perf(jsx/dom): if `defaultProps` does not exist, do not create a new …
usualoma Aug 17, 2024
318eec7
perf(jsx/dom): if `callbacks` is empty, do not call `requestAnimation…
usualoma Aug 17, 2024
d0014bd
perf(jsx/dom): optimize `build` function
usualoma Aug 17, 2024
740cbc2
perf(jsx/dom): skip `push` call if target list is empty.
usualoma Aug 17, 2024
5ccda77
perf(jsx/dom): optimize `apply` function
usualoma Aug 17, 2024
915a823
refactor(jsx/dom): remove unused type import
usualoma Aug 18, 2024
bb5ad26
refactor(jsx/dom): remove redundant code `applyNode`
usualoma Aug 18, 2024
32c4b02
test(jsx/dom): add tests
usualoma Aug 18, 2024
627fe66
refactor(jsx/dom): tweaks `delete node.vR` timing
usualoma Aug 18, 2024
9d6e16e
Merge branch 'main' into perf/jsx-dom
usualoma Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/jsx/dom/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Child } from '../base'
import { DOM_ERROR_HANDLER } from '../constants'
import type { Context } from '../context'
import { globalContexts } from '../context'
import { newJSXNode, setInternalTagFlag } from './utils'
import { setInternalTagFlag } from './utils'

export const createContextProviderFunction =
<T>(values: T[]): Function =>
Expand Down Expand Up @@ -33,7 +33,7 @@ export const createContextProviderFunction =
}),
props: {},
})
const res = newJSXNode({ tag: '', props })
const res = { tag: '', props, type: '' }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
values.pop()
Expand Down
37 changes: 37 additions & 0 deletions src/jsx/dom/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,16 @@ describe('DOM', () => {
})
})

describe('dangerouslySetInnerHTML', () => {
it('string', () => {
const App = () => {
return <div dangerouslySetInnerHTML={{ __html: '<p>Hello</p>' }} />
}
render(<App />, root)
expect(root.innerHTML).toBe('<div><p>Hello</p></div>')
})
})

describe('Event', () => {
it('bubbling phase', async () => {
const clicked: string[] = []
Expand Down Expand Up @@ -884,6 +894,13 @@ describe('DOM', () => {
render(<App />, root)
expect(addEventListenerSpy).not.toHaveBeenCalled()
})

it('invalid event handler value', async () => {
const App = () => {
return <div onClick={1 as unknown as () => void}></div>
}
expect(() => render(<App />, root)).toThrow()
})
})

it('simple Counter', async () => {
Expand Down Expand Up @@ -1874,6 +1891,16 @@ describe('DOM', () => {
await Promise.resolve()
expect(root.innerHTML).toBe('<div><p>1</p></div>')
})

it('title', async () => {
const App = () => {
return <div>{createElement('title', {}, 'Hello')}</div>
}
const app = <App />
render(app, root)
expect(document.head.innerHTML).toBe('<title>Hello</title>')
expect(root.innerHTML).toBe('<div></div>')
})
})

describe('dom-specific createElement', () => {
Expand All @@ -1889,6 +1916,16 @@ describe('DOM', () => {
await Promise.resolve()
expect(root.innerHTML).toBe('<div><p>1</p></div>')
})

it('title', async () => {
const App = () => {
return <div>{createElementForDom('title', {}, 'Hello')}</div>
}
const app = <App />
render(app, root)
expect(document.head.innerHTML).toBe('<title>Hello</title>')
expect(root.innerHTML).toBe('<div></div>')
})
})

describe('cloneElement', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/jsx/dom/intrinsic-element/components.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ describe('intrinsic element', () => {
)
})

it('should be ordered by precedence attribute', () => {
it('should ignore precedence attribute', () => {
const App = () => {
return (
<div>
Expand Down
54 changes: 35 additions & 19 deletions src/jsx/dom/intrinsic-element/components.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Props } from '../../base'
import type { FC, JSXNode, PropsWithChildren, RefObject } from '../../types'
import { newJSXNode } from '../utils'
import { createPortal, getNameSpaceContext } from '../render'
import type { PreserveNodeType } from '../render'
import { useContext } from '../../context'
Expand Down Expand Up @@ -58,10 +57,12 @@ const documentMetadataTag = (
supportBlocking: boolean
) => {
if (props?.itemProp) {
return newJSXNode({
return {
tag,
props,
})
type: tag,
ref: props.ref,
}
}

const head = document.head
Expand Down Expand Up @@ -192,13 +193,15 @@ const documentMetadataTag = (
}
}

const jsxNode = newJSXNode({
const jsxNode = {
tag,
type: tag,
props: {
...restProps,
ref,
},
}) as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }
ref,
} as unknown as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }

jsxNode.p = preserveNodeType // preserve for unmounting
if (element) {
Expand All @@ -215,30 +218,37 @@ export const title: FC<PropsWithChildren> = (props) => {
const nameSpaceContext = getNameSpaceContext()
const ns = nameSpaceContext && useContext(nameSpaceContext)
if (ns?.endsWith('svg')) {
return newJSXNode({
return {
tag: 'title',
props,
})
type: 'title',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref: (props as any).ref,
} as unknown as JSXNode
}
return documentMetadataTag('title', props, undefined, false, false)
}

export const script: FC<PropsWithChildren<IntrinsicElements['script']>> = (props) => {
if (!props || ['src', 'async'].some((k) => !props[k])) {
return newJSXNode({
return {
tag: 'script',
props,
})
type: 'script',
ref: props.ref,
} as unknown as JSXNode
}
return documentMetadataTag('script', props, 1, false, true)
}

export const style: FC<PropsWithChildren<IntrinsicElements['style']>> = (props) => {
if (!props || !['href', 'precedence'].every((k) => k in props)) {
return newJSXNode({
return {
tag: 'style',
props,
})
type: 'style',
ref: props.ref,
} as unknown as JSXNode
}
props['data-href'] = props.href
delete props.href
Expand All @@ -251,10 +261,12 @@ export const link: FC<PropsWithChildren<IntrinsicElements['link']>> = (props) =>
['onLoad', 'onError'].some((k) => k in props) ||
(props.rel === 'stylesheet' && (!('precedence' in props) || 'disabled' in props))
) {
return newJSXNode({
return {
tag: 'link',
props,
})
type: 'link',
ref: props.ref,
} as unknown as JSXNode
}
return documentMetadataTag('link', props, 1, 'precedence' in props, true)
}
Expand Down Expand Up @@ -309,7 +321,7 @@ export const form: FC<

const [data, isDirty] = state
state[1] = false
return newJSXNode({
return {
tag: FormContext as unknown as Function,
props: {
value: {
Expand All @@ -318,17 +330,19 @@ export const form: FC<
method: data ? 'post' : null,
action: data ? action : null,
},
children: newJSXNode({
children: {
tag: 'form',
props: {
...restProps,
ref,
},
}),
type: 'form',
ref,
},
},
f: isDirty,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as any
} as any
}

const formActionableElement = (
Expand Down Expand Up @@ -357,11 +371,13 @@ const formActionableElement = (
})
}

return newJSXNode({
return {
tag,
props,
type: tag,
ref: props.ref,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
} as any
}

export const input: FC<PropsWithChildren<IntrinsicElements['input']>> = (props) =>
Expand Down
14 changes: 8 additions & 6 deletions src/jsx/dom/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
*/

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

export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => {
return newJSXNode({
tag:
(typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) ||
tag,
if (typeof tag === 'string' && intrinsicElementTags[tag as keyof typeof intrinsicElementTags]) {
tag = intrinsicElementTags[tag as keyof typeof intrinsicElementTags]
}
return {
tag,
type: tag,
props,
key,
})
ref: props.ref,
} as JSXNode
}

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