Measured nodes in a cached subtree grow by a rounding step on every layout pass

Aperta
#2,053 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@salmanmkc ci sta già lavorando.

Dal 30/10/2026.

  • #2054 di @salmanmkc — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
68/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
cpp, javascript, node.js
Ambito
frontend

Direzione di ricerca

Start by running the provided Node.js reproduction with Yoga 3.2.1 and compare it with the point-scale-factor-zero result. Trace how calculateLayout handles cached subtrees and pixel-grid rounding for measured nodes; the payload does not name source files or tests. Done when repeated layout passes leave unchanged measured-node heights at 28.8 and a regression test covers this cached-subtree case.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

With a point scale factor set, measured nodes inside a subtree that comes from the layout cache grow by a rounding step on many calculateLayout calls, and keep growing. In a UI where one node changes size every frame (a progress bar), text boxes grow until the text, centered in its box, drifts onto the next line.

Repro with yoga-layout 3.2.1 on Node 26 (macOS arm64):

// yoga-layout 3.2.1: measured nodes in a cached subtree grow on every layout pass.
import Yoga, {Edge, FlexDirection, PositionType} from 'yoga-layout';

const config = Yoga.Config.create();
config.setPointScaleFactor(100); // round to 0.01 layout units

const root = Yoga.Node.create(config);
root.setWidth(620);
root.setPadding(Edge.All, 24.3);

// Changing this sibling's width relays out the root, while the list
// below is served from the layout cache.
const bar = Yoga.Node.create(config);
bar.setHeight(8);
bar.setWidth(10);
root.insertChild(bar, 0);

const spacer = Yoga.Node.create(config);
spacer.setHeight(517.37);
root.insertChild(spacer, 1);

const anchor = Yoga.Node.create(config);
anchor.setHeight(50.8);
root.insertChild(anchor, 2);

const list = Yoga.Node.create(config);
list.setPositionType(PositionType.Absolute);
list.setPosition(Edge.Bottom, '110%');
anchor.insertChild(list, 0);

const labels = [];
for (let i = 0; i < 30; i++) {
  const row = Yoga.Node.create(config);
  row.setFlexDirection(FlexDirection.Row);
  row.setPadding(Edge.Vertical, 10);
  const label = Yoga.Node.create(config);
  label.setMeasureFunc(() => ({width: 70.37, height: 28.8}));
  row.insertChild(label, 0);
  list.insertChild(row, i);
  labels.push(label);
}

// Each label measures 28.8 tall; print the ones whose layout height grew.
const grown = () =>
  labels
    .map((label, i) => [i + 1, label.getComputedHeight()])
    .filter(([, h]) => h > 28.8 + 0.005)
    .map(([i, h]) => `#${i}=${h.toFixed(2)}`)
    .join(' ') || '(none)';

root.calculateLayout(undefined, undefined);
console.log('pass 0   ', grown());
for (let pass = 1; pass <= 600; pass++) {
  bar.setWidth(10 + (pass % 50));
  root.calculateLayout(undefined, undefined);
  if ([1, 10, 100, 600].includes(pass))
    console.log(`pass ${pass}`.padEnd(9), grown());
}

Output:

pass 0    (none)
pass 1    #1=28.81 #2=28.81 #3=28.81 #4=28.81 #5=28.81 #6=28.81 #7=28.81 #8=28.81 #9=28.81 #10=28.81 #11=28.81 #12=28.81 #13=28.81 #14=28.81 #15=28.81 #16=28.81 #17=28.81 #19=28.81 #20=28.81 #21=28.81 #22=28.81 #23=28.81 #24=28.81 #25=28.81 #26=28.81 #27=28.81 #28=28.81 #29=28.81 #30=28.81
pass 10   #1=28.82 #2=28.83 #3=28.85 #4=28.90 #5=28.90 #6=28.82 #7=28.83 #8=28.90 #9=28.90 #10=28.90 #11=28.90 #12=28.82 #13=28.81 #14=28.90 #15=28.86 #16=28.86 #17=28.82 #19=28.81 #20=28.82 #21=28.86 #22=28.86 #23=28.81 #24=28.86 #25=28.90 #26=28.90 #27=28.90 #28=28.81 #29=28.90 #30=28.92
pass 100  #1=28.82 #2=28.83 #3=28.85 #4=29.00 #5=29.00 #6=28.82 #7=28.83 #8=29.00 #9=29.00 #10=29.00 #11=29.00 #12=28.82 #13=28.81 #14=29.80 #15=28.86 #16=28.86 #17=28.82 #19=28.81 #20=28.82 #21=28.86 #22=28.86 #23=28.81 #24=28.86 #25=29.00 #26=29.00 #27=29.00 #28=28.81 #29=29.00 #30=29.99
pass 600  #1=28.82 #2=28.83 #3=28.85 #4=29.00 #5=29.00 #6=28.82 #7=28.83 #8=29.00 #9=29.00 #10=29.00 #11=29.00 #12=28.82 #13=28.81 #14=34.80 #15=28.86 #16=28.86 #17=28.82 #19=28.81 #20=28.82 #21=28.86 #22=28.86 #23=28.81 #24=28.86 #25=29.00 #26=29.00 #27=29.00 #28=28.81 #29=29.00 #30=35.95

Every label measures 28.8 tall and nothing in the list changes between passes. With setPointScaleFactor(0) all heights stay at 28.8.

My guess is that the pixel-grid rounding runs again over the cached subtree, which still holds the previous pass's rounded values. For measured nodes the top edge is floored and the bottom edge ceiled, so where float32 storage leaves a small residue in the absolute position, the height goes up one more step each time. Nodes without a measure function keep their size.

Found through @pmndrs/uikit, which sets a point scale factor of 100 (layout rounded to 0.01 units).

Lingua principale
C++
Stelle
18.9k
Fork
1.6k
Merge medio
2g 5h
PR unite (30g)
12

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di react/yoga

Tutte le issue di react/yoga

Issue simili

Altre issue su C++

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.