Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
76a5990
perf(render): diff and map commitupdate props in one pass
RuudBurger Oct 15, 2026
f9d6d82
perf(element): destroy a deleted subtree without detaching each desce…
RuudBurger Oct 15, 2026
334dd62
perf(element): skip node props on copy instead of deleting them
RuudBurger Oct 15, 2026
32050a8
perf(element): build node props in one object without a rest copy
RuudBurger Oct 15, 2026
037a0bf
perf(element): use prototype methods instead of per-instance closures
RuudBurger Oct 15, 2026
89a6e34
perf(flexbox): track elements with one shared observer instead of per…
RuudBurger Oct 15, 2026
645ad8f
perf(flexbox): skip style proxy reads in layout readback without a ra…
RuudBurger Oct 15, 2026
170bba5
perf(flexbox): read yoga display only when the style can change it
RuudBurger Oct 15, 2026
ec12c3e
perf(element): skip the style proxy for internal style reads and merges
RuudBurger Oct 15, 2026
4b2d6d9
perf(element): skip unchanged alpha and clipping writes to the node
RuudBurger Oct 15, 2026
b44eab2
perf(element): create the event emitter on first subscription
RuudBurger Oct 15, 2026
b8bad41
perf(react-native): track first layout with a shared observer instead…
RuudBurger Oct 15, 2026
b7312a4
perf(element): let the classname plugin skip style-only updates it ha…
RuudBurger Oct 15, 2026
458bd91
perf(css-transform): convert lone transform and opacity updates on th…
RuudBurger Oct 15, 2026
ee3d5da
perf(flexbox): skip the layout pass when a style changes nothing yoga…
RuudBurger Oct 15, 2026
3c3bdb8
perf(flexbox): skip clean roots and prune layout readback to nodes yo…
RuudBurger Oct 15, 2026
bce2ae6
perf(flexbox): seed the grow-only text width from the first layout so…
RuudBurger Oct 15, 2026
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
6 changes: 6 additions & 0 deletions .changeset/classname-style-fast-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@plextv/react-lightning': patch
'@plextv/react-native-lightning': patch
---

add an optional `needsTransform` plugin hook so a plugin can let style-only updates take the fast path per instance, and use it in the className plugin: style-only updates on elements without resolved class styles no longer go through the full transform pipeline
6 changes: 6 additions & 0 deletions .changeset/css-transform-fast-lane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@plextv/react-lightning': patch
'@plextv/react-lightning-plugin-css-transform': patch
---

add an optional `transformStyle` plugin hook so a plugin can convert the keys it handles inside the style fast path, and use it in css-transform for updates that only carry `transform` and `opacity` (reanimated's pushes), which used to go through the full transform pipeline
5 changes: 5 additions & 0 deletions .changeset/destroy-deleted-subtree-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

destroy each element once, and skip detaching descendants one by one when react deletes a whole subtree
6 changes: 6 additions & 0 deletions .changeset/element-lifecycle-observer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@plextv/react-lightning': patch
'@plextv/react-lightning-plugin-flexbox': patch
---

let plugins observe element lifecycle through one shared observer, and track flexbox elements with it instead of nine listeners per element
5 changes: 5 additions & 0 deletions .changeset/element-prototype-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

move per-element event, visibility and style proxy closures to the prototype
5 changes: 5 additions & 0 deletions .changeset/element-style-target-direct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

read and merge the element's own style target directly instead of through the style proxy
5 changes: 5 additions & 0 deletions .changeset/flexbox-display-read-on-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

only read a node's display back from yoga when the style could have changed it
5 changes: 5 additions & 0 deletions .changeset/flexbox-prune-readback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

skip clean flex roots and only read back nodes yoga laid out again
5 changes: 5 additions & 0 deletions .changeset/flexbox-readback-raw-first.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

check the raw style before reading the style proxy when applying layout positions
5 changes: 5 additions & 0 deletions .changeset/flexbox-single-mount-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

lay out a new text node once on mount instead of re-measuring it in a second pass
5 changes: 5 additions & 0 deletions .changeset/flexbox-skip-noop-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning-plugin-flexbox': patch
---

skip the layout pass when a style or text update changes nothing yoga reads
5 changes: 5 additions & 0 deletions .changeset/fused-commit-update-diff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

diff and map commitUpdate props in one pass, and skip diffing children on views and images
5 changes: 5 additions & 0 deletions .changeset/lazy-element-emitter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

create an element's event emitter on its first subscription
5 changes: 5 additions & 0 deletions .changeset/node-props-one-object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

map element props to node props in one object, without a rest copy
6 changes: 6 additions & 0 deletions .changeset/rn-loaded-layout-observer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@plextv/react-lightning': patch
'@plextv/react-native-lightning': patch
---

mark react-native elements loaded through a shared layout observer instead of a once listener per element
5 changes: 5 additions & 0 deletions .changeset/skip-node-props-without-delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

skip non-numeric sizes and a withheld alpha while copying props onto the node instead of deleting them first
5 changes: 5 additions & 0 deletions .changeset/skip-unchanged-alpha-clipping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@plextv/react-lightning': patch
---

skip rewriting an unchanged alpha or clipping on the node, which made the renderer recompute the whole subtree
80 changes: 65 additions & 15 deletions packages/plugin-css-transform/src/convertCSSStyleToLightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,70 @@ const CONVERTED_KEYS: ReadonlySet<string> = new Set([
'borderBottomEndRadius',
]);

function applyTransform(finalStyle: LightningElementStyle, transform: AllStyles['transform']) {
const { scaleX, scaleY, rotation, ...translateTransforms } = parseTransform(
transform as Parameters<typeof parseTransform>[0],
);

if (scaleX != null) {
finalStyle.scaleX = scaleX;
}

if (scaleY != null) {
finalStyle.scaleY = scaleY;
}

if (rotation != null) {
finalStyle.rotation = rotation;
}

finalStyle.transform = translateTransforms;
}

/**
* The common animated update (reanimated pushes a lone transform or opacity)
* converted without the full pass. Returns the same style as
* convertCSSStyleToLightning, or null when the style has any other key that
* function converts, so the caller falls back to it.
*/
export function convertTransformAndOpacity(
style: Record<string, unknown>,
): LightningElementStyle | null {
const finalStyle = {} as LightningElementStyle;
let opacity: unknown;
let transform: unknown;

for (const key in style) {
const value = style[key];

if (key === 'opacity') {
opacity = value;
} else if (key === 'transform') {
transform = value;
} else if (CONVERTED_KEYS.has(key) || key === 'display' || key === 'left' || key === 'top') {
return null;
} else if (value !== undefined) {
(finalStyle as Record<string, unknown>)[key] = value;
}
}

const partial = (style as Record<PropertyKey, unknown>)[PARTIAL_STYLE];

if (partial !== undefined) {
(finalStyle as Record<PropertyKey, unknown>)[PARTIAL_STYLE] = partial;
}

if (typeof opacity === 'number') {
finalStyle.alpha = opacity;
}

if (transform != null) {
applyTransform(finalStyle, transform as AllStyles['transform']);
}

return finalStyle;
}

export function convertCSSStyleToLightning(
style: AllStyleProps,
): LightningElementStyle | undefined {
Expand Down Expand Up @@ -292,21 +356,7 @@ export function convertCSSStyleToLightning(
}

if (transform != null) {
const { scaleX, scaleY, rotation, ...translateTransforms } = parseTransform(transform);

if (scaleX != null) {
finalStyle.scaleX = scaleX;
}

if (scaleY != null) {
finalStyle.scaleY = scaleY;
}

if (rotation != null) {
finalStyle.rotation = rotation;
}

finalStyle.transform = translateTransforms;
applyTransform(finalStyle, transform);
}

// Disabled for now as some components set overflow to hidden while not having their size correctly calculated
Expand Down
102 changes: 102 additions & 0 deletions packages/plugin-css-transform/src/convertTransformAndOpacity.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, expect, it } from 'vitest';

import { PARTIAL_STYLE } from '@plextv/react-lightning';

import {
convertCSSStyleToLightning,
convertTransformAndOpacity,
} from './convertCSSStyleToLightning';
import type { AllStyleProps } from './types/ReactStyle';

const transforms: unknown[] = [
[{ translateX: 10 }],
[{ translateY: -24.5 }],
[{ translateX: '50%' }],
[{ translateY: ' -100% ' }],
[{ translate: [4, 8] }],
[{ translate: 6 }],
[{ scale: 1.08 }],
[{ scale: '1.5' }],
[{ scaleX: 2 }],
[{ scaleY: 0.5 }],
[{ rotate: '45deg' }],
[{ rotate: '0.5turn' }],
[{ rotate: '100grad' }],
[{ rotate: '1rad' }],
[{ rotation: 0.25 }],
[{ matrix: [1, 0, 0, 1, 0, 0] }],
[{ translateX: 10 }, { scale: 1.1 }, { rotate: '90deg' }],
[{ scale: 1.1 }, { translateX: 10 }],
[{ scale: 2 }, { scaleX: 3 }],
[{ scaleX: 3 }, { scale: 2 }],
[{ translateX: 5 }, { translateX: 7 }],
[],
{ translateX: 12, scaleY: 1.2 },
'translateX(10px) scale(1.2) rotate(30deg)',
'translate(3, 4)',
];

const extras: Record<string, unknown>[] = [
{},
{ opacity: 0.5 },
{ opacity: 0 },
{ opacity: '0.5' },
{ opacity: undefined },
{ alpha: 0.3 },
{ alpha: 0.3, opacity: 0.6 },
{ scaleX: 4 },
{ x: 10, y: 20 },
{ pivot: 0.5 },
{ pivotX: 0, pivotY: 1 },
{ zIndex: 2, undefinedKey: undefined },
];

function cases(): Record<PropertyKey, unknown>[] {
const out: Record<PropertyKey, unknown>[] = [];

for (const extra of extras) {
out.push({ ...extra });
out.push({ ...extra, [PARTIAL_STYLE]: true });

for (const transform of transforms) {
out.push({ ...extra, transform });
out.push({ transform, ...extra, [PARTIAL_STYLE]: true });
}
}

return out;
}

function snapshot(style: object | null | undefined) {
return style
? { ...style, partial: (style as Record<PropertyKey, unknown>)[PARTIAL_STYLE] }
: style;
}

describe('convertTransformAndOpacity', () => {
it.each(cases())('matches convertCSSStyleToLightning for %o', (style) => {
const fast = convertTransformAndOpacity(style as Record<string, unknown>);

expect(fast).not.toBeNull();
expect(snapshot(fast)).toStrictEqual(
snapshot(convertCSSStyleToLightning(style as AllStyleProps)),
);
});

it.each([
{ opacity: 1, display: 'none' },
{ opacity: 1, display: 'flex' },
{ transform: [{ scale: 1 }], left: 10 },
{ transform: [{ scale: 1 }], top: '5' },
{ opacity: 1, backgroundColor: '#fff' },
{ opacity: 1, width: 10 },
{ opacity: 1, borderRadius: 4 },
{ opacity: 1, borderTopLeftRadius: 4 },
{ opacity: 1, overflow: 'hidden' },
{ opacity: 1, textAlign: 'center' },
{ opacity: 1, fontWeight: '600' },
{ opacity: 1, borderColor: 'red' },
])('leaves %o to the full conversion', (style) => {
expect(convertTransformAndOpacity(style)).toBeNull();
});
});
9 changes: 8 additions & 1 deletion packages/plugin-css-transform/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Plugin } from '@plextv/react-lightning';

import { convertCSSStyleToLightning } from './convertCSSStyleToLightning';
import {
convertCSSStyleToLightning,
convertTransformAndOpacity,
} from './convertCSSStyleToLightning';
import type { AllStyleProps } from './types/ReactStyle';

export { convertCSSStyleToLightning };
Expand Down Expand Up @@ -41,6 +44,10 @@ export function plugin(): Plugin {
return {
handledStyleProps: CSS_HANDLED_STYLE_PROPS,

transformStyle(_instance, style) {
return convertTransformAndOpacity(style as Record<string, unknown>);
},

transformProps(_instance, props) {
if (!('style' in props)) {
return props;
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin-flexbox/bench/baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"home": { "nodeCount": 875, "layoutPasses": 50, "settles": 25, "maxPassesToSettle": 2, "reflows": 0, "textRemeasures": 425 },
"details": { "nodeCount": 670, "layoutPasses": 40, "settles": 20, "maxPassesToSettle": 2, "reflows": 0, "textRemeasures": 326 },
"grid": { "nodeCount": 570, "layoutPasses": 60, "settles": 30, "maxPassesToSettle": 2, "reflows": 0, "textRemeasures": 180 },
"epg": { "nodeCount": 832, "layoutPasses": 64, "settles": 32, "maxPassesToSettle": 2, "reflows": 0, "textRemeasures": 512 }
"home": { "nodeCount": 875, "layoutPasses": 25, "settles": 25, "maxPassesToSettle": 1, "reflows": 0, "textRemeasures": 0 },
"details": { "nodeCount": 670, "layoutPasses": 20, "settles": 20, "maxPassesToSettle": 1, "reflows": 0, "textRemeasures": 0 },
"grid": { "nodeCount": 570, "layoutPasses": 30, "settles": 30, "maxPassesToSettle": 1, "reflows": 0, "textRemeasures": 0 },
"epg": { "nodeCount": 832, "layoutPasses": 32, "settles": 32, "maxPassesToSettle": 1, "reflows": 0, "textRemeasures": 0 }
}
Loading
Loading