From 9f4521a75eab47c9fec1264a7a63410d7acd332a Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:07:05 +0100 Subject: [PATCH] perF(form-core): extract meta markers to state object In order to compute if we can re-use the previous meta, we currently tag two properties onto the meta (`derivedMetaSourceKey`, `derivedMetaCanDisplayErrorsKey`). This is fairly expensive because it means we change the shape of an otherwise consistently structured object each time the value changes. We only make use of this when the value changes, so this change switches to using a `markers` object which transitions between values. Basically this pattern: ```ts const markers = { ... }; createAtom((prev) => { // in here, mutate `markers` when prev != curr }); ``` This means the underlying meta object never changes shape. Also important to note, this now means `getFieldSnapshot` doesn't even pass through this code path anymore. NOTE: there's also a bit of a drive-by in here. I updated `nameToFieldNodeSegments` to slice between separators instead of appending characters one-by-one. Much faster, but can be split into its own PR if needed. --- .changeset/easy-rice-live.md | 5 ++ .../form-core/src/FieldApi/FieldApi.lib.ts | 13 +++- .../form-core/src/FieldApi/fieldState.lib.ts | 59 ++++++------------- packages/form-core/src/utils.lib.ts | 21 +++---- 4 files changed, 45 insertions(+), 53 deletions(-) create mode 100644 .changeset/easy-rice-live.md diff --git a/.changeset/easy-rice-live.md b/.changeset/easy-rice-live.md new file mode 100644 index 000000000..46ca06595 --- /dev/null +++ b/.changeset/easy-rice-live.md @@ -0,0 +1,5 @@ +--- +'@tanstack/form-core': patch +--- + +Improve field snapshot and metadata re-use strategy. diff --git a/packages/form-core/src/FieldApi/FieldApi.lib.ts b/packages/form-core/src/FieldApi/FieldApi.lib.ts index 6fdfbed98..674f2461b 100644 --- a/packages/form-core/src/FieldApi/FieldApi.lib.ts +++ b/packages/form-core/src/FieldApi/FieldApi.lib.ts @@ -61,6 +61,7 @@ import type { } from '../validation.public' import type { ChildContributionStates, + DerivedMetaMarkers, FieldAtoms, InternalBaseFieldMeta, InternalFieldMeta, @@ -348,11 +349,21 @@ export class InternalFieldApi< metaAtom = createAtom(defaultInternalBaseFieldMeta) } if (!storeAtom) { + const markers: DerivedMetaMarkers = { + source: undefined, + canDisplayErrors: undefined, + } storeAtom = createAtom((prev) => { const newMeta = metaAtom.get() const value = this._getValue() - const meta = deriveFromBaseFieldMeta(newMeta, prev?.meta, this, value) + const meta = deriveFromBaseFieldMeta( + newMeta, + prev?.meta, + this, + value, + markers, + ) if (prev?.meta === meta && prev.value === value) { return prev diff --git a/packages/form-core/src/FieldApi/fieldState.lib.ts b/packages/form-core/src/FieldApi/fieldState.lib.ts index da58f07e6..c213d596f 100644 --- a/packages/form-core/src/FieldApi/fieldState.lib.ts +++ b/packages/form-core/src/FieldApi/fieldState.lib.ts @@ -49,14 +49,13 @@ export interface FormGroupFieldErrorMeta { export interface InternalBaseFieldMeta extends BaseFieldMeta, MetaExtension {} export interface InternalFieldMeta extends AnyPublicFieldMeta, MetaExtension {} -const derivedMetaSourceKey = Symbol('tanstack-form-derived-meta-source') -const derivedMetaCanDisplayErrorsKey = Symbol( - 'tanstack-form-derived-meta-can-display-errors', -) - -type DerivedMetaMarkers = { - [derivedMetaCanDisplayErrorsKey]?: boolean - [derivedMetaSourceKey]?: InternalBaseFieldMeta +/** + * @private + * Used to track the prev meta for a field + */ +export interface DerivedMetaMarkers { + source: InternalBaseFieldMeta | undefined + canDisplayErrors: boolean | undefined } export interface InternalFieldState extends PublicFieldState { @@ -118,6 +117,7 @@ export function deriveFromBaseFieldMeta( previousMeta: InternalFieldMeta | undefined, field: AnyInternalFieldApi | undefined, value?: any, + markers?: DerivedMetaMarkers, ): InternalFieldMeta { const isDefaultValue = field ? field._getIsDefaultValue(value) : true const errorVisibility = getErrorVisibility(field) @@ -152,10 +152,12 @@ export function deriveFromBaseFieldMeta( if ( previousMeta && + markers && canReusePreviousMeta({ baseMeta, canDisplayErrors, isDefaultValue, + markers, originalErrors, previousMeta, }) @@ -184,55 +186,32 @@ export function deriveFromBaseFieldMeta( subfields, isPristine: !isDirty, } - return markDerivedMeta(result, baseMeta, canDisplayErrors) -} -function markDerivedMeta( - meta: InternalFieldMeta, - baseMeta: InternalBaseFieldMeta, - canDisplayErrors: boolean, -): InternalFieldMeta { - Object.defineProperties(meta, { - [derivedMetaCanDisplayErrorsKey]: { - value: canDisplayErrors, - }, - [derivedMetaSourceKey]: { - value: baseMeta, - }, - }) - - return meta -} - -function getDerivedMetaSource( - meta: (InternalFieldMeta & DerivedMetaMarkers) | undefined, -): InternalBaseFieldMeta | undefined { - return meta?.[derivedMetaSourceKey] -} + if (markers) { + markers.source = baseMeta + markers.canDisplayErrors = canDisplayErrors + } -function getDerivedMetaCanDisplayErrors( - meta: (InternalFieldMeta & DerivedMetaMarkers) | undefined, -): boolean | undefined { - return meta?.[derivedMetaCanDisplayErrorsKey] + return result } function canReusePreviousMeta({ baseMeta, canDisplayErrors, isDefaultValue, + markers, originalErrors, previousMeta, }: { baseMeta: InternalBaseFieldMeta canDisplayErrors: boolean isDefaultValue: boolean + markers: DerivedMetaMarkers originalErrors: Array previousMeta: InternalFieldMeta }): boolean { - if (getDerivedMetaSource(previousMeta) !== baseMeta) return false - if (getDerivedMetaCanDisplayErrors(previousMeta) !== canDisplayErrors) { - return false - } + if (markers.source !== baseMeta) return false + if (markers.canDisplayErrors !== canDisplayErrors) return false if (previousMeta.isDefaultValue !== isDefaultValue) return false if (previousMeta.original.errors !== originalErrors) return false diff --git a/packages/form-core/src/utils.lib.ts b/packages/form-core/src/utils.lib.ts index bbf5a3de1..3c4533d14 100644 --- a/packages/form-core/src/utils.lib.ts +++ b/packages/form-core/src/utils.lib.ts @@ -29,30 +29,27 @@ export function nameToFieldNodeSegments( if (typeof nameOrSegments !== 'string') return nameOrSegments.slice() const result: NameSegments = [] - let s = '' + let start = 0 for (let i = 0; i < nameOrSegments.length; i++) { switch (nameOrSegments.charCodeAt(i)) { case 0x2e: // '.' case 0x5b: // '[' - if (s.length > 0) { - result.push(s) - s = '' + if (i > start) { + result.push(nameOrSegments.slice(start, i)) } + start = i + 1 break case 0x5d: // ']' - if (s.length > 0) { - result.push(parseInt(s, 10)) - s = '' + if (i > start) { + result.push(parseInt(nameOrSegments.slice(start, i), 10)) } - break - default: - s += nameOrSegments.charAt(i) + start = i + 1 break } } - if (s.length > 0) { - result.push(s) + if (start < nameOrSegments.length) { + result.push(nameOrSegments.slice(start)) } return result