Skip to content

Commit 66a4661

Browse files
authoredJan 8, 2025··
perf: isolate plugin logic (#451)
* feat: vanilla function resolves Fixes #435 * chore: broken test * perf: isolate plugin logic
1 parent b2ed420 commit 66a4661

File tree

17 files changed

+133
-91
lines changed

17 files changed

+133
-91
lines changed
 

‎packages/addons/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@
5353
"stub": "unbuild . --stub",
5454
"export:sizes": "npx export-size . -r"
5555
},
56+
"peerDependencies": {
57+
"unhead": "workspace:*"
58+
},
5659
"dependencies": {
5760
"@rollup/pluginutils": "^5.1.4",
5861
"@unhead/schema": "workspace:*",
@@ -64,9 +67,6 @@
6467
"unplugin": "^2.1.2",
6568
"unplugin-ast": "^0.13.1"
6669
},
67-
"peerDependencies": {
68-
"unhead": "workspace:*"
69-
},
7070
"devDependencies": {
7171
"@babel/types": "^7.26.3"
7272
}

‎packages/shared/src/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export const SelfClosingTags = new Set(['meta', 'link', 'base'])
2+
export const DupeableTags = new Set(['link', 'style', 'script', 'noscript'])
23
export const TagsWithInnerContent = new Set(['title', 'titleTemplate', 'script', 'style', 'noscript'])
34
export const HasElementTags = new Set([
45
'base',
@@ -24,7 +25,7 @@ export const ValidHeadTags = new Set([
2425

2526
export const UniqueTags = new Set(['base', 'title', 'titleTemplate', 'bodyAttrs', 'htmlAttrs', 'templateParams'])
2627

27-
export const TagConfigKeys = new Set(['tagPosition', 'tagPriority', 'tagDuplicateStrategy', 'innerHTML', 'textContent', 'processTemplateParams'])
28+
export const TagConfigKeys = new Set(['key', 'tagPosition', 'tagPriority', 'tagDuplicateStrategy', 'innerHTML', 'textContent', 'processTemplateParams'])
2829

2930
export const IsBrowser = typeof window !== 'undefined'
3031

‎packages/shared/src/normalise.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Head, HeadEntry, HeadTag } from '@unhead/schema'
2-
import { TagConfigKeys, TagsWithInnerContent, ValidHeadTags } from './constants'
2+
import { DupeableTags, TagConfigKeys, TagsWithInnerContent, ValidHeadTags } from './constants'
3+
import { hashCode } from './hashCode'
4+
import { tagDedupeKey } from './tagDedupeKey'
35

46
export function normaliseTag<T extends HeadTag>(tagName: T['tag'], input: HeadTag['props'] | string, e: HeadEntry<T>, normalizedProps?: HeadTag['props']): T | T[] {
57
const props = normalizedProps || normaliseProps<T>(
@@ -27,6 +29,19 @@ export function normaliseTag<T extends HeadTag>(tagName: T['tag'], input: HeadTa
2729
delete tag.props[k]
2830
}
2931
}
32+
// only if the user has provided a key
33+
// only tags which can't dedupe themselves, ssr only
34+
if (tag.key && DupeableTags.has(tag.tag)) {
35+
// add a HTML key so the client-side can hydrate without causing duplicates
36+
tag.props['data-hid'] = tag._h = hashCode(tag.key!)
37+
}
38+
const generatedKey = tagDedupeKey(tag)
39+
if (generatedKey && !generatedKey.startsWith('meta:og:') && !generatedKey.startsWith('meta:twitter:')) {
40+
delete tag.key
41+
}
42+
const dedupe = generatedKey || (tag.key ? `${tag.tag}:${tag.key}` : false)
43+
if (dedupe)
44+
tag._d = dedupe
3045
// shorthand for objects
3146
if (tag.tag === 'script') {
3247
if (typeof tag.innerHTML === 'object') {

‎packages/unhead/src/client/createHead.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,19 @@ import type { CreateHeadOptions, Head } from '@unhead/schema'
22
import { IsBrowser } from '@unhead/shared'
33
import { unheadCtx } from '../context'
44
import { createHeadCore } from '../createHead'
5-
import { DomPlugin } from './domPlugin'
5+
import { DomPlugin } from './plugins/domPlugin'
6+
import { ClientEventHandlerPlugin } from './plugins/eventHandlers'
67

78
export function createHead<T extends Record<string, any> = Head>(options: CreateHeadOptions = {}) {
89
const head = createHeadCore<T>({
910
document: (IsBrowser ? document : undefined),
1011
...options,
12+
plugins: [
13+
...(options.plugins || []),
14+
DomPlugin(),
15+
ClientEventHandlerPlugin,
16+
],
1117
})
12-
head.use(DomPlugin())
13-
// should only be one instance client-side
14-
if (!head.ssr && IsBrowser) {
15-
unheadCtx.set(head, true)
16-
}
18+
unheadCtx.set(head, true)
1719
return head
1820
}

‎packages/unhead/src/client/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export * from './createHead'
22
export * from './debounced'
3-
export * from './domPlugin'
3+
export * from './plugins/domPlugin'
44
export * from './renderDOMHead'

‎packages/unhead/src/client/domPlugin.ts ‎packages/unhead/src/client/plugins/domPlugin.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { RenderDomHeadOptions } from './renderDOMHead'
1+
import type { RenderDomHeadOptions } from '../renderDOMHead'
22
import { defineHeadPlugin } from '@unhead/shared'
3-
import { debouncedRenderDOMHead } from './debounced'
3+
import { debouncedRenderDOMHead } from '../debounced'
44

55
export interface DomPluginOptions extends RenderDomHeadOptions {
66
delayFn?: (fn: () => void) => void

‎packages/unhead/src/plugins/eventHandlers.ts ‎packages/unhead/src/client/plugins/eventHandlers.ts

+4-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineHeadPlugin, hashCode, NetworkEvents } from '@unhead/shared'
1+
import { defineHeadPlugin, NetworkEvents } from '@unhead/shared'
22

33
const ValidEventTags = new Set(['script', 'link', 'bodyAttrs'])
44

@@ -7,7 +7,7 @@ const ValidEventTags = new Set(['script', 'link', 'bodyAttrs'])
77
*
88
* When SSR we need to strip out these values. On CSR we
99
*/
10-
export default defineHeadPlugin(head => ({
10+
export const ClientEventHandlerPlugin = defineHeadPlugin({
1111
hooks: {
1212
'tags:resolve': (ctx) => {
1313
for (const tag of ctx.tags) {
@@ -33,21 +33,10 @@ export default defineHeadPlugin(head => ({
3333
continue
3434
}
3535

36-
// insert a inline script to set the status of onload and onerror
37-
if (head.ssr && NetworkEvents.has(key)) {
38-
props[key] = `this.dataset.${key}fired = true`
39-
}
40-
else {
41-
delete props[key]
42-
}
43-
36+
delete props[key]
4437
tag._eventHandlers = tag._eventHandlers || {}
4538
tag._eventHandlers![key] = value
4639
}
47-
48-
if (head.ssr && tag._eventHandlers && (tag.props.src || tag.props.href)) {
49-
tag.key = tag.key || hashCode(tag.props.src || tag.props.href)
50-
}
5140
}
5241
},
5342
'dom:renderTag': ({ $el, tag }) => {
@@ -75,4 +64,4 @@ export default defineHeadPlugin(head => ({
7564
}
7665
},
7766
},
78-
}))
67+
})

‎packages/unhead/src/createHead.ts

-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import type {
1212
import { normaliseEntryTags } from '@unhead/shared'
1313
import { createHooks } from 'hookable'
1414
import DedupePlugin from './plugins/dedupe'
15-
import EventHandlersPlugin from './plugins/eventHandlers'
16-
import HashKeyedPlugin from './plugins/hashKeyed'
1715
import SortPlugin from './plugins/sort'
1816
import TemplateParamsPlugin from './plugins/templateParams'
1917
import TitleTemplatePlugin from './plugins/titleTemplate'
@@ -114,8 +112,6 @@ export function createHeadCore<T extends Record<string, any> = Head>(options: Cr
114112
}
115113
;[
116114
DedupePlugin,
117-
EventHandlersPlugin,
118-
HashKeyedPlugin,
119115
SortPlugin,
120116
TemplateParamsPlugin,
121117
TitleTemplatePlugin,

‎packages/unhead/src/legacy.ts

+25-10
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
import type { CreateHeadOptions, Head } from '@unhead/schema'
22
import { IsBrowser } from '@unhead/shared'
3-
import { DomPlugin } from './client/domPlugin'
3+
import { DomPlugin } from './client/plugins/domPlugin'
4+
import { ClientEventHandlerPlugin } from './client/plugins/eventHandlers'
45
import { unheadCtx } from './context'
56
import { createHeadCore } from './createHead'
67
import { DeprecationsPlugin } from './optionalPlugins/deprecations'
78
import { PromisesPlugin } from './optionalPlugins/promises'
9+
import { ServerEventHandlerPlugin } from './server/plugins/eventHandlers'
10+
import { PayloadPlugin } from './server/plugins/payload'
811

912
export function createServerHead<T extends Record<string, any> = Head>(options: CreateHeadOptions = {}) {
10-
// @ts-expect-error untyped
11-
const head = createHeadCore<T>({ disableCapoSorting: true, ...options, document: false })
12-
head.use(DeprecationsPlugin)
13-
head.use(PromisesPlugin)
14-
return head
13+
return createHeadCore<T>({
14+
disableCapoSorting: true,
15+
...options,
16+
// @ts-expect-error untyped
17+
document: false,
18+
plugins: [
19+
...(options.plugins || []),
20+
DomPlugin(),
21+
DeprecationsPlugin,
22+
PromisesPlugin,
23+
ServerEventHandlerPlugin,
24+
PayloadPlugin,
25+
],
26+
})
1527
}
1628

1729
export function createHead<T extends Record<string, any> = Head>(options: CreateHeadOptions = {}) {
18-
const head = createHeadCore<T>({ disableCapoSorting: true, ...options })
19-
head.use(DomPlugin())
20-
head.use(DeprecationsPlugin)
21-
head.use(PromisesPlugin)
30+
const head = createHeadCore<T>({ disableCapoSorting: true, ...options, plugins: [
31+
...(options.plugins || []),
32+
DomPlugin(),
33+
DeprecationsPlugin,
34+
PromisesPlugin,
35+
ClientEventHandlerPlugin,
36+
] })
2237
// should only be one instance client-side
2338
if (!head.ssr && IsBrowser) {
2439
unheadCtx.set(head, true)

‎packages/unhead/src/plugins/dedupe.ts

+1-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
11
import type { HeadTag } from '@unhead/schema'
2-
import { defineHeadPlugin, HasElementTags, hashTag, tagDedupeKey, tagWeight } from '@unhead/shared'
2+
import { defineHeadPlugin, HasElementTags, hashTag, tagWeight } from '@unhead/shared'
33

44
const UsesMergeStrategy = new Set(['templateParams', 'htmlAttrs', 'bodyAttrs'])
55

66
export default defineHeadPlugin(head => ({
77
hooks: {
8-
'tag:normalise': ({ tag }) => {
9-
if (tag.props.key) {
10-
tag.key = tag.props.key
11-
delete tag.props.key
12-
}
13-
const generatedKey = tagDedupeKey(tag)
14-
if (generatedKey && !generatedKey.startsWith('meta:og:') && !generatedKey.startsWith('meta:twitter:')) {
15-
delete tag.key
16-
}
17-
const dedupe = generatedKey || (tag.key ? `${tag.tag}:${tag.key}` : false)
18-
if (dedupe)
19-
tag._d = dedupe
20-
},
218
'tags:resolve': (ctx) => {
229
// 1. Dedupe tags
2310
const deduping: Record<string, HeadTag> = Object.create(null)

‎packages/unhead/src/plugins/hashKeyed.ts

-16
This file was deleted.

‎packages/unhead/src/plugins/sort.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,8 @@ export default defineHeadPlugin(head => ({
3333
const bWeight = tagWeight(head, b)
3434

3535
// 2c. sort based on critical tags
36-
if (aWeight < bWeight) {
37-
return -1
38-
}
39-
else if (aWeight > bWeight) {
40-
return 1
36+
if (aWeight !== bWeight) {
37+
return aWeight - bWeight
4138
}
4239

4340
// 2b. sort tags in their natural order
+12-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import type { CreateHeadOptions, Head } from '@unhead/schema'
22
import { createHeadCore } from '../createHead'
3-
import PayloadPlugin from './plugins/payload'
3+
import { ServerEventHandlerPlugin } from './plugins/eventHandlers'
4+
import { PayloadPlugin } from './plugins/payload'
45

56
export function createHead<T extends Record<string, any> = Head>(options: CreateHeadOptions = {}) {
6-
// @ts-expect-error untyped
7-
const head = createHeadCore<T>({ ...options, document: false })
8-
head.use(PayloadPlugin)
9-
return head
7+
return createHeadCore<T>({
8+
...options,
9+
// @ts-expect-error untyped
10+
document: false,
11+
plugins: [
12+
...(options.plugins || []),
13+
PayloadPlugin,
14+
ServerEventHandlerPlugin,
15+
],
16+
})
1017
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { defineHeadPlugin, hashCode, NetworkEvents } from '@unhead/shared'
2+
3+
const ValidEventTags = new Set(['script', 'link', 'bodyAttrs'])
4+
5+
/**
6+
* Supports DOM event handlers (i.e `onload`) as functions.
7+
*
8+
* When SSR we need to strip out these values. On CSR we
9+
*/
10+
export const ServerEventHandlerPlugin = defineHeadPlugin({
11+
hooks: {
12+
'tags:resolve': (ctx) => {
13+
for (const tag of ctx.tags) {
14+
if (!ValidEventTags.has(tag.tag)) {
15+
continue
16+
}
17+
18+
const props = tag.props
19+
20+
let hasEventHandlers = false
21+
for (const key in props) {
22+
// on
23+
if (key[0] !== 'o' || key[1] !== 'n') {
24+
continue
25+
}
26+
27+
if (!Object.prototype.hasOwnProperty.call(props, key)) {
28+
continue
29+
}
30+
31+
const value = props[key]
32+
33+
if (typeof value !== 'function') {
34+
continue
35+
}
36+
37+
// insert a inline script to set the status of onload and onerror
38+
if (NetworkEvents.has(key)) {
39+
props[key] = `this.dataset.${key}fired = true`
40+
hasEventHandlers = true
41+
}
42+
}
43+
44+
if (hasEventHandlers && (tag.props.src || tag.props.href)) {
45+
tag.key = tag.key || hashCode(tag.props.src || tag.props.href)
46+
}
47+
}
48+
},
49+
},
50+
})

‎packages/unhead/src/server/plugins/payload.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { defineHeadPlugin } from '@unhead/shared'
22

3-
export default defineHeadPlugin({
4-
mode: 'server',
3+
export const PayloadPlugin = defineHeadPlugin({
54
hooks: {
65
'tags:beforeResolve': (ctx) => {
76
const payload: { titleTemplate?: string | ((s: string) => string), templateParams?: Record<string, string>, title?: string } = {}

‎test/unhead/ssr/eventHandlers.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { renderSSRHead } from '@unhead/ssr'
22
import { useHead } from 'unhead'
33
import { describe, it } from 'vitest'
4-
import { createHeadWithContext } from '../../util'
4+
import { createServerHeadWithContext } from '../../util'
55

66
describe('ssr event handlers', () => {
77
it('basic', async () => {
8-
const head = createHeadWithContext()
8+
const head = createServerHeadWithContext()
99

1010
useHead({
1111
script: [

‎test/unhead/ssr/useScript.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { renderSSRHead } from '@unhead/ssr'
22
import { useScript } from 'unhead'
33
import { describe, it } from 'vitest'
4-
import { createHeadWithContext } from '../../util'
4+
import { createHeadWithContext, createServerHeadWithContext } from '../../util'
55

66
describe('ssr useScript', () => {
77
it('default', async () => {
@@ -23,7 +23,7 @@ describe('ssr useScript', () => {
2323
`)
2424
})
2525
it('server', async () => {
26-
const head = createHeadWithContext()
26+
const head = createServerHeadWithContext()
2727

2828
useScript({
2929
src: 'https://cdn.example.com/script.js',
@@ -43,7 +43,7 @@ describe('ssr useScript', () => {
4343
`)
4444
})
4545
it('await ', async () => {
46-
const head = createHeadWithContext()
46+
const head = createServerHeadWithContext()
4747

4848
// mock a promise, test that it isn't resolved in 1 second
4949
useScript<{ foo: 'bar' }>({
@@ -64,7 +64,7 @@ describe('ssr useScript', () => {
6464
`)
6565
})
6666
it('google ', async () => {
67-
const head = createHeadWithContext()
67+
const head = createServerHeadWithContext()
6868

6969
const gtag = useScript<{ dataLayer: any[] }>({
7070
src: 'https://www.googletagmanager.com/gtm.js?id=GTM-MNJD4B',

0 commit comments

Comments
 (0)
Please sign in to comment.