Skip to content

Commit eef7237

Browse files
committed
Reduce Preact prop updater allocations
1 parent 1e3ab34 commit eef7237

3 files changed

Lines changed: 80 additions & 32 deletions

File tree

.changeset/tidy-pandas-smile.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals": patch
3+
---
4+
5+
Reduce the memory used by signal-bound DOM properties.

mangle.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,13 @@
6969
"$_signal": "s",
7070
"$_updater": "__$u",
7171
"$_updateFlags": "__$f",
72-
"$_updaters": "U"
72+
"$_updaters": "U",
73+
"preact: PropUpdater": "",
74+
"$_dom": "D",
75+
"$_prop": "R",
76+
"$_propSignal": "P",
77+
"$_props": "O",
78+
"$_setAsProperty": "A"
7379
}
7480
}
7581
}

packages/preact/src/index.ts

Lines changed: 68 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type ModelConstructor,
1212
type ModelFactory,
1313
Signal,
14+
Effect as CoreEffect,
1415
type ReadonlySignal,
1516
untracked,
1617
SignalOptions,
@@ -312,47 +313,83 @@ hook(OptionsTypes.DIFFED, (old, vnode) => {
312313
old(vnode);
313314
});
314315

315-
function createPropUpdater(
316+
interface PropUpdater extends CoreEffect, PropertyUpdater {
317+
_dom: Element;
318+
_prop: string;
319+
_propSignal: Signal<Signal>;
320+
_props: Record<string, any>;
321+
_setAsProperty: boolean;
322+
}
323+
324+
function updateProp(this: PropUpdater) {
325+
this._notify = notifyDomUpdates;
326+
const value = this._propSignal.value.value;
327+
// If Preact just rendered this value, don't render it again:
328+
if (this._props[this._prop] === value) return;
329+
// Write the value back into the rendered props so that Preact's next
330+
// diff compares against what is actually in the DOM. The Signal
331+
// reference itself lives in vnode.__np and is restored into props by
332+
// the UNMOUNT hook, so this never clobbers it.
333+
this._props[this._prop] = value;
334+
if (this._setAsProperty) {
335+
// @ts-ignore-next-line silly
336+
this._dom[this._prop] = value;
337+
// Match Preact's attribute handling: data-* and aria-* attributes
338+
// https://github.com/preactjs/preact/blob/main/src/diff/props.js#L132
339+
} else if (value != null && (value !== false || this._prop[4] === "-")) {
340+
this._dom.setAttribute(this._prop, value);
341+
} else {
342+
this._dom.removeAttribute(this._prop);
343+
}
344+
}
345+
346+
const PropUpdater = function (
347+
this: PropUpdater,
316348
dom: Element,
317349
prop: string,
318350
propSignal: Signal,
319351
props: Record<string, any>
320-
): PropertyUpdater {
321-
const setAsProperty =
352+
) {
353+
CoreEffect.call(this, updateProp);
354+
this._dom = dom;
355+
this._prop = prop;
356+
this._propSignal = signal(propSignal);
357+
this._props = props;
358+
this._setAsProperty =
322359
prop in dom &&
323360
// SVG elements need to go through `setAttribute` because they
324361
// expect things like SVGAnimatedTransformList instead of strings.
325362
// @ts-ignore
326363
dom.ownerSVGElement === undefined;
364+
try {
365+
this._callback();
366+
} catch (err) {
367+
this._dispose();
368+
throw err;
369+
}
370+
} as unknown as {
371+
new (
372+
dom: Element,
373+
prop: string,
374+
propSignal: Signal,
375+
props: Record<string, any>
376+
): PropUpdater;
377+
prototype: PropUpdater;
378+
};
327379

328-
const changeSignal = signal(propSignal);
329-
return {
330-
_update: (newSignal: Signal, newProps: typeof props) => {
331-
changeSignal.value = newSignal;
332-
props = newProps;
333-
},
334-
_dispose: effect(function (this: Effect) {
335-
this._notify = notifyDomUpdates;
336-
const value = changeSignal.value.value;
337-
// If Preact just rendered this value, don't render it again:
338-
if (props[prop] === value) return;
339-
// Write the value back into the rendered props so that Preact's next
340-
// diff compares against what is actually in the DOM. The Signal
341-
// reference itself lives in vnode.__np and is restored into props by
342-
// the UNMOUNT hook, so this never clobbers it.
343-
props[prop] = value;
344-
if (setAsProperty) {
345-
// @ts-ignore-next-line silly
346-
dom[prop] = value;
347-
// Match Preact's attribute handling: data-* and aria-* attributes
348-
// https://github.com/preactjs/preact/blob/main/src/diff/props.js#L132
349-
} else if (value != null && (value !== false || prop[4] === "-")) {
350-
dom.setAttribute(prop, value);
351-
} else {
352-
dom.removeAttribute(prop);
353-
}
354-
}),
355-
};
380+
PropUpdater.prototype = new CoreEffect(updateProp) as PropUpdater;
381+
PropUpdater.prototype._update = function (newSignal, newProps) {
382+
this._propSignal.value = newSignal;
383+
this._props = newProps;
384+
};
385+
386+
function createPropUpdater(
387+
dom: Element,
388+
prop: string,
389+
propSignal: Signal,
390+
props: Record<string, any>
391+
): PropertyUpdater {
392+
return new PropUpdater(dom, prop, propSignal, props);
356393
}
357394

358395
/** Unsubscribe from Signals when unmounting components/vnodes */

0 commit comments

Comments
 (0)