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(hydration): supports tree shaking of code in dev environments. #10326

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
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
228 changes: 119 additions & 109 deletions packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ export function createHydrationFunctions(
// check hydration mismatch
if (
__DEV__ &&
propHasMismatch(el, key, props[key], vnode, parentComponent)
propHasMismatch?.(el, key, props[key], vnode, parentComponent)
) {
hasMismatch = true
}
Expand Down Expand Up @@ -716,139 +716,149 @@ export function createHydrationFunctions(
/**
* Dev only
*/
function propHasMismatch(
let propHasMismatch: (
el: Element,
key: string,
clientValue: any,
vnode: VNode,
instance: ComponentInternalInstance | null,
): boolean {
let mismatchType: string | undefined
let mismatchKey: string | undefined
let actual: any
let expected: any
if (key === 'class') {
// classes might be in different order, but that doesn't affect cascade
// so we just need to check if the class lists contain the same classes.
actual = el.getAttribute('class')
expected = normalizeClass(clientValue)
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
mismatchType = mismatchKey = `class`
}
} else if (key === 'style') {
// style might be in different order, but that doesn't affect cascade
actual = el.getAttribute('style')
expected = isString(clientValue)
? clientValue
: stringifyStyle(normalizeStyle(clientValue))
const actualMap = toStyleMap(actual)
const expectedMap = toStyleMap(expected)
// If `v-show=false`, `display: 'none'` should be added to expected
if (vnode.dirs) {
for (const { dir, value } of vnode.dirs) {
// @ts-expect-error only vShow has this internal name
if (dir.name === 'show' && !value) {
expectedMap.set('display', 'none')
) => boolean
if (__DEV__) {
propHasMismatch = function (
el: Element,
key: string,
clientValue: any,
vnode: VNode,
instance: ComponentInternalInstance | null,
): boolean {
let mismatchType: string | undefined
let mismatchKey: string | undefined
let actual: any
let expected: any
if (key === 'class') {
// classes might be in different order, but that doesn't affect cascade
// so we just need to check if the class lists contain the same classes.
actual = el.getAttribute('class')
expected = normalizeClass(clientValue)
if (!isSetEqual(toClassSet(actual || ''), toClassSet(expected))) {
mismatchType = mismatchKey = `class`
}
} else if (key === 'style') {
// style might be in different order, but that doesn't affect cascade
actual = el.getAttribute('style')
expected = isString(clientValue)
? clientValue
: stringifyStyle(normalizeStyle(clientValue))
const actualMap = toStyleMap(actual)
const expectedMap = toStyleMap(expected)
// If `v-show=false`, `display: 'none'` should be added to expected
if (vnode.dirs) {
for (const { dir, value } of vnode.dirs) {
// @ts-expect-error only vShow has this internal name
if (dir.name === 'show' && !value) {
expectedMap.set('display', 'none')
}
}
}
}

const cssVars = instance?.getCssVars?.()
for (const key in cssVars) {
expectedMap.set(`--${key}`, String(cssVars[key]))
}
const cssVars = instance?.getCssVars?.()
for (const key in cssVars) {
expectedMap.set(`--${key}`, String(cssVars[key]))
}

if (!isMapEqual(actualMap, expectedMap)) {
mismatchType = mismatchKey = 'style'
}
} else if (
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
(el instanceof HTMLElement && (isBooleanAttr(key) || isKnownHtmlAttr(key)))
) {
if (isBooleanAttr(key)) {
actual = el.hasAttribute(key)
expected = includeBooleanAttr(clientValue)
} else if (clientValue == null) {
actual = el.hasAttribute(key)
expected = false
} else {
if (el.hasAttribute(key)) {
actual = el.getAttribute(key)
} else if (key === 'value' && el.tagName === 'TEXTAREA') {
// #10000 textarea.value can't be retrieved by `hasAttribute`
actual = (el as HTMLTextAreaElement).value
if (!isMapEqual(actualMap, expectedMap)) {
mismatchType = mismatchKey = 'style'
}
} else if (
(el instanceof SVGElement && isKnownSvgAttr(key)) ||
(el instanceof HTMLElement &&
(isBooleanAttr(key) || isKnownHtmlAttr(key)))
) {
if (isBooleanAttr(key)) {
actual = el.hasAttribute(key)
expected = includeBooleanAttr(clientValue)
} else if (clientValue == null) {
actual = el.hasAttribute(key)
expected = false
} else {
actual = false
if (el.hasAttribute(key)) {
actual = el.getAttribute(key)
} else if (key === 'value' && el.tagName === 'TEXTAREA') {
// #10000 textarea.value can't be retrieved by `hasAttribute`
actual = (el as HTMLTextAreaElement).value
} else {
actual = false
}
expected = isRenderableAttrValue(clientValue)
? String(clientValue)
: false
}
if (actual !== expected) {
mismatchType = `attribute`
mismatchKey = key
}
expected = isRenderableAttrValue(clientValue)
? String(clientValue)
: false
}
if (actual !== expected) {
mismatchType = `attribute`
mismatchKey = key
}
}

if (mismatchType) {
const format = (v: any) =>
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
const preSegment = `Hydration ${mismatchType} mismatch on`
const postSegment =
`\n - rendered on server: ${format(actual)}` +
`\n - expected on client: ${format(expected)}` +
`\n Note: this mismatch is check-only. The DOM will not be rectified ` +
`in production due to performance overhead.` +
`\n You should fix the source of the mismatch.`
if (__TEST__) {
// during tests, log the full message in one single string for easier
// debugging.
warn(`${preSegment} ${el.tagName}${postSegment}`)
} else {
warn(preSegment, el, postSegment)
if (mismatchType) {
const format = (v: any) =>
v === false ? `(not rendered)` : `${mismatchKey}="${v}"`
const preSegment = `Hydration ${mismatchType} mismatch on`
const postSegment =
`\n - rendered on server: ${format(actual)}` +
`\n - expected on client: ${format(expected)}` +
`\n Note: this mismatch is check-only. The DOM will not be rectified ` +
`in production due to performance overhead.` +
`\n You should fix the source of the mismatch.`
if (__TEST__) {
// during tests, log the full message in one single string for easier
// debugging.
warn(`${preSegment} ${el.tagName}${postSegment}`)
} else {
warn(preSegment, el, postSegment)
}
return true
}
return true
return false
}
return false
}

function toClassSet(str: string): Set<string> {
return new Set(str.trim().split(/\s+/))
}

function isSetEqual(a: Set<string>, b: Set<string>): boolean {
if (a.size !== b.size) {
return false
function toClassSet(str: string): Set<string> {
return new Set(str.trim().split(/\s+/))
}
for (const s of a) {
if (!b.has(s)) {

function isSetEqual(a: Set<string>, b: Set<string>): boolean {
if (a.size !== b.size) {
return false
}
for (const s of a) {
if (!b.has(s)) {
return false
}
}
return true
}
return true
}

function toStyleMap(str: string): Map<string, string> {
const styleMap: Map<string, string> = new Map()
for (const item of str.split(';')) {
let [key, value] = item.split(':')
key = key?.trim()
value = value?.trim()
if (key && value) {
styleMap.set(key, value)
function toStyleMap(str: string): Map<string, string> {
const styleMap: Map<string, string> = new Map()
for (const item of str.split(';')) {
let [key, value] = item.split(':')
key = key?.trim()
value = value?.trim()
if (key && value) {
styleMap.set(key, value)
}
}
return styleMap
}
return styleMap
}

function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
if (a.size !== b.size) {
return false
}
for (const [key, value] of a) {
if (value !== b.get(key)) {
function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
if (a.size !== b.size) {
return false
}
for (const [key, value] of a) {
if (value !== b.get(key)) {
return false
}
}
return true
}
return true
}
Loading