chore: drop URL and SVG list exceptions from the hydration attribute skip

Skipping the write keeps list items retrieved through `baseVal` attached and avoids another request, as the `src`/`srcset` early return already does, so these attributes no longer need to be written. The remaining check only depends on the element, and autonomous custom elements never reach it.
pull/18869/head
xyrolle 12 hours ago
parent 25857448bf
commit 88445e8270

@ -46,33 +46,6 @@ const OPTION_TAG = IS_XHTML ? 'option' : 'OPTION';
const SELECT_TAG = IS_XHTML ? 'select' : 'SELECT';
const PROGRESS_TAG = IS_XHTML ? 'progress' : 'PROGRESS';
/** Writing one of these again can refetch its resource, e.g. `<image href>` fires another `load` */
const URL_ATTRIBUTES = [
'href',
'xlink:href',
'background',
'classid',
'codebase',
'formaction',
'itemid',
'longdesc',
'manifest',
'usemap'
];
/** Writing an SVG list again detaches the items retrieved through `baseVal` */
const SVG_LIST_ATTRIBUTES = [
'values',
'tableValues',
'kernelMatrix',
'transform',
'gradientTransform',
'patternTransform',
'points'
];
/** Lists on `<text>` and `<tspan>` only */
const SVG_TEXT_POSITION_ATTRIBUTES = ['x', 'y', 'dx', 'dy', 'rotate'];
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
* to remove it upon hydration to avoid a bug when someone resets the form value.
@ -224,7 +197,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
hydrating &&
(typeof value === 'number' || typeof value === 'boolean') &&
previous === String(value) &&
is_inert_write(element, attribute)
is_native_element(element)
)
) {
element.setAttribute(attribute, value);
@ -241,28 +214,18 @@ var get_namespace_uri;
var get_local_name;
/**
* Whether writing an attribute's current value again only produces a mutation record. A custom
* element observes it, so an HTML element needs the prototype of a native one. The platform getters
* are used because form controls and own properties can shadow `localName` and `namespaceURI`
* Whether the element is not a customized built-in, which would observe writing an attribute's
* current value again through `attributeChangedCallback`. Autonomous custom elements never get here,
* as they are written with hydration turned off. The platform getters are used because form
* controls and own properties can shadow `localName` and `namespaceURI`
* @param {Element} element
* @param {string} attribute
*/
function is_inert_write(element, attribute) {
if (URL_ATTRIBUTES.includes(attribute)) return false;
function is_native_element(element) {
get_namespace_uri ??= /** @type {any} */ (get_descriptor(Element.prototype, 'namespaceURI')).get;
if (get_namespace_uri.call(element) !== NAMESPACE_HTML) return true;
get_local_name ??= /** @type {any} */ (get_descriptor(Element.prototype, 'localName')).get;
var name = get_local_name.call(element);
if (get_namespace_uri.call(element) !== NAMESPACE_HTML) {
return !(
SVG_LIST_ATTRIBUTES.includes(attribute) ||
((name === 'text' || name === 'tspan') && SVG_TEXT_POSITION_ATTRIBUTES.includes(attribute))
);
}
if (name.includes('-')) return false;
var prototype = native_prototypes.get(name);
if (prototype === undefined) {

@ -8,6 +8,9 @@ let inputs;
/** @type {MutationObserver} */
let observer;
/** @type {{ value: number }} */
let item;
export default test({
mode: ['hydrate'],
@ -23,6 +26,8 @@ export default test({
record();
item = /** @type {any} */ (target.querySelector('text')).x.baseVal.getItem(0);
observer = new MutationObserver(() => {});
observer.observe(target, { attributes: true, subtree: true });
},
@ -60,6 +65,10 @@ export default test({
observer.disconnect();
// the list item retrieved before hydration is still attached
item.value = 2;
assert.equal(target.querySelector('text')?.getAttribute('x'), '2');
assert.deepEqual(shadowed, ['HTMLFormElement', 'HTMLCollection', 'HTMLObjectElement']);
// no recovery
@ -84,6 +93,13 @@ export default test({
<form data-n="1"><input id="localName"></form>
<form data-n="1"><input name="namespaceURI"></form>
<xmlns data-n="1">x</xmlns><xmlns:ab data-n="1">x</xmlns:ab><svg><xmlns data-n="1"></xmlns></svg>
<svg>
<text x="2" rotate="1">a<tspan dy="1">b</tspan></text>
<polygon points="1"></polygon>
<filter><feColorMatrix type="saturate" values="1"></feColorMatrix></filter>
<image href="1"></image>
<image xlink:href="1"></image>
</svg>
`
);

@ -8,6 +8,7 @@
size = 24,
label = '2',
n = 1,
point = 1,
spread = { disabled: true, hidden: true, tabindex: -1 }
} = $props();
@ -52,3 +53,11 @@
<xmlns data-n={1}>x</xmlns>
<xmlns:ab data-n={1}>x</xmlns:ab>
<svg><xmlns data-n={1}></xmlns></svg>
<svg>
<text x={point} rotate={point}>a<tspan {...{ dy: point }}>b</tspan></text>
<polygon points={point} />
<filter><feColorMatrix type="saturate" values={point} /></filter>
<image href={point} />
<image {...{ 'xlink:href': point }} />
</svg>

@ -72,21 +72,6 @@ function alpha(canvas) {
/** @type {MutationObserver} */
let observer;
/** @type {Array<{ element: any, attribute: string, item: { value: number } }>} */
let lists;
/** @type {number[]} */
let errors;
const svg_lists = [
['text', 'x'],
['tspan', 'dy'],
['text', 'rotate'],
['feColorMatrix', 'values'],
['feFuncR', 'tableValues'],
['feConvolveMatrix', 'kernelMatrix']
];
export default test({
mode: ['hydrate'],
@ -134,16 +119,6 @@ export default test({
ok(alpha(canvas) === 255);
}
lists = svg_lists.map(([name, attribute]) => {
const element = /** @type {any} */ (target.querySelector(name));
return { element, attribute, item: element[attribute].baseVal.getItem(0) };
});
// on `about:blank` no image can load, and each has reported its error already
const images = [...target.querySelectorAll('image')];
errors = images.map(() => 0);
images.forEach((image, i) => image.addEventListener('error', () => errors[i]++));
const ObservedButton = observe(HTMLButtonElement, Object.keys(attributes));
const Inherited = observe(HTMLButtonElement, ['data-n']);
const Own = observe(HTMLButtonElement, ['data-n']);
@ -203,7 +178,7 @@ export default test({
});
},
async test({ assert, component, target }) {
test({ assert, component, target }) {
// spread `disabled` and `hidden` always use their setters
assert.deepEqual(
observer
@ -279,29 +254,6 @@ export default test({
assert.equal(canvas.height, 30);
}
// a list attribute write detaches the items retrieved before it
assert.deepEqual(
lists.map(({ element, attribute, item }) => {
item.value = 2;
return [
element.localName,
attribute,
item.value,
element.getAttribute(attribute),
element[attribute].baseVal.getItem(0).value
];
}),
svg_lists.map(([name, attribute]) => [name, attribute, 2, '1', 1])
);
// a URL write resolves it again
const start = performance.now();
while (errors.some((count) => count === 0) && performance.now() - start < 2000) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
await new Promise((resolve) => setTimeout(resolve, 100));
assert.deepEqual(errors, [1, 1, 1, 1, 1, 1]);
// with the native prototype, a customized built-in is compared as native (a known limitation)
assert.deepEqual(
changes.splice(0).sort(),

@ -10,7 +10,6 @@
translate,
draggable,
href,
n = 1,
size = 30,
observed,
native,
@ -53,21 +52,6 @@
<canvas width={size} height={size}></canvas>
<canvas {...{ width: size, height: size }}></canvas>
<svg>
<text x={n} rotate={n}>a<tspan {...{ dy: n }}>b</tspan></text>
<filter>
<feColorMatrix type="saturate" {...{ values: n }} />
<feComponentTransfer><feFuncR type="discrete" tableValues={n} /></feComponentTransfer>
<feConvolveMatrix order="1" {...{ kernelMatrix: n }} />
</filter>
<image href={n} />
<image href={true} />
<image {...{ href: n }} />
<image {...{ href: true }} />
<image {...{ 'xlink:href': n }} />
<image {...{ 'xlink:href': true }} />
</svg>
<button {...observed.retained}>retained</button>
<button {...observed.removed}>removed</button>
<button {...observed.unset}>unset</button>

Loading…
Cancel
Save