From 149e2874babaf6886dd8d6d0046abd0bc95a320f Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sat, 9 May 2026 23:47:07 -0400 Subject: [PATCH 01/13] cache transition lookups for faster property setting --- src/core/elementNode.ts | 85 +++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index c5561b6..aa8c46f 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -136,12 +136,6 @@ export function convertToShader( return renderer.createShader(type, v); } -function getPropertyAlias(name: string) { - if (name === 'w') return 'width'; - if (name === 'h') return 'height'; - return name; -} - export const LightningRendererNumberProps = [ 'alpha', 'color', @@ -259,6 +253,11 @@ export interface ElementNode extends RendererNode, FocusNode { _states?: States; _style?: Styles; _theme?: Styles; + _transition?: + | Record + | true + | false; + _transitionLookup?: Record; _lastAnyKeyPressTime?: number; _type: 'element' | 'textNode'; _undoStyles?: string[]; @@ -602,16 +601,6 @@ export interface ElementNode extends RendererNode, FocusNode { * @see https://lightning-tv.github.io/solid/#/flow/layout */ zIndex?: number; - /** - * Defines transitions for animatable properties. - * - * @see https://lightning-tv.github.io/solid/#/essentials/transitions?id=transitions-animations - */ - transition?: - | Record - | true - | false; - /** Optional handler for when the element is created and rendered. * * @see https://lightning-tv.github.io/solid/#/flow/ondestroy @@ -856,27 +845,20 @@ export class ElementNode extends Object { } _sendToLightningAnimatable(name: string, value: number) { - if ( - this.transition && - this.rendered && - Config.animationsEnabled && - (this.transition === true || - this.transition[name] || - this.transition[getPropertyAlias(name)]) - ) { - const animationSettings = - this.transition === true || this.transition[name] === true - ? undefined - : this.transition[name] || - (this.transition[getPropertyAlias(name)] as - | undefined - | AnimationSettings); - - return (this.lng as any).animateProp( - name, - value, - animationSettings || this.animationSettings || {}, - ); + const t = this._transition; + if (t !== undefined && this.rendered && Config.animationsEnabled) { + // Fast path: single probe into a pre-aliased lookup, instead of probing + // both `transition[name]` and `transition[getPropertyAlias(name)]`. + const setting = + t === true ? true : (this._transitionLookup as any)?.[name]; + if (setting !== undefined) { + const animationSettings = setting === true ? undefined : setting; + return (this.lng as any).animateProp( + name, + value, + animationSettings || this.animationSettings || {}, + ); + } } (this.lng[name as keyof (IRendererNode | INode)] as number | string) = @@ -1055,6 +1037,35 @@ export class ElementNode extends Object { return this._style || {}; } + set transition( + v: + | Record + | true + | false + | undefined, + ) { + this._transition = v; + if (v === undefined || v === true || v === false) { + this._transitionLookup = undefined; + return; + } + // Pre-build an aliased lookup so the hot path needs only a single probe. + // Only width/height have aliases (w/h); copy other keys verbatim. + const lookup: Record = {}; + for (const key in v) { + const val = v[key]; + if (val === undefined || val === false) continue; + lookup[key] = val as AnimationSettings | true; + if (key === 'width') lookup.w = val as AnimationSettings | true; + else if (key === 'height') lookup.h = val as AnimationSettings | true; + } + this._transitionLookup = lookup; + } + + get transition() { + return this._transition; + } + set theme(styles: Styles | undefined) { if (!styles) { return; From da31612a7d7e3541b2500bca9945d6023977f8e7 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 00:38:03 -0400 Subject: [PATCH 02/13] add textNode class for createTextNode for v8 optimizations --- src/core/dom-renderer/domRenderer.ts | 2 + src/core/elementNode.ts | 18 +++- src/core/intrinsicTypes.ts | 10 +- src/core/nodeTypes.ts | 12 +++ src/core/utils.ts | 4 +- src/primitives/useMouse.ts | 6 +- src/solidOpts.ts | 5 +- tests/flex-perf-results.json | 142 +++++++++++++-------------- tests/flex.spec.ts | 12 +-- tests/flex_min_size.spec.ts | 10 +- 10 files changed, 116 insertions(+), 105 deletions(-) diff --git a/src/core/dom-renderer/domRenderer.ts b/src/core/dom-renderer/domRenderer.ts index aa24440..066051d 100644 --- a/src/core/dom-renderer/domRenderer.ts +++ b/src/core/dom-renderer/domRenderer.ts @@ -144,6 +144,7 @@ class AnimationController implements lng.IAnimationController { loop: rawSettings.loop ?? false, repeat: rawSettings.repeat ?? 1, stopMethod: false, + adaptiveDuration: rawSettings.adaptiveDuration ?? false, }; this.timeEnd = @@ -1862,6 +1863,7 @@ export class DOMRendererMain implements IRendererMain { }, loadFont: async () => {}, cleanup() {}, + requestRender() {}, }; this.root = new DOMNode( diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index aa8c46f..146bfbe 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -8,7 +8,6 @@ import { type Styles, AddColorString, TextProps, - TextNode, type OnEvent, NewOmit, SingleBorderStyle, @@ -47,7 +46,7 @@ import type { INodeProps, } from '@solidtv/renderer'; import { assertTruthy } from '@solidtv/renderer/utils'; -import { NodeType } from './nodeTypes.js'; +import { NodeType, TextNode } from './nodeTypes.js'; import { ForwardFocusHandler, setActiveElement, @@ -262,6 +261,16 @@ export interface ElementNode extends RendererNode, FocusNode { _type: 'element' | 'textNode'; _undoStyles?: string[]; autosize?: boolean; + /** + * Optional component name for inspector / dev tooling — emitted by the + * Babel devtools plugin (see `devtools/jsx-locator.js`). + */ + componentName?: string; + /** + * Optional source-location string for inspector / dev tooling — emitted by + * the Babel devtools plugin. + */ + componentLocation?: string; /** * The distance from the bottom edge of the parent element. * When `bottom` is set, `mountY` is automatically set to 1. @@ -815,7 +824,10 @@ export class ElementNode extends Object { removeChild(node: ElementNode | ElementText | TextNode) { if (spliceItem(this.children, node, 1) > -1) { - node.onRemove?.call(node, node); + if (isElementNode(node) && node.onRemove) { + node.onRemove.call(node, node); + } + if (this.requiresLayout()) { addToLayoutQueue(this); } diff --git a/src/core/intrinsicTypes.ts b/src/core/intrinsicTypes.ts index f94f1f9..7ee5a85 100644 --- a/src/core/intrinsicTypes.ts +++ b/src/core/intrinsicTypes.ts @@ -1,5 +1,6 @@ import * as lngr from '@solidtv/renderer'; import { ElementNode, type RendererNode } from './elementNode.js'; +import type { TextNode } from './nodeTypes.js'; import { NodeStates } from './states.js'; import { ShaderBorderProps, @@ -115,13 +116,6 @@ export interface ElementText style: TextStyles; } -export interface TextNode { - _type: 'text'; - parent?: ElementText; - text: string; - [key: string]: any; -} - export interface NodeProps extends RendererNode, EventHandlers, @@ -140,6 +134,8 @@ export interface NodeProps | 'preFlexheight' | 'width' | 'height' + | 'componentName' + | 'componentLocation' > > { states?: NodeStates; diff --git a/src/core/nodeTypes.ts b/src/core/nodeTypes.ts index c7ef095..ede3068 100644 --- a/src/core/nodeTypes.ts +++ b/src/core/nodeTypes.ts @@ -1,6 +1,18 @@ +import type { ElementText } from './intrinsicTypes.js'; + export const NodeType = { Element: 'element', TextNode: 'textNode', Text: 'text', } as const; export type NodeTypes = (typeof NodeType)[keyof typeof NodeType]; + +export class TextNode { + readonly _type: 'text' = 'text'; + parent: ElementText | undefined = undefined; + text: string; + + constructor(text: string) { + this.text = text; + } +} diff --git a/src/core/utils.ts b/src/core/utils.ts index 7e36478..b04dd77 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,8 +1,8 @@ import { type INode, type Point } from '@solidtv/renderer'; import { Config, isDev } from './config.js'; -import type { Styles, ElementText, TextNode } from './intrinsicTypes.js'; +import type { Styles, ElementText } from './intrinsicTypes.js'; import { ElementNode } from './elementNode.js'; -import { NodeType } from './nodeTypes.js'; +import { NodeType, TextNode } from './nodeTypes.js'; function hasDebug(node: any) { return isObject(node) && node.debug; diff --git a/src/primitives/useMouse.ts b/src/primitives/useMouse.ts index 7a013ae..c0fc352 100644 --- a/src/primitives/useMouse.ts +++ b/src/primitives/useMouse.ts @@ -30,21 +30,21 @@ export function addCustomStateToElement( element: RenderableNode, state: CustomState, ): void { - element.states?.add(state); + (element as ElementNode).states?.add(state); } export function removeCustomStateFromElement( element: RenderableNode, state: CustomState, ): void { - element?.states?.remove(state); + (element as ElementNode)?.states?.remove(state); } export function hasCustomState( element: RenderableNode, state: CustomState, ): boolean { - return element.states?.has(state); + return (element as ElementNode).states?.has(state); } function createKeyboardEvent( diff --git a/src/solidOpts.ts b/src/solidOpts.ts index e4f404d..64ffa30 100644 --- a/src/solidOpts.ts +++ b/src/solidOpts.ts @@ -2,10 +2,9 @@ import { assertTruthy, isElementText, ElementNode, - NodeType, + TextNode, log, type ElementText, - type TextNode, } from './core/index.js'; import type { SolidNode, SolidRendererOptions } from './types.js'; @@ -47,7 +46,7 @@ export default { }, createTextNode(text: string): TextNode { // A text node is just a string - not the node - return { _type: NodeType.Text, text }; + return new TextNode(text); }, replaceText(node: TextNode, value: string): void { log('Replace Text: ', node, value); diff --git a/tests/flex-perf-results.json b/tests/flex-perf-results.json index 3a7c765..d060f68 100644 --- a/tests/flex-perf-results.json +++ b/tests/flex-perf-results.json @@ -1,494 +1,494 @@ { - "timestamp": "2026-01-28T14:56:07.215Z", + "timestamp": "2026-05-10T04:06:01.439Z", "results": [ { "id": "Row, FlexStart, NoGrow-3", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 3, - "durationMs": 0.09181966666668966, + "durationMs": 0.11875000000001516, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-5", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 5, - "durationMs": 0.07754166666666151, + "durationMs": 0.05390266666677235, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-10", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 10, - "durationMs": 0.013624666666676907, + "durationMs": 0.012680666666634957, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-15", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 15, - "durationMs": 0.0075693333333219925, + "durationMs": 0.018555666666694986, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-20", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 20, - "durationMs": 0.013458666666679164, + "durationMs": 0.022000333333380695, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-50", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 50, - "durationMs": 0.033972333333395, + "durationMs": 0.029555333333291856, "containerUpdated": false }, { "id": "Row, FlexStart, NoGrow-100", "scenarioName": "Row, FlexStart, NoGrow", "numChildren": 100, - "durationMs": 0.05277766666665684, + "durationMs": 0.06299966666665568, "containerUpdated": false }, { "id": "Column, Center, WithGrow-3", "scenarioName": "Column, Center, WithGrow", "numChildren": 3, - "durationMs": 0.039985999999961074, + "durationMs": 0.038736000000047476, "containerUpdated": false }, { "id": "Column, Center, WithGrow-5", "scenarioName": "Column, Center, WithGrow", "numChildren": 5, - "durationMs": 0.006319333333332604, + "durationMs": 0.008291666666764286, "containerUpdated": false }, { "id": "Column, Center, WithGrow-10", "scenarioName": "Column, Center, WithGrow", "numChildren": 10, - "durationMs": 0.00856933333333624, + "durationMs": 0.010361333333321454, "containerUpdated": false }, { "id": "Column, Center, WithGrow-15", "scenarioName": "Column, Center, WithGrow", "numChildren": 15, - "durationMs": 0.014097333333362863, + "durationMs": 0.017236333333357834, "containerUpdated": false }, { "id": "Column, Center, WithGrow-20", "scenarioName": "Column, Center, WithGrow", "numChildren": 20, - "durationMs": 0.07026399999998982, + "durationMs": 0.07058333333323692, "containerUpdated": false }, { "id": "Column, Center, WithGrow-50", "scenarioName": "Column, Center, WithGrow", "numChildren": 50, - "durationMs": 0.032528000000032385, + "durationMs": 0.04047200000006038, "containerUpdated": false }, { "id": "Column, Center, WithGrow-100", "scenarioName": "Column, Center, WithGrow", "numChildren": 100, - "durationMs": 0.057375000000016975, + "durationMs": 0.09191633333337752, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-3", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 3, - "durationMs": 0.013583333333296347, + "durationMs": 0.00926366666665975, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-5", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 5, - "durationMs": 0.004722333333385602, + "durationMs": 0.005611333333339037, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-10", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 10, - "durationMs": 0.008374999999963014, + "durationMs": 0.007764000000027711, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-15", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 15, - "durationMs": 0.009791666666653024, + "durationMs": 0.014541999999967933, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-20", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 20, - "durationMs": 0.011611000000016247, + "durationMs": 0.019888666666626403, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-50", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 50, - "durationMs": 0.02738899999997102, + "durationMs": 0.02681933333330259, "containerUpdated": false }, { "id": "Row, SpaceBetween, MixedGrow-100", "scenarioName": "Row, SpaceBetween, MixedGrow", "numChildren": 100, - "durationMs": 0.05229166666670911, + "durationMs": 0.047180333333320355, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-3", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 3, - "durationMs": 0.01195866666667674, + "durationMs": 0.01819399999999405, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-5", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 5, - "durationMs": 0.004111000000042016, + "durationMs": 0.0060829999999896245, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-10", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 10, - "durationMs": 0.005652999999976298, + "durationMs": 0.010041333333295674, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-15", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 15, - "durationMs": 0.007749999999987267, + "durationMs": 0.009902666666675941, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-20", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 20, - "durationMs": 0.010055000000003625, + "durationMs": 0.014041666666647265, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-50", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 50, - "durationMs": 0.02672233333332012, + "durationMs": 0.03129166666659936, "containerUpdated": false }, { "id": "Row, Wrap, FlexStart-100", "scenarioName": "Row, Wrap, FlexStart", "numChildren": 100, - "durationMs": 0.050846999999976106, + "durationMs": 0.07265266666665109, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-3", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 3, - "durationMs": 0.0036666666666936485, + "durationMs": 0.00651366666663004, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-5", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 5, - "durationMs": 0.0026663333333469077, + "durationMs": 0.004652333333448648, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-10", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 10, - "durationMs": 0.004611333333324789, + "durationMs": 0.00651366666663004, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-15", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 15, - "durationMs": 0.006485999999957433, + "durationMs": 0.008555666666704079, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-20", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 20, - "durationMs": 0.008750333333296112, + "durationMs": 0.01136099999992742, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-50", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 50, - "durationMs": 0.020319666666637204, + "durationMs": 0.0339446666666845, "containerUpdated": false }, { "id": "Row, RTL, FlexEnd-100", "scenarioName": "Row, RTL, FlexEnd", "numChildren": 100, - "durationMs": 0.044847666666669284, + "durationMs": 0.046291666666699406, "containerUpdated": false }, { "id": "Row, WithOrder-3", "scenarioName": "Row, WithOrder", "numChildren": 3, - "durationMs": 0.017847333333406823, + "durationMs": 0.024333666666734644, "containerUpdated": false }, { "id": "Row, WithOrder-5", "scenarioName": "Row, WithOrder", "numChildren": 5, - "durationMs": 0.0045136666666773335, + "durationMs": 0.011805333333313683, "containerUpdated": false }, { "id": "Row, WithOrder-10", "scenarioName": "Row, WithOrder", "numChildren": 10, - "durationMs": 0.005639000000011644, + "durationMs": 0.008222333333227047, "containerUpdated": false }, { "id": "Row, WithOrder-15", "scenarioName": "Row, WithOrder", "numChildren": 15, - "durationMs": 0.008500333333320972, + "durationMs": 0.021194333333293496, "containerUpdated": false }, { "id": "Row, WithOrder-20", "scenarioName": "Row, WithOrder", "numChildren": 20, - "durationMs": 0.011596999999975802, + "durationMs": 0.014153000000078464, "containerUpdated": false }, { "id": "Row, WithOrder-50", "scenarioName": "Row, WithOrder", "numChildren": 50, - "durationMs": 0.024833666666684923, + "durationMs": 0.03894466666664206, "containerUpdated": false }, { "id": "Row, WithOrder-100", "scenarioName": "Row, WithOrder", "numChildren": 100, - "durationMs": 0.058277666666678364, + "durationMs": 0.07912466666668176, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-3", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 3, - "durationMs": 0.007847000000045531, + "durationMs": 0.013125333333315817, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-5", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 5, - "durationMs": 0.0032916666666551464, + "durationMs": 0.005166666666658178, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-10", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 10, - "durationMs": 0.005319666666650846, + "durationMs": 0.006486333333441507, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-15", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 15, - "durationMs": 0.010333333333392147, + "durationMs": 0.008652999999943253, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-20", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 20, - "durationMs": 0.018374999999991815, + "durationMs": 0.01225033333336493, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-50", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 50, - "durationMs": 0.02566700000003645, + "durationMs": 0.0261803333332864, "containerUpdated": false }, { "id": "Row, FlexStart, WithMargins-100", "scenarioName": "Row, FlexStart, WithMargins", "numChildren": 100, - "durationMs": 0.07530566666666043, + "durationMs": 0.05445799999999205, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-3", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 3, - "durationMs": 0.006624999999985448, + "durationMs": 0.00927799999999479, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-5", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 5, - "durationMs": 0.003055333333350063, + "durationMs": 0.004819666666586879, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-10", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 10, - "durationMs": 0.005347333333361348, + "durationMs": 0.00670833333341155, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-15", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 15, - "durationMs": 0.007416666666661816, + "durationMs": 0.008860666666654046, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-20", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 20, - "durationMs": 0.009458333333327573, + "durationMs": 0.012014000000059847, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-50", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 50, - "durationMs": 0.023111333333304174, + "durationMs": 0.024389333333298662, "containerUpdated": false }, { "id": "Row, FlexStart, WithGap-100", "scenarioName": "Row, FlexStart, WithGap", "numChildren": 100, - "durationMs": 0.04465300000000146, + "durationMs": 0.0451666666666218, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-3", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 3, - "durationMs": 0.005819666666676919, + "durationMs": 0.00968066666678169, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-5", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 5, - "durationMs": 0.0031943333333401824, + "durationMs": 0.005028000000038446, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-10", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 10, - "durationMs": 0.005847333333311629, + "durationMs": 0.006764333333270163, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-15", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 15, - "durationMs": 0.008639000000016495, + "durationMs": 0.008958333333339397, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-20", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 20, - "durationMs": 0.010791666666667274, + "durationMs": 0.012375000000020009, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-50", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 50, - "durationMs": 0.02612500000001698, + "durationMs": 0.02586133333337178, "containerUpdated": false }, { "id": "Row, FlexStart, AlignCenter (Cross Axis)-100", "scenarioName": "Row, FlexStart, AlignCenter (Cross Axis)", "numChildren": 100, - "durationMs": 0.05776400000002013, + "durationMs": 0.05345833333346187, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-3", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 3, - "durationMs": 0.012652666666667756, + "durationMs": 0.014444666666652969, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-5", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 5, - "durationMs": 0.003597333333345887, + "durationMs": 0.005333666666577604, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-10", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 10, - "durationMs": 0.006291666666659997, + "durationMs": 0.007194666666615983, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-15", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 15, - "durationMs": 0.008819333333311382, + "durationMs": 0.014250000000023041, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-20", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 20, - "durationMs": 0.011222333333307688, + "durationMs": 0.01663866666664641, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-50", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 50, - "durationMs": 0.026847000000050986, + "durationMs": 0.033486000000038985, "containerUpdated": false }, { "id": "Row, FlexStart, AlignSelf (Cross Axis)-100", "scenarioName": "Row, FlexStart, AlignSelf (Cross Axis)", "numChildren": 100, - "durationMs": 0.0635420000000219, + "durationMs": 0.08762466666674602, "containerUpdated": false } ] diff --git a/tests/flex.spec.ts b/tests/flex.spec.ts index d1ae0b1..5357585 100644 --- a/tests/flex.spec.ts +++ b/tests/flex.spec.ts @@ -2,8 +2,8 @@ import { ElementNode } from '../src/core/elementNode.ts'; import calculateFlex from '../src/core/flex.ts'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { isElementNode } from '../src/core/utils.ts'; -import { NodeType } from '../src/core/nodeTypes.ts'; -import type { ElementText, TextNode } from '../src/index.ts'; +import { TextNode } from '../src/core/nodeTypes.ts'; +import type { ElementText } from '../src/index.ts'; // Helper to create a basic ElementNode for flex testing // (Adapted from flex.performance.spec.ts) @@ -14,13 +14,7 @@ function createTestElement( } = {}, ): ElementNode | TextNode { if (initialProps.nodeType === 'text') { - // Create a simple TextNode (not ElementText which is a type of ElementNode) - const textNodeInstance: TextNode = { - _type: NodeType.Text, - text: (initialProps as any).text || '', - // Add other properties if TextNode has them and they are relevant for testing - }; - return textNodeInstance; + return new TextNode((initialProps as any).text || ''); } const nodeTypeName = diff --git a/tests/flex_min_size.spec.ts b/tests/flex_min_size.spec.ts index 2deb0b5..5ed5498 100644 --- a/tests/flex_min_size.spec.ts +++ b/tests/flex_min_size.spec.ts @@ -2,8 +2,8 @@ import { ElementNode } from '../src/core/elementNode.ts'; import calculateFlex from '../src/core/flex.ts'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { isElementNode } from '../src/core/utils.ts'; -import { NodeType } from '../src/core/nodeTypes.ts'; -import type { ElementText, TextNode } from '../src/index.ts'; +import { TextNode } from '../src/core/nodeTypes.ts'; +import type { ElementText } from '../src/index.ts'; // Helper to create a basic ElementNode for flex testing function createTestElement( @@ -13,11 +13,7 @@ function createTestElement( } = {}, ): ElementNode | TextNode { if (initialProps.nodeType === 'text') { - const textNodeInstance: TextNode = { - _type: NodeType.Text, - text: (initialProps as any).text || '', - }; - return textNodeInstance; + return new TextNode((initialProps as any).text || ''); } const nodeTypeName = From 2bf6f4976865a51ede72832566aac38b258504f4 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 01:00:39 -0400 Subject: [PATCH 03/13] use a single post render queue --- src/core/elementNode.ts | 109 ++++++++++++++++++++++++---------------- src/solidOpts.ts | 16 ++---- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 146bfbe..6124183 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -62,27 +62,46 @@ import { IRendererTextNodeProps, } from './dom-renderer/domRendererTypes.js'; +// Unified post-mutation scheduler. +// +// Three phases run in one microtask (or one renderer-tick callback): +// 1. delete-flush — destroy nodes that were removed and not re-inserted +// 2. layout — recompute flex layout for any dirty subtree +// 3. focus — resolve forwardFocus on deferred elements, then apply +// +// Order matters: layout reads the rendered tree (so destroyed nodes must be +// gone), and focus reads the laid-out tree. +let postMutationQueued = false; let nextActiveElement: ElementNode | null = null; -let focusQueued: boolean = false; -let layoutRunQueued = false; +let deferredFocusElement: ElementNode | null = null; const layoutQueue = new Set(); +export const elementDeleteQueue: ElementNode[] = []; + +export function schedulePostMutation() { + if (postMutationQueued) return; + postMutationQueued = true; + if ('reprocessUpdates' in renderer.stage && renderer.stage.reprocessUpdates) { + renderer.stage.reprocessUpdates(runPostMutation); + } else { + queueMicrotask(runPostMutation); + } +} -function addToLayoutQueue(node: ElementNode) { - layoutQueue.add(node); - if (!layoutRunQueued) { - layoutRunQueued = true; - if ( - 'reprocessUpdates' in renderer.stage && - renderer.stage.reprocessUpdates - ) { - renderer.stage.reprocessUpdates(runLayout); - } else { - queueMicrotask(runLayout); +function runPostMutation() { + postMutationQueued = false; + + // Phase 1: delete-flush + if (elementDeleteQueue.length > 0) { + for (let el of elementDeleteQueue) { + if (Number(el._queueDelete) < 0) { + el.destroy(); + } + el._queueDelete = undefined; } + elementDeleteQueue.length = 0; } -} -function runLayout() { + // Phase 2: layout while (layoutQueue.size > 0) { const queue = [...layoutQueue]; layoutQueue.clear(); @@ -91,7 +110,25 @@ function runLayout() { node.updateLayout(); } } - layoutRunQueued = false; + + // Phase 3: focus. setFocus() may have evaluated forwardFocus pre-render + // (when no children existed yet); deferredFocusElement re-runs setFocus + // here once the subtree has rendered, then setActiveElement is applied. + if (deferredFocusElement !== null) { + const el = deferredFocusElement; + deferredFocusElement = null; + el.setFocus(); + } + if (nextActiveElement !== null) { + const element = nextActiveElement; + nextActiveElement = null; + setActiveElement(element); + } +} + +function addToLayoutQueue(node: ElementNode) { + layoutQueue.add(node); + schedulePostMutation(); } const parseAndAssignShaderProps = ( @@ -958,19 +995,10 @@ export class ElementNode extends Object { } } } - // Delay setting focus so children can render (useful for Row + Column) + // Delay setting focus so children can render (useful for Row + Column). + // The post-mutation scheduler applies setActiveElement in its focus phase. nextActiveElement = this; - if (focusQueued === false) { - focusQueued = true; - queueMicrotask(() => { - focusQueued = false; - if (nextActiveElement) { - const element = nextActiveElement; - nextActiveElement = null; - setActiveElement(element); - } - }); - } + schedulePostMutation(); } else { this._autofocus = true; } @@ -978,12 +1006,7 @@ export class ElementNode extends Object { _layoutOnLoad() { (this.lng as IRendererNode).on('loaded', () => { - if ( - 'reprocessUpdates' in renderer.stage && - renderer.stage.reprocessUpdates - ) { - renderer.stage.reprocessUpdates(runLayout); - } + schedulePostMutation(); this.parent!.updateLayout(); }); } @@ -1173,9 +1196,12 @@ export class ElementNode extends Object { */ set autofocus(val: any) { this._autofocus = val; - // Delay setting focus so children can render (useful for Row + Column) - // which now uses forwardFocus - val && queueMicrotask(() => this.setFocus()); + // Defer setFocus so children render first (forwardFocus needs them). + // The post-mutation focus phase calls setFocus on this element. + if (val) { + deferredFocusElement = this; + schedulePostMutation(); + } } get autofocus() { @@ -1519,11 +1545,10 @@ export class ElementNode extends Object { } } } - if (topNode && !layoutRunQueued) { - //Do one pass of layout, then another with Text loads - layoutRunQueued = true; - // We use queue because loop will add children one at a time, causing lots of layout - queueMicrotask(runLayout); + if (topNode) { + // Schedule one post-mutation pass; may add many children in one + // tick, but the scheduler dedupes and runs everything once. + schedulePostMutation(); } node._autofocus && node.setFocus(); diff --git a/src/solidOpts.ts b/src/solidOpts.ts index 64ffa30..1b880c2 100644 --- a/src/solidOpts.ts +++ b/src/solidOpts.ts @@ -5,6 +5,8 @@ import { TextNode, log, type ElementText, + elementDeleteQueue, + schedulePostMutation, } from './core/index.js'; import type { SolidNode, SolidRendererOptions } from './types.js'; @@ -17,23 +19,11 @@ Object.defineProperty(ElementNode.prototype, 'preserve', { }, }); -let elementDeleteQueue: ElementNode[] = []; - -function flushDeleteQueue(): void { - for (let el of elementDeleteQueue) { - if (Number(el._queueDelete) < 0) { - el.destroy(); - } - el._queueDelete = undefined; - } - elementDeleteQueue.length = 0; -} - function pushDeleteQueue(node: ElementNode, n: number): void { if (node._queueDelete === undefined) { node._queueDelete = n; if (elementDeleteQueue.push(node) === 1) { - queueMicrotask(flushDeleteQueue); + schedulePostMutation(); } } else { node._queueDelete += n; From a0085fb723c477015e0ea22568744cc9c98ffeec Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 01:10:39 -0400 Subject: [PATCH 04/13] optimize settings styles before initial render --- src/core/elementNode.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 6124183..5431101 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -205,6 +205,14 @@ export const LightningRendererNumberProps = [ 'zIndexLocked', ]; +/** + * Keys whose setter (pre-render) is equivalent to `this.lng[key] = value`. + * Used by the `style` setter's fast path to skip the prototype setter chain + * when applying styles before render. Built lazily after both prop arrays + * exist (see bottom of file). + */ +const fastPathStyleKeys = new Set(); + const LightningRendererNonAnimatingProps = [ 'absX', 'absY', @@ -1059,11 +1067,20 @@ export class ElementNode extends Object { this._style = style; - // Keys set in JSX are more important - for (const key in this._style) { - // be careful of 0 values - if (this[key as keyof Styles] === undefined) { - this[key as keyof Styles] = this._style[key as keyof Styles]; + // Keys set in JSX are more important. Pre-render, plain animatable / + // non-animating props can be written directly to the lng props bag, + // bypassing the per-key setter chain. Special-meaning keys (shader, + // border, transition, states, etc.) still go through the setter. + const lng = this.lng as any; + const fastPath = !this.rendered; + for (const key in style) { + if (fastPath && fastPathStyleKeys.has(key)) { + if (lng[key] === undefined) { + lng[key] = (style as any)[key]; + } + } else if (this[key as keyof Styles] === undefined) { + // be careful of 0 values + this[key as keyof Styles] = style[key as keyof Styles]; } } } @@ -1556,6 +1573,7 @@ export class ElementNode extends Object { } for (const key of LightningRendererNumberProps) { + fastPathStyleKeys.add(key); Object.defineProperty(ElementNode.prototype, key, { get(): number { return this.lng[key]; @@ -1567,6 +1585,7 @@ for (const key of LightningRendererNumberProps) { } for (const key of LightningRendererNonAnimatingProps) { + fastPathStyleKeys.add(key); Object.defineProperty(ElementNode.prototype, key, { get(): unknown { return this.lng[key]; From 2687f94410b395e079343cd268bc6219e5d1312a Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 01:24:26 -0400 Subject: [PATCH 05/13] optimize Config.fontSettings defaults --- src/core/elementNode.ts | 43 ++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 5431101..f526920 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -131,6 +131,27 @@ function addToLayoutQueue(node: ElementNode) { schedulePostMutation(); } +// Text-default template, built once on first use. Config.fontSettings is +// expected to be set at app startup and not change afterwards. +let _fontTemplate: Array<[string, any]> | undefined; +let _fontFamilyIdx = -1; +let _fontFamilyWithWeight: string | undefined; + +function buildFontTemplate() { + const tpl: Array<[string, any]> = []; + const fs = Config.fontSettings; + if (fs) { + for (const key in fs) { + if (key === 'fontFamily') { + _fontFamilyIdx = tpl.length; + _fontFamilyWithWeight = `${fs.fontFamily}${fs.fontWeight || ''}`; + } + tpl.push([key, (fs as any)[key]]); + } + } + _fontTemplate = tpl; +} + const parseAndAssignShaderProps = ( prefix: string, obj: Record, @@ -1417,14 +1438,22 @@ export class ElementNode extends Object { if (isElementText(node)) { const textProps = props as TextProps; - if (Config.fontSettings) { - for (const key in Config.fontSettings) { + if (_fontTemplate === undefined) buildFontTemplate(); + const tpl = _fontTemplate!; + if (tpl.length > 0) { + const familyIdx = _fontFamilyIdx; + const familyWithWeight = + textProps['fontWeight'] === undefined + ? _fontFamilyWithWeight + : undefined; + for (let i = 0; i < tpl.length; i++) { + const entry = tpl[i]!; + const key = entry[0]; if (textProps[key] === undefined) { - let value = Config.fontSettings[key]; - if (key === 'fontFamily' && textProps['fontWeight'] === undefined) { - value = `${value}${Config.fontSettings.fontWeight || ''}`; - } - textProps[key] = value; + textProps[key] = + i === familyIdx && familyWithWeight !== undefined + ? familyWithWeight + : entry[1]; } } } From 0a6210dc2f2a1e6718963a27759b38526609ac82 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 01:57:31 -0400 Subject: [PATCH 06/13] fix up single post render queue --- src/core/elementNode.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index f526920..3554397 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -82,9 +82,8 @@ export function schedulePostMutation() { postMutationQueued = true; if ('reprocessUpdates' in renderer.stage && renderer.stage.reprocessUpdates) { renderer.stage.reprocessUpdates(runPostMutation); - } else { - queueMicrotask(runPostMutation); } + queueMicrotask(runPostMutation); } function runPostMutation() { From 6a0aefe4d0308b2639e4a2c5a585948e3f8f89ea Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Thu, 30 Apr 2026 16:06:18 -0400 Subject: [PATCH 07/13] relocate router exports to better decouple solidjs-router --- package.json | 7 +++++++ src/primitives/index.ts | 2 -- src/primitives/routerIndex.ts | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/primitives/routerIndex.ts diff --git a/package.json b/package.json index 5fcf19a..e1bfa04 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,13 @@ "default": "./dist/src/primitives/index.js" } }, + "./primitives/router": { + "@solidtv/source": "./src/primitives/routerIndex.ts", + "import": { + "types": "./dist/src/primitives/routerIndex.d.ts", + "default": "./dist/src/primitives/routerIndex.js" + } + }, "./devtools": { "@solidtv/source": "./src/devtools/index.ts", "import": { diff --git a/src/primitives/index.ts b/src/primitives/index.ts index 7622b2b..d93e94d 100644 --- a/src/primitives/index.ts +++ b/src/primitives/index.ts @@ -8,7 +8,6 @@ export * from './Lazy.jsx'; export * from './LazyImport.js'; export * from './Image.jsx'; export * from './Visible.jsx'; -export * from './router.js'; export * from './Column.jsx'; export * from './Row.jsx'; export * from './Grid.jsx'; @@ -19,7 +18,6 @@ export * from './Suspense.jsx'; export * from './Marquee.jsx'; export * from './createFocusStack.jsx'; export * from './useHold.js'; -export * from './KeepAlive.jsx'; export * from './VirtualGrid.jsx'; export * from './Virtual.jsx'; export * from './utils/withScrolling.js'; diff --git a/src/primitives/routerIndex.ts b/src/primitives/routerIndex.ts new file mode 100644 index 0000000..1b99662 --- /dev/null +++ b/src/primitives/routerIndex.ts @@ -0,0 +1,2 @@ +export * from './router.js'; +export * from './KeepAlive.jsx'; From 01b35f0e342dcadeb9fa2d5fc7bbf55daf91ef27 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 17:12:43 -0400 Subject: [PATCH 08/13] optimize constructor for ElementNode --- src/core/elementNode.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 3554397..ffd2ea7 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -746,13 +746,39 @@ export interface ElementNode extends RendererNode, FocusNode { stateOrder?: DollarString[]; } -export class ElementNode extends Object { +export class ElementNode { constructor(name: string) { - super(); this._type = name === 'text' ? NodeType.TextNode : NodeType.Element; this.rendered = false; this.lng = {}; this.children = []; + + // Initialize lazy underscore fields explicitly in a fixed order. This + // gives every ElementNode the same hidden class on construction; later + // assignments transition predictably instead of forking shapes by + // first-touch order. + this._queueDelete = undefined; + this._animationQueue = undefined; + this._animationQueueSettings = undefined; + this._animationRunning = undefined; + this._animationSettings = undefined; + this._autofocus = undefined; + this._calcWidth = undefined; + this._calcHeight = undefined; + this._containsFlexGrow = undefined; + this._hasRenderedChildren = undefined; + this._effects = undefined; + this._fontFamily = undefined; + this._fontWeight = undefined; + this._id = undefined; + this._parent = undefined; + this._states = undefined; + this._style = undefined; + this._theme = undefined; + this._transition = undefined; + this._transitionLookup = undefined; + this._lastAnyKeyPressTime = undefined; + this._undoStyles = undefined; } get effects(): StyleEffects | undefined { From d1c496fe59e3c3b064d3f5604f0b3992ff9cec22 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 17:12:53 -0400 Subject: [PATCH 09/13] Revert "optimize settings styles before initial render" This reverts commit a0085fb723c477015e0ea22568744cc9c98ffeec. --- src/core/elementNode.ts | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index ffd2ea7..66a075b 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -225,14 +225,6 @@ export const LightningRendererNumberProps = [ 'zIndexLocked', ]; -/** - * Keys whose setter (pre-render) is equivalent to `this.lng[key] = value`. - * Used by the `style` setter's fast path to skip the prototype setter chain - * when applying styles before render. Built lazily after both prop arrays - * exist (see bottom of file). - */ -const fastPathStyleKeys = new Set(); - const LightningRendererNonAnimatingProps = [ 'absX', 'absY', @@ -1113,20 +1105,11 @@ export class ElementNode { this._style = style; - // Keys set in JSX are more important. Pre-render, plain animatable / - // non-animating props can be written directly to the lng props bag, - // bypassing the per-key setter chain. Special-meaning keys (shader, - // border, transition, states, etc.) still go through the setter. - const lng = this.lng as any; - const fastPath = !this.rendered; - for (const key in style) { - if (fastPath && fastPathStyleKeys.has(key)) { - if (lng[key] === undefined) { - lng[key] = (style as any)[key]; - } - } else if (this[key as keyof Styles] === undefined) { - // be careful of 0 values - this[key as keyof Styles] = style[key as keyof Styles]; + // Keys set in JSX are more important + for (const key in this._style) { + // be careful of 0 values + if (this[key as keyof Styles] === undefined) { + this[key as keyof Styles] = this._style[key as keyof Styles]; } } } @@ -1627,7 +1610,6 @@ export class ElementNode { } for (const key of LightningRendererNumberProps) { - fastPathStyleKeys.add(key); Object.defineProperty(ElementNode.prototype, key, { get(): number { return this.lng[key]; @@ -1639,7 +1621,6 @@ for (const key of LightningRendererNumberProps) { } for (const key of LightningRendererNonAnimatingProps) { - fastPathStyleKeys.add(key); Object.defineProperty(ElementNode.prototype, key, { get(): unknown { return this.lng[key]; From 31df39b43bd562781fd8cb55c450b9075d1f0855 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 17:31:05 -0400 Subject: [PATCH 10/13] Revert "optimize settings styles before initial render" This reverts commit a0085fb723c477015e0ea22568744cc9c98ffeec. --- src/core/elementNode.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 66a075b..3b4ad20 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -742,7 +742,18 @@ export class ElementNode { constructor(name: string) { this._type = name === 'text' ? NodeType.TextNode : NodeType.Element; this.rendered = false; - this.lng = {}; + // initialize lng with standard properties for v8 optimization + this.lng = { + w: undefined, + h: undefined, + x: undefined, + y: undefined, + alpha: undefined, + color: undefined, + shader: undefined, + clipping: undefined, + text: undefined, + }; this.children = []; // Initialize lazy underscore fields explicitly in a fixed order. This From 0cb635dce3f8b02481b1c21d994cf983c438d941 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 17:35:17 -0400 Subject: [PATCH 11/13] Revert "cache transition lookups for faster property setting" This reverts commit 149e2874babaf6886dd8d6d0046abd0bc95a320f. --- src/core/elementNode.ts | 85 ++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 3b4ad20..4cb314e 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -192,6 +192,12 @@ export function convertToShader( return renderer.createShader(type, v); } +function getPropertyAlias(name: string) { + if (name === 'w') return 'width'; + if (name === 'h') return 'height'; + return name; +} + export const LightningRendererNumberProps = [ 'alpha', 'color', @@ -309,11 +315,6 @@ export interface ElementNode extends RendererNode, FocusNode { _states?: States; _style?: Styles; _theme?: Styles; - _transition?: - | Record - | true - | false; - _transitionLookup?: Record; _lastAnyKeyPressTime?: number; _type: 'element' | 'textNode'; _undoStyles?: string[]; @@ -667,6 +668,16 @@ export interface ElementNode extends RendererNode, FocusNode { * @see https://lightning-tv.github.io/solid/#/flow/layout */ zIndex?: number; + /** + * Defines transitions for animatable properties. + * + * @see https://lightning-tv.github.io/solid/#/essentials/transitions?id=transitions-animations + */ + transition?: + | Record + | true + | false; + /** Optional handler for when the element is created and rendered. * * @see https://lightning-tv.github.io/solid/#/flow/ondestroy @@ -951,20 +962,27 @@ export class ElementNode { } _sendToLightningAnimatable(name: string, value: number) { - const t = this._transition; - if (t !== undefined && this.rendered && Config.animationsEnabled) { - // Fast path: single probe into a pre-aliased lookup, instead of probing - // both `transition[name]` and `transition[getPropertyAlias(name)]`. - const setting = - t === true ? true : (this._transitionLookup as any)?.[name]; - if (setting !== undefined) { - const animationSettings = setting === true ? undefined : setting; - return (this.lng as any).animateProp( - name, - value, - animationSettings || this.animationSettings || {}, - ); - } + if ( + this.transition && + this.rendered && + Config.animationsEnabled && + (this.transition === true || + this.transition[name] || + this.transition[getPropertyAlias(name)]) + ) { + const animationSettings = + this.transition === true || this.transition[name] === true + ? undefined + : this.transition[name] || + (this.transition[getPropertyAlias(name)] as + | undefined + | AnimationSettings); + + return (this.lng as any).animateProp( + name, + value, + animationSettings || this.animationSettings || {}, + ); } (this.lng[name as keyof (IRendererNode | INode)] as number | string) = @@ -1129,35 +1147,6 @@ export class ElementNode { return this._style || {}; } - set transition( - v: - | Record - | true - | false - | undefined, - ) { - this._transition = v; - if (v === undefined || v === true || v === false) { - this._transitionLookup = undefined; - return; - } - // Pre-build an aliased lookup so the hot path needs only a single probe. - // Only width/height have aliases (w/h); copy other keys verbatim. - const lookup: Record = {}; - for (const key in v) { - const val = v[key]; - if (val === undefined || val === false) continue; - lookup[key] = val as AnimationSettings | true; - if (key === 'width') lookup.w = val as AnimationSettings | true; - else if (key === 'height') lookup.h = val as AnimationSettings | true; - } - this._transitionLookup = lookup; - } - - get transition() { - return this._transition; - } - set theme(styles: Styles | undefined) { if (!styles) { return; From 178474504486e074772ff685aa6a321625d69c3c Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 17:42:17 -0400 Subject: [PATCH 12/13] remove extra props --- src/core/elementNode.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 4cb314e..56fa3ce 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -789,8 +789,6 @@ export class ElementNode { this._states = undefined; this._style = undefined; this._theme = undefined; - this._transition = undefined; - this._transitionLookup = undefined; this._lastAnyKeyPressTime = undefined; this._undoStyles = undefined; } From 082ac699648fd2d64b9b7bfba6580511ddd1a585 Mon Sep 17 00:00:00 2001 From: Chris Lorenzo Date: Sun, 10 May 2026 18:56:08 -0400 Subject: [PATCH 13/13] backwards compat with lightning renderer --- src/core/elementNode.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/elementNode.ts b/src/core/elementNode.ts index 56fa3ce..a30f7ad 100644 --- a/src/core/elementNode.ts +++ b/src/core/elementNode.ts @@ -976,6 +976,16 @@ export class ElementNode { | undefined | AnimationSettings); + // If the renderer doesn't support animateProp, + // keep backwards compatible with LightningRenderer + if (!('animateProp' in this.lng)) { + const animationController = this.animate( + { [name]: value }, + animationSettings, + ); + return animationController.start(); + } + return (this.lng as any).animateProp( name, value,