react-x11 2.52.0 → 2.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/docs/README.md +160 -314
- package/docs/accessibility.md +176 -229
- package/docs/appearance.md +77 -176
- package/docs/click-to-component.md +18 -51
- package/docs/clipboard.md +40 -59
- package/docs/components.md +518 -1036
- package/docs/dbus.md +61 -133
- package/docs/debugging.md +152 -245
- package/docs/desktop-calendar.md +51 -88
- package/docs/desktop.md +222 -347
- package/docs/devtools.md +50 -167
- package/docs/drag-and-drop.md +126 -204
- package/docs/ecosystem/animation.md +91 -125
- package/docs/ecosystem/data-fetching.md +38 -53
- package/docs/ecosystem/dev-tooling.md +159 -202
- package/docs/ecosystem/forms.md +61 -97
- package/docs/ecosystem/headless.md +64 -89
- package/docs/ecosystem/i18n.md +18 -21
- package/docs/ecosystem/icons.md +93 -126
- package/docs/ecosystem/layout.md +42 -52
- package/docs/ecosystem/routing.md +18 -29
- package/docs/ecosystem/state.md +71 -88
- package/docs/ecosystem/testing.md +131 -157
- package/docs/ecosystem/theming.md +119 -165
- package/docs/ecosystem.md +145 -179
- package/docs/elements.md +1091 -1774
- package/docs/embedding.md +83 -132
- package/docs/events.md +412 -695
- package/docs/extending.md +784 -1326
- package/docs/eyedropper.md +63 -122
- package/docs/filedialog.md +71 -107
- package/docs/frame.md +195 -339
- package/docs/gl.md +65 -62
- package/docs/globalmenu.md +63 -146
- package/docs/macos.md +531 -3234
- package/docs/notifications.md +33 -47
- package/docs/packaging.md +166 -309
- package/docs/permissions.md +9 -14
- package/docs/react-features.md +81 -106
- package/docs/remote.md +32 -76
- package/docs/scale.md +72 -186
- package/docs/styling.md +329 -547
- package/docs/system.md +200 -280
- package/docs/testing.md +63 -83
- package/docs/typescript.md +6 -48
- package/docs/uri-schemes.md +67 -107
- package/docs/wayland-backend.md +200 -459
- package/docs/windows-integrations.md +75 -131
- package/docs/windows.md +343 -1367
- package/package.json +3 -2
- package/src/Reconciler.js +2 -2
- package/src/a11y.js +1 -1
- package/src/backend/context2d.js +3 -3
- package/src/bootstrap.js +1 -1
- package/src/cocoa/app.js +6 -6
- package/src/cocoa/bezels.js +1 -1
- package/src/cocoa/fonts.js +1 -1
- package/src/cocoa/globalmenu.js +1 -1
- package/src/cocoa/keymap.js +1 -1
- package/src/cocoa/panewindow.js +1 -1
- package/src/cocoa/player.js +1 -1
- package/src/cocoa/presenter.js +2 -2
- package/src/cocoa/promotion.js +1 -1
- package/src/cocoa/relaunch.js +1 -1
- package/src/cocoa/threaded.js +1 -1
- package/src/cocoa/video.js +1 -1
- package/src/cocoa/window.js +4 -4
- package/src/components/Menu.js +7 -2
- package/src/components/Switch.js +1 -1
- package/src/components/native.js +1 -1
- package/src/dnd.js +2 -2
- package/src/glnodes.js +1 -1
- package/src/grid.js +1 -1
- package/src/layouts.js +1 -1
- package/src/nodes/animation.js +1 -1
- package/src/nodes/invalidate.js +1 -1
- package/src/nodes/layouthost.js +1 -1
- package/src/nodes/video.js +19 -8
- package/src/nodes/window/droptarget.js +1 -1
- package/src/pacing.js +1 -1
- package/src/refresh/index.js +2 -1
- package/src/types/video.d.ts +5 -2
- package/src/videobackend.js +28 -0
- package/src/videoframes.js +29 -16
- package/src/wayland/app.js +27 -0
- package/src/wayland/video.js +376 -0
- package/src/win32/a11y.js +1 -1
- package/src/win32/app.js +2 -2
- package/src/win32/fonts.js +1 -1
- package/src/win32/glarea.js +4 -4
- package/src/win32/overlay.js +1 -1
- package/src/win32/panewindow.js +3 -3
- package/src/win32/shell.js +1 -1
- package/src/win32/window.js +2 -2
- package/src/windowid.js +1 -1
- package/src/xvideo.js +189 -0
- package/docs/architecture/accessibility.md +0 -470
- package/docs/architecture/animation.md +0 -707
- package/docs/architecture/container-queries.md +0 -525
- package/docs/architecture/custom-layout.md +0 -334
- package/docs/architecture/drag-and-drop.md +0 -941
- package/docs/architecture/element-layer-contents.md +0 -622
- package/docs/architecture/frame-pacing.md +0 -255
- package/docs/architecture/grid-layout.md +0 -358
- package/docs/architecture/protocol-efficiency.md +0 -357
- package/docs/architecture/video.md +0 -1149
- package/docs/architecture/windowed-regions.md +0 -854
- package/docs/glx.md +0 -137
- package/docs/wayland.md +0 -545
- package/docs/windows-embedding.md +0 -271
- package/docs/windows-gl.md +0 -221
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
# Frame pacing: a budget over paint time, not a count of updates
|
|
2
|
-
|
|
3
|
-
_Design record, 2026-09-07. Written against master at 25d6869 (react-x11
|
|
4
|
-
2.8.3, ntk ^8.7.0, `@windowkit/appkit` ^0.6.0), for the `frameRate` prop
|
|
5
|
-
that shipped with it. The user-facing account is
|
|
6
|
-
[elements.md](../elements.md#framerate--pacing-the-frames-under-a-flood);
|
|
7
|
-
this is why it is shaped the way it is, where it sits, and what it
|
|
8
|
-
measured. The problem was found in `@react-x11/components`'s vt terminal
|
|
9
|
-
(sidorares/react-x11-components#69, "PRD: adaptive frame pacing"), whose
|
|
10
|
-
§6 lists what core would have to grow; this document is core's half of it,
|
|
11
|
-
made generic._
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 0. TL;DR
|
|
16
|
-
|
|
17
|
-
- A window fed off an input path — a terminal under `cat`, a chart on a
|
|
18
|
-
socket, a simulation, a scene drawing as fast as it can — claims a repaint
|
|
19
|
-
every time its data moves. The frame clock decides _when_ a frame may go
|
|
20
|
-
out and says nothing about whether it is worth painting, so on a 120Hz
|
|
21
|
-
panel such a window paints 120 times a second. Where a frame costs
|
|
22
|
-
milliseconds of the JS thread (full-window CoreGraphics on macOS), the
|
|
23
|
-
thread paints screens nobody sees and the producer gets what is left:
|
|
24
|
-
measured on a streaming terminal, 60% of wall time in paint and a flood
|
|
25
|
-
four times slower on macOS than under XQuartz on the same machine.
|
|
26
|
-
- The fix prices frames in **CPU time**, not in updates: a token bucket
|
|
27
|
-
over paint time. Credit accrues at `budget` per millisecond of wall time
|
|
28
|
-
up to a burst; a frame spends what it measurably cost; a claim that finds
|
|
29
|
-
the bucket in debt waits for it to reach zero and no longer. Idle is
|
|
30
|
-
immediate, cheap is unthrottled, one expensive frame among cheap ones is
|
|
31
|
-
free, and a flood that ends is un-throttled on its next claim.
|
|
32
|
-
- It lives **above both frame clocks**, in `WindowNode._scheduleFrame` and
|
|
33
|
-
`GlAreaNode.requestFrame`: a held claim delays the `requestAnimationFrame`
|
|
34
|
-
request, and the clock — ntk's fence or vertical blank on X11, the display
|
|
35
|
-
period on macOS — still gates the frame itself. Nothing changes what a
|
|
36
|
-
frame paints.
|
|
37
|
-
- **Opt-in.** `'display'` — every frame the clock gives, after React's own
|
|
38
|
-
batching — is the default; `'adaptive'` is what a streaming window asks
|
|
39
|
-
for, on the `<window>` or `<glarea>`, for a root, or from
|
|
40
|
-
`REACT_X11_FRAME_RATE`.
|
|
41
|
-
- Beside it, `Node.opaqueRect()`: an element that composites an opaque
|
|
42
|
-
bitmap over its box says so, and a pass inside it skips the fills that
|
|
43
|
-
were under it — the other fifth of the terminal's frame.
|
|
44
|
-
|
|
45
|
-
## 1. The problem, measured
|
|
46
|
-
|
|
47
|
-
The components PRD's numbers, on an M1 Pro with a 120Hz panel, a 1000×700
|
|
48
|
-
window at 2×, a 125×45 terminal grid and 300,000 distinct lines `cat`-ed
|
|
49
|
-
into its pty:
|
|
50
|
-
|
|
51
|
-
| variant | flood | paints/s | paint share |
|
|
52
|
-
| --------------------------------------------- | -----: | -------: | ----------: |
|
|
53
|
-
| Cocoa, defaults | 3.50 s | 110 | 60% |
|
|
54
|
-
| Cocoa, `cocoa: { frameInterval: 33.3 }` | 1.08 s | 34 | 21% |
|
|
55
|
-
| X11 (XQuartz), defaults | 0.85 s | 35 | 7% |
|
|
56
|
-
| Cocoa, an element-owned pacer at a 25% budget | 0.95 s | 35 | 20% |
|
|
57
|
-
|
|
58
|
-
Three facts fall out of it. **Frame count is the lever**: both backends
|
|
59
|
-
run the clock at the display's period, but the X11 frame is _fenced_ — ntk
|
|
60
|
-
does not start the next until the server has consumed the last, and the
|
|
61
|
-
server takes ~28ms to composite a screenful of glyphs, so the terminal
|
|
62
|
-
paints 35 times a second by backpressure. The Cocoa backend has no
|
|
63
|
-
equivalent: its "server" is CoreGraphics on the JS thread and the frame is
|
|
64
|
-
due whenever the display is. **Per-frame cost is full-area memory passes,
|
|
65
|
-
not glyphs**: of a 6ms Cocoa frame, 2.0ms is the cell backgrounds, 1.7ms
|
|
66
|
-
the surface-to-window composite, 0.95ms the swapchain catch-up copy,
|
|
67
|
-
0.7ms two background fills under the opaque surface — and 0.26ms the
|
|
68
|
-
glyphs. **A fixed cap is the wrong tool**: `frameInterval: 33.3` recovers
|
|
69
|
-
most of the gap and costs every window on the root its 120Hz, including
|
|
70
|
-
the ones whose frames are cheap.
|
|
71
|
-
|
|
72
|
-
## 2. The rule
|
|
73
|
-
|
|
74
|
-
The PRD's rule was "defer a claim until `lastPaintEnd + cost × (1/budget −
|
|
75
|
-
1)`", from the last frame's cost. That is the right steady state and the
|
|
76
|
-
wrong transient: it holds the frame after a single expensive one — the one
|
|
77
|
-
relayout in a blit-scrolled list — for three times its cost, on a scroll
|
|
78
|
-
that should stay at 120Hz because everything else in it is a memmove and a
|
|
79
|
-
strip. The user's requirement was exactly that case: _budget CPU price,
|
|
80
|
-
not the number of updates; a scroll that is mostly blits stays at 120Hz
|
|
81
|
-
with rare repaints._
|
|
82
|
-
|
|
83
|
-
The token bucket is the same rule with a burst (`src/pacing.js`,
|
|
84
|
-
`FramePacer`):
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
credit += budget × (now − lastAccrual), capped at burst
|
|
88
|
-
frame: credit −= cost // what the flush measured
|
|
89
|
-
claim: wait = credit ≥ 0 ? 0 : −credit / budget
|
|
90
|
-
wait = min(wait, lastEnd + 1000/minFps − now) // the floor
|
|
91
|
-
wait = max(wait, lastStart + 1000/maxFps − now) // the ceiling
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
- With the bucket at zero, a frame of cost `c` leaves it at `−c(1 −
|
|
95
|
-
budget)` and the next claim waits `c(1/budget − 1)` — the PRD's rule, in
|
|
96
|
-
steady state under a flood. Over a long stream the paint share converges
|
|
97
|
-
on the budget exactly (`test/pacing.test.js`, "converges on the budget").
|
|
98
|
-
- The **burst** is what absorbs the rare expensive frame: a single frame up
|
|
99
|
-
to the burst never earns a wait, and only a stream drains it. The burst
|
|
100
|
-
is the floor's interval (50ms at `minFps: 20`), 50ms without a floor —
|
|
101
|
-
one frame up to the longest wait the pacer may impose is always free.
|
|
102
|
-
- The debt is bounded by one frame's cost, so a flood that ends is
|
|
103
|
-
un-throttled after that one wait. An average would have kept throttling
|
|
104
|
-
the prompt that appears when the flood stops.
|
|
105
|
-
- Waits under a millisecond are not armed: a timer cannot keep them and a
|
|
106
|
-
clock tick would swallow them. The debt carries to the next claim, so a
|
|
107
|
-
run of sub-millisecond frames claimed back to back still pays once it is
|
|
108
|
-
worth a timer, and a cheap frame after a flood lands on the tick it would
|
|
109
|
-
have landed on anyway.
|
|
110
|
-
|
|
111
|
-
**Cost is the JS thread's time.** The flush — layout, the paint passes, the
|
|
112
|
-
requests or the CoreGraphics work — bracketed in `WindowNode.flush`, plus on
|
|
113
|
-
macOS the present that runs after it (`CocoaWindow.present` reports the
|
|
114
|
-
flip and its catch-up copy back to the node). Not the server's: on X11 the
|
|
115
|
-
fence already paces to that, and the pacer is deliberately inert where a
|
|
116
|
-
backend has backpressure of its own. The PRD's Q5 asked whether a core seam
|
|
117
|
-
reporting the whole flush's cost would make the rule exact on both
|
|
118
|
-
backends; the answer is that the thread's time _is_ the cost the rule is
|
|
119
|
-
about, because it is what the producer is starved of.
|
|
120
|
-
|
|
121
|
-
## 3. Where it lives, and what it composes with
|
|
122
|
-
|
|
123
|
-
The pacer is per frame source: one on each `WindowNode` (popups included)
|
|
124
|
-
and one on each `GlAreaNode`, whose frames run on a clock of their own.
|
|
125
|
-
`_scheduleFrame` asks it before requesting the window's callback; a claim
|
|
126
|
-
raised by the frame on itself — an animation stepping, a container query
|
|
127
|
-
settling, a promotion moving a node — is answered once the frame is over,
|
|
128
|
-
so the frame that raised it is the one priced.
|
|
129
|
-
|
|
130
|
-
- **The clocks.** A held claim delays the `requestAnimationFrame` request
|
|
131
|
-
by the wait; the clock then gates the frame as it always did. On macOS a
|
|
132
|
-
request between two pump ticks used to wait for the next tick to look at
|
|
133
|
-
it, up to a pump interval on top of the wait; `CocoaApp._requestFrame`
|
|
134
|
-
now arms the same one-shot a tick arms for a frame it just missed, at the
|
|
135
|
-
moment the clock is due, so a paced claim lands at its wait rather than
|
|
136
|
-
its wait rounded up to the pump. On X11 ntk's clock is untouched.
|
|
137
|
-
- **Discrete input.** A click or a key paints the window at once
|
|
138
|
-
(`flushPendingFrames`, [events.md](../events.md)), held claims included —
|
|
139
|
-
a held claim is damage like any other, and the early flush stands the
|
|
140
|
-
wait down. The debt is paid then. Answering the input is never held.
|
|
141
|
-
- **Animations.** A transition's frames go through the same claims and are
|
|
142
|
-
paced like any other; at the default nothing changes, and under
|
|
143
|
-
`'adaptive'` cheap transition frames never wait. A layer promotion's
|
|
144
|
-
animations run in the render server and schedule no frames at all.
|
|
145
|
-
- **Hidden and occluded windows** schedule no frames; the bucket refills
|
|
146
|
-
while they wait, so the catch-up frame is immediate.
|
|
147
|
-
- **`cocoa: { frameInterval }`** is still the clock's own cap on macOS and
|
|
148
|
-
applies to everything on the clock; `frameRate` is the policy above it,
|
|
149
|
-
per window, on both backends. `maxFps` is the cap an app should write.
|
|
150
|
-
|
|
151
|
-
## 4. The API
|
|
152
|
-
|
|
153
|
-
`frameRate` on `<window>`, `<popup>` and `<glarea>`; `createRoot({
|
|
154
|
-
frameRate })` for a root's default; `REACT_X11_FRAME_RATE` over both. A
|
|
155
|
-
preset name, a number (a ceiling and nothing else), or `{ budget, minFps,
|
|
156
|
-
maxFps }` with the ones left out taken from `'display'`. A `<glarea>` takes
|
|
157
|
-
its window's policy unless it names one. Resolved in `src/pacing.js`
|
|
158
|
-
(`resolveFrameRate`, `resolveFramePolicy`), validated at the call that
|
|
159
|
-
passed the value — a bad root option fails at `createRoot`, a bad prop at
|
|
160
|
-
the window — with the choices in the error.
|
|
161
|
-
|
|
162
|
-
Decided, and why (the PRD's §11, answered for core):
|
|
163
|
-
|
|
164
|
-
- **The name** is `frameRate`: it is what an author thinks in, and the
|
|
165
|
-
numeric form reads as a cap.
|
|
166
|
-
- **The environment overrides the prop**, as `REACT_X11_BACKEND` does: an
|
|
167
|
-
A/B run and a field diagnosis must not need a code change in every app.
|
|
168
|
-
- **A number is a ceiling over the default**, not over `'adaptive'`. With
|
|
169
|
-
`'display'` the default, `frameRate={30}` has to mean "never more than
|
|
170
|
-
30" and nothing else; adaptive-and-capped is spelled with the numbers.
|
|
171
|
-
- **The default is `'display'`.** A UI answers its input as quickly as it
|
|
172
|
-
can unless it says otherwise; the pacer is what a window that streams
|
|
173
|
-
asks for. It costs the default path one property read per claim.
|
|
174
|
-
- **Several windows pace themselves.** Four floods at once can take four
|
|
175
|
-
budgets; a shared budget is a process-level clock and is not built.
|
|
176
|
-
|
|
177
|
-
Observability: `REACT_X11_TRACE=requests` prints `waited=` on every frame
|
|
178
|
-
the pacer held ([debugging.md](../debugging.md)), the `frame` trace hook
|
|
179
|
-
carries it, and the pacer's counters — frames, held, coalesced, the last
|
|
180
|
-
cost and the last wait — are on the node for a bench to print.
|
|
181
|
-
|
|
182
|
-
## 5. The opaque-content hint
|
|
183
|
-
|
|
184
|
-
Of the terminal's 6ms frame, 0.7ms was two background fills under a
|
|
185
|
-
surface that covers every pixel of them, and on X11 the same fills are
|
|
186
|
-
composites the server ran for nothing. Core had no way to know.
|
|
187
|
-
`Node.opaqueRect()` is the element's word: the rect it writes opaque pixels
|
|
188
|
-
over on every paint, in whole pixels; a pass inside it is painted without
|
|
189
|
-
the clear, the window's background, and the backgrounds of the node and its
|
|
190
|
-
ancestors. Clipping ancestors shrink the cover to what reaches the surface,
|
|
191
|
-
a rounded one by its radius all round. The element claims its damage as a
|
|
192
|
-
rect inside the answer, because a node claim carries a pixel of slop that
|
|
193
|
-
lies outside it ([extending.md](../extending.md#an-element-that-covers-its-box)).
|
|
194
|
-
Pinned by `test/opaque-content.test.js`: the fills skipped are exactly
|
|
195
|
-
those, and a covered pass leaves the pixels a full repaint leaves.
|
|
196
|
-
|
|
197
|
-
## 6. What is not here, and why
|
|
198
|
-
|
|
199
|
-
Two of the PRD's four seams need the native bridge first, and the third
|
|
200
|
-
needs a design of its own. Each is filed rather than folklore:
|
|
201
|
-
|
|
202
|
-
- **`globalCompositeOperation` on the Cocoa context, with a memcpy for
|
|
203
|
-
`'copy'`** (the 1.7ms surface-to-window composite). `@windowkit/appkit`
|
|
204
|
-
0.6.0 has no blend-mode verb, and its one memcpy, `copySurfaceRegion`,
|
|
205
|
-
refuses surfaces of unequal size — it is the swapchain's catch-up copy,
|
|
206
|
-
not a blit. Both verbs are the bridge's to grow; the property is declared
|
|
207
|
-
optional on `Context2D` in the meantime, so an element asks for it
|
|
208
|
-
rather than assumes it.
|
|
209
|
-
- **Element-owned layer contents** (`presentSurface()` on a node over an
|
|
210
|
-
IOSurface-backed `Surface`, so the terminal's surface _is_ its layer and
|
|
211
|
-
the present and the swapchain copy disappear). The bridge has the
|
|
212
|
-
primitives — `createSurfaceIOSurface`, `setLayerContentsIOSurface` — but
|
|
213
|
-
a layer showing an IOSurface the element then draws into again tears; it
|
|
214
|
-
needs a two-buffer chain of its own, a flip on present, and the promotion
|
|
215
|
-
policy of `src/cocoa/promotion.js` extended to "this node presents a
|
|
216
|
-
surface" with the same z-order rule. A design document first.
|
|
217
|
-
- **An adaptive tick-skip in the Cocoa clock.** Not needed: with the pacer
|
|
218
|
-
above the clock the clock has no tick to skip — a window that is holding
|
|
219
|
-
a claim has no callback queued.
|
|
220
|
-
|
|
221
|
-
## 7. Measured
|
|
222
|
-
|
|
223
|
-
`npm run bench:presenters -- --scenario=stream` is the flood in core's own
|
|
224
|
-
terms: a window-sized element that answers `opaqueRect()`, painting a
|
|
225
|
-
120×40 grid of cell fills, claimed every 2ms from a timer — the producer —
|
|
226
|
-
with `--frame-rate` choosing the policy. Read `cpu` and the paint share
|
|
227
|
-
against `claims/s`, which is how often the producer got the thread.
|
|
228
|
-
|
|
229
|
-
On the M1 Pro's 120Hz panel, a 900×700 window at 2× (the stream pane is
|
|
230
|
-
the window less a header), six seconds a cell, `--columns=surface`:
|
|
231
|
-
|
|
232
|
-
| `--frame-rate` | fps | flush avg | paint share of wall | cpu | producer: claims/s |
|
|
233
|
-
| -------------- | ----: | --------: | ------------------: | --: | -----------------: |
|
|
234
|
-
| `display` | 120.0 | 4.84ms | 58% | 72% | 244 |
|
|
235
|
-
| `adaptive` | 36.1 | 5.79ms | 21% | 27% | 529 |
|
|
236
|
-
| `30` | 29.7 | 6.47ms | 19% | 24% | 539 |
|
|
237
|
-
| `throughput` | 8.8 | 10.26ms | 9% | 13% | 592 |
|
|
238
|
-
|
|
239
|
-
Read across a row. At the default the window paints every refresh and the
|
|
240
|
-
thread spends 58% of its time doing it; the producer, a 2ms timer that
|
|
241
|
-
would fire 500 times a second on an idle thread, fires 244 — the flood is
|
|
242
|
-
starving the thing that feeds it, which is the whole finding. Under
|
|
243
|
-
`'adaptive'` the pacer holds 330 of 341 claims (the first eleven are the
|
|
244
|
-
burst), paints 36 times a second, keeps paint at the budget — 21% where
|
|
245
|
-
the rule says 25%, the difference being the clock rounding each wait up to
|
|
246
|
-
the next tick — and the producer gets 529 of its 500-per-second, which is
|
|
247
|
-
to say all of it. The cap at 30 lands where it says. `'throughput'` is the
|
|
248
|
-
other end: 9% of the thread, one frame every 100ms. The per-frame cost
|
|
249
|
-
rises as frames get rarer (a cold cache, a core the OS clocks down between
|
|
250
|
-
frames), which is why the row is priced in share and not in milliseconds.
|
|
251
|
-
|
|
252
|
-
What the table does not show is as important: a scroll under `'adaptive'`
|
|
253
|
-
paints at 120Hz like the default, because its frames cost a fraction of a
|
|
254
|
-
millisecond (`test/frame-pacing.test.js`, "cheap frames never wait"), and
|
|
255
|
-
a click during the flood is answered on the click.
|
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
# Grid layout: CSS grid on the layout seam
|
|
2
|
-
|
|
3
|
-
_Design record, 2026-09-11. It began the same day as a feasibility study
|
|
4
|
-
with a prototype, on the branch that added the layout seam
|
|
5
|
-
([custom-layout.md](custom-layout.md)); this is the record of what was
|
|
6
|
-
built from it. Measured in the in-process X server with real text shaping
|
|
7
|
-
(KaTeX Main), on one machine (Apple M1 Pro, node 26), and against Chrome 152. The outside facts in §8 are as of the date and linked; they will
|
|
8
|
-
drift._
|
|
9
|
-
|
|
10
|
-
_[styling.md](../styling.md#grid) is the reference for the style
|
|
11
|
-
properties; this is why they are the shape they are._
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## 0. TL;DR
|
|
16
|
-
|
|
17
|
-
- **What:** `display: 'grid'` lays a box's children out by CSS Grid's
|
|
18
|
-
placement algorithm (css-grid-2 §8.5) and its track sizing algorithm
|
|
19
|
-
(§12), inside the layout pass, as a layout on the seam. `layout: 'grid'`
|
|
20
|
-
is the same request; a box whose `display` and `layout` disagree is
|
|
21
|
-
reported and laid out as flexbox.
|
|
22
|
-
- **The style is CSS's, and flat** (§2): `gridTemplateColumns`,
|
|
23
|
-
`gridTemplateRows`, `gridTemplateAreas`, `gridAutoColumns`,
|
|
24
|
-
`gridAutoRows`, `gridAutoFlow` and `justifyItems` on the box; `gridColumn`,
|
|
25
|
-
`gridRow`, `gridArea` and `justifySelf` on a child. The names every
|
|
26
|
-
CSS-in-JS library and React DOM write, and the ones yoga's own grid uses.
|
|
27
|
-
- **Held to Chrome** (§4): a 400-case corpus Chrome laid out is part of the
|
|
28
|
-
test suite and lays out here within a pixel of it, but for one case where
|
|
29
|
-
Chrome parts ways with the spec and this grid keeps to the spec.
|
|
30
|
-
- **Cheaper than the flexbox that does the nearest thing** on every frame
|
|
31
|
-
measured (§3) — 300 cards: mount 59 ms against 63, one card's text
|
|
32
|
-
changing 2.1 ms against 13.1, a live resize 20.3 ms against 25.8.
|
|
33
|
-
- **Left out** (§5): subgrid, which this seam cannot carry; baselines;
|
|
34
|
-
named lines; the `grid` and `grid-template` shorthands; the second column
|
|
35
|
-
pass.
|
|
36
|
-
|
|
37
|
-
## 1. What grid is for here
|
|
38
|
-
|
|
39
|
-
Flexbox lines things up in one direction. The layouts an app here reached
|
|
40
|
-
for and could not write:
|
|
41
|
-
|
|
42
|
-
- **A form.** Labels in a column as wide as the widest label, fields taking
|
|
43
|
-
the rest. Flexbox did it with a fixed label width, which a translation
|
|
44
|
-
breaks, or by measuring in an effect, a frame late
|
|
45
|
-
([custom-layout.md](custom-layout.md#1-why-the-core)).
|
|
46
|
-
- **A dashboard.** Tiles spanning columns and rows, packed densely.
|
|
47
|
-
- **A gallery.** As many columns as fit, each row as tall as its tallest
|
|
48
|
-
item. `flexWrap` nearly does it, but the last row's items stretch across
|
|
49
|
-
the whole width.
|
|
50
|
-
|
|
51
|
-
Those are the three cases measured in §3.
|
|
52
|
-
|
|
53
|
-
## 2. API
|
|
54
|
-
|
|
55
|
-
```jsx
|
|
56
|
-
<box
|
|
57
|
-
style={{ display: 'grid', gridTemplateColumns: 'auto 1fr', columnGap: 12 }}
|
|
58
|
-
>
|
|
59
|
-
<text>Name</text>
|
|
60
|
-
<textinput />
|
|
61
|
-
<box style={{ gridColumn: '1 / -1' }} />
|
|
62
|
-
</box>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Flat, and CSS's names
|
|
66
|
-
|
|
67
|
-
The study proposed the shape a registered layout has —
|
|
68
|
-
`layout: { name: 'grid', columns: 'auto 1fr' }` on the box and
|
|
69
|
-
`layoutItem: { column: 'span 2' }` on a child — and it read as heavy,
|
|
70
|
-
because it is the one thing nobody else does. The prior art (§8) splits in
|
|
71
|
-
two:
|
|
72
|
-
|
|
73
|
-
- **Style objects use CSS's own names, flat.** Objects in emotion and
|
|
74
|
-
styled-components, StyleX, vanilla-extract, MUI's `sx`, Chakra's and
|
|
75
|
-
Panda's style props, and React DOM's `style` all write
|
|
76
|
-
`display: 'grid', gridTemplateColumns: '…'` and `gridColumn: 'span 2'`.
|
|
77
|
-
None of them shortens anything inside the object. Yoga's own grid,
|
|
78
|
-
unreleased, names its style `gridTemplateColumns` too, and CSS's masonry
|
|
79
|
-
shipped as `display: grid-lanes`, reusing grid's properties.
|
|
80
|
-
- **What is short lives a layer up**: Tailwind's `grid-cols-3` and
|
|
81
|
-
`col-span-2`, or a component's props — Radix's `<Grid columns="3">`,
|
|
82
|
-
Chakra's `<SimpleGrid columns={3}>`, Panda's `grid({ columns: 3 })`.
|
|
83
|
-
|
|
84
|
-
So the style here is CSS's longhands, flat. A grid copied from a React DOM
|
|
85
|
-
codebase lays out as written; a style array or a query block can change one
|
|
86
|
-
property without restating the rest — `flattenStyle` replaces a `layout`
|
|
87
|
-
object whole, so with the options bag a block that changed the columns had
|
|
88
|
-
to restate the name and the areas too; and the day yoga's grid ships,
|
|
89
|
-
handing it the work changes no style. The terse spelling belongs in the
|
|
90
|
-
class-string layer the compact-styles prototype proposes, where Tailwind's
|
|
91
|
-
names carry over as they are.
|
|
92
|
-
|
|
93
|
-
**`display: 'grid'`, and `layout: 'grid'` beside it.** Every system
|
|
94
|
-
above turns grid on with `display` — CSS, Tailwind's `grid` class, React
|
|
95
|
-
DOM, yoga's `Display.Grid`, `display: grid-lanes` — so `display` does here
|
|
96
|
-
too. `layout` stays the name every layout goes by, the Houdini role
|
|
97
|
-
(`display: layout(name)` in CSS), and `layout: 'grid'` asks for the same
|
|
98
|
-
grid. They disagree when `display: 'grid'` stands beside another layout, or
|
|
99
|
-
`layout: 'grid'` beside `display: 'flex'`; the box is reported and laid out
|
|
100
|
-
as flexbox until they agree (`layoutOf`, src/layouts.js). `display: 'flex'`
|
|
101
|
-
beside `masonry` is no disagreement: it says the box is shown, which is all
|
|
102
|
-
`display` said before grid was one of its values, and the docs' own
|
|
103
|
-
`'@container …': { display: 'flex' }` to show a box again relies on it.
|
|
104
|
-
|
|
105
|
-
A registered layout keeps `layout: { name, … }` and `layoutItem`: it cannot
|
|
106
|
-
claim global style names. That is the asymmetry flexbox already has, whose
|
|
107
|
-
properties are flat because they are CSS's.
|
|
108
|
-
|
|
109
|
-
### The values
|
|
110
|
-
|
|
111
|
-
| on the box | CSS | |
|
|
112
|
-
| ----------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------- |
|
|
113
|
-
| `gridTemplateColumns`, `gridTemplateRows` | `grid-template-*` | px (logical), %, fr, `auto`, `min-content`, `max-content`, `minmax()`, `fit-content()`, `repeat()` |
|
|
114
|
-
| `gridTemplateAreas` | `grid-template-areas` | one quoted string per row, or an array of the rows |
|
|
115
|
-
| `gridAutoColumns`, `gridAutoRows` | `grid-auto-*` | the sizes an implicit track takes, in turn |
|
|
116
|
-
| `gridAutoFlow` | `grid-auto-flow` | `row`, `column`, and `dense` |
|
|
117
|
-
| `justifyItems` | `justify-items` | `stretch`, `flex-start`, `center`, `flex-end` |
|
|
118
|
-
| **on a child** | | |
|
|
119
|
-
| `gridColumn`, `gridRow` | `grid-column/row` | `2`, `'2 / 4'`, `'span 2'`, `'2 / span 3'`, `'1 / -1'` |
|
|
120
|
-
| `gridArea` | `grid-area` | a name from `gridTemplateAreas`, or four lines |
|
|
121
|
-
| `justifySelf` | `justify-self` | `auto`, and `justifyItems`' values |
|
|
122
|
-
|
|
123
|
-
`gap`, `columnGap`, `rowGap`, `alignItems`, `alignSelf`, `justifyContent`
|
|
124
|
-
and `alignContent` are the box's and the child's own flex properties, and
|
|
125
|
-
mean for a grid what they mean in CSS.
|
|
126
|
-
|
|
127
|
-
- **A number is a count in a template**: `gridTemplateColumns: 3` is
|
|
128
|
-
`repeat(3, minmax(0, 1fr))`. Every layer that shortens grid means that by
|
|
129
|
-
a number, and compiles it to exactly that string — Tailwind's
|
|
130
|
-
`grid-cols-3`, Radix's `columns="3"`, Chakra's and Mantine's
|
|
131
|
-
`SimpleGrid`, Panda's `grid()` pattern. A number that is not a length is
|
|
132
|
-
no stranger here than `maxLines: 3` or `flex: 1`, and CSS gives a bare
|
|
133
|
-
number no meaning in a track list. On `gridAutoRows` a number is a length
|
|
134
|
-
— an implicit track has a size and never a count — and on `gridColumn` a
|
|
135
|
-
line, as in CSS and in React DOM, which never gives it `px`.
|
|
136
|
-
- **The alignment keywords are this renderer's**, the ones `alignItems`
|
|
137
|
-
takes: `flex-start` and `flex-end` where CSS grid writes `start` and
|
|
138
|
-
`end`. One spelling per concept; the error names the one to write.
|
|
139
|
-
- **Checked where written.** The style validator runs the layout's own
|
|
140
|
-
parsers in development (`validateValue`, src/styles.js), so a track list
|
|
141
|
-
that would not lay out is an error naming the property and the fix — a
|
|
142
|
-
unitless length says to write `px` and that a bare number is a count — not
|
|
143
|
-
a layout that throws and falls back to flexbox.
|
|
144
|
-
|
|
145
|
-
### What the seam gained
|
|
146
|
-
|
|
147
|
-
Two things, both general:
|
|
148
|
-
|
|
149
|
-
- **`LayoutChild.style`**, the child's resolved style: what a grid places a
|
|
150
|
-
child by and aligns it with. A registered layout can read a child's
|
|
151
|
-
`alignSelf` the same way.
|
|
152
|
-
- **`info.report(message, consequence)`**, to say a mistake the algorithm
|
|
153
|
-
can lay out around — a `gridArea` naming no area, `layoutItem` on a grid's
|
|
154
|
-
child — once, the way a bad style value is said. A throw stays for what
|
|
155
|
-
cannot be laid out.
|
|
156
|
-
|
|
157
|
-
And one thing the funnel had to learn: a grid's properties are not yoga's,
|
|
158
|
-
so nothing dirtied a grid whose `gridTemplateColumns` changed.
|
|
159
|
-
`_retarget` compares `GRID_CONTAINER_PROPS` and `GRID_ITEM_PROPS` and asks
|
|
160
|
-
the host again, as it already did for `layoutItem`.
|
|
161
|
-
|
|
162
|
-
## 3. Performance
|
|
163
|
-
|
|
164
|
-
`npm run bench:grid`: layout time per frame, `WindowNode._layoutStep`'s.
|
|
165
|
-
The changes are an item's text going from two words to forty and back;
|
|
166
|
-
changes and resizes are medians.
|
|
167
|
-
|
|
168
|
-
| case | flexbox | grid | masonry |
|
|
169
|
-
| ------------------------ | ------: | ------: | ------: |
|
|
170
|
-
| form — mount | 36.4 ms | 28.6 ms | |
|
|
171
|
-
| form — one text changed | 13.9 ms | 3.5 ms | |
|
|
172
|
-
| form — resized 1000↔900 | 5.2 ms | 2.9 ms | |
|
|
173
|
-
| form — live resize | 7.6 ms | 6.8 ms | |
|
|
174
|
-
| cards — mount | 63.0 ms | 59.3 ms | 75.3 ms |
|
|
175
|
-
| cards — one text changed | 13.1 ms | 2.1 ms | 1.8 ms |
|
|
176
|
-
| cards — resized 1000↔900 | 20.2 ms | 5.2 ms | 4.3 ms |
|
|
177
|
-
| cards — live resize | 25.8 ms | 20.3 ms | 27.9 ms |
|
|
178
|
-
| dashboard — mount | | 15.2 ms | |
|
|
179
|
-
| dashboard — one changed | | 2.2 ms | |
|
|
180
|
-
| dashboard — live resize | | 4.5 ms | |
|
|
181
|
-
|
|
182
|
-
The form is 200 rows of `auto 1fr` against flexbox rows with a fixed label
|
|
183
|
-
width; the cards 300 cards of 9 yoga nodes each in
|
|
184
|
-
`repeat(auto-fill, minmax(200px, 1fr))` against a flex-wrap row with a 200px
|
|
185
|
-
basis; the dashboard 120 tiles in `repeat(6, 1fr)`, dense, a third two
|
|
186
|
-
columns wide and a fifth two rows tall.
|
|
187
|
-
|
|
188
|
-
**Two resizes, and the difference matters.** The study measured only the
|
|
189
|
-
first: a window resized back and forth between two widths, which the seam
|
|
190
|
-
answers from what each child said at those widths before. A live resize —
|
|
191
|
-
a width nothing has been asked at, every frame, which is what a drag is —
|
|
192
|
-
asks every child again; there grid is level with flexbox rather than four
|
|
193
|
-
times cheaper. 801 child-tree layouts a frame for the 300 cards, under
|
|
194
|
-
three a card.
|
|
195
|
-
|
|
196
|
-
**Why it is cheaper than the prototype**, whose cards took 92 ms to mount
|
|
197
|
-
and 36 ms a live-resize frame: a track asks its children for sizes only
|
|
198
|
-
when its sizing function is intrinsic, as the spec has it. The gallery's
|
|
199
|
-
`minmax(200px, 1fr)` has a fixed minimum, so its columns ask nothing — no
|
|
200
|
-
max-content layout of any card — and each card is asked its height once,
|
|
201
|
-
at the snapped width it is then laid out at, which is the question the
|
|
202
|
-
placement's own check asks.
|
|
203
|
-
|
|
204
|
-
**What the seam still costs**, found building this and left for changes of
|
|
205
|
-
their own:
|
|
206
|
-
|
|
207
|
-
- `probeHeightFloors` asks a host its height at its **border-box** width
|
|
208
|
-
(`_heightForWidth`), where yoga's measure is given the content box: a
|
|
209
|
-
grid with padding is asked at widths nothing is drawn at, each a layout
|
|
210
|
-
of every child. Masonry's four layouts a card a live-resize frame are
|
|
211
|
-
mostly that.
|
|
212
|
-
- `intrinsicSizes()` lays a child out unconstrained for its max-content
|
|
213
|
-
width even when only the minimum is read.
|
|
214
|
-
- The floors pass asks a host its height at its narrowest, which lays
|
|
215
|
-
every child out at a width no frame uses.
|
|
216
|
-
|
|
217
|
-
## 4. Held to Chrome
|
|
218
|
-
|
|
219
|
-
`test/grid-conformance.test.js` lays out 400 grids that Chrome laid out
|
|
220
|
-
first (`scripts/grid-fixture.mjs`, into `test/fixtures/grid-chrome.json`)
|
|
221
|
-
and compares every item's rect, within a pixel. The grids are seeded and
|
|
222
|
-
text-free — an item holds a box of a fixed size or a wrapping row of fixed
|
|
223
|
-
chips — so both engines agree on every content size and the fixture is the
|
|
224
|
-
same on any machine. Half the cases pile features together; the other half
|
|
225
|
-
add exactly one of the harder ones — negative lines, a span wider than the
|
|
226
|
-
grid, a line past it, self and content alignment, a definite height, column
|
|
227
|
-
flow — to a case otherwise made of plain ones, so each feature has cases of
|
|
228
|
-
its own.
|
|
229
|
-
|
|
230
|
-
**399 of 400 match.** The one that does not is a case where Chrome and the
|
|
231
|
-
spec part ways, and this grid keeps to the spec: an auto-placed item whose
|
|
232
|
-
span in the flow's direction reaches several tracks past the explicit grid.
|
|
233
|
-
The spec's cursor finds it room across the grid's edge; Chrome puts it
|
|
234
|
-
after everything placed. Probed on its own: a straddle of one track lays
|
|
235
|
-
out as Chrome does, in either flow; the cases that differ reach four or
|
|
236
|
-
five tracks past it.
|
|
237
|
-
|
|
238
|
-
Built against three more corpora of the same kind — 1,000 cases — it
|
|
239
|
-
matched all but three, and those three are the one difference snapping the
|
|
240
|
-
lines makes: Chrome lays a track out at 94.66 px and wraps a row of chips
|
|
241
|
-
that fits in the 95 px here. The prototype, on the first two of those
|
|
242
|
-
corpora, matched 97–99% of the cases using only what it claimed and 59–70%
|
|
243
|
-
overall: negative lines threw, and neither self nor content alignment nor
|
|
244
|
-
column flow was there.
|
|
245
|
-
|
|
246
|
-
**The lines are snapped, not the items.** Tracks are sized exactly, and
|
|
247
|
-
each track line is rounded to a whole pixel (`trackLines`); an item's rect
|
|
248
|
-
runs from one rounded line to another. Rounding each item's own position
|
|
249
|
-
and width, which the prototype did, left a seam of a pixel between
|
|
250
|
-
neighbours in `1fr 1fr 1fr` and drew a 7px gap as 6. It would also lay a
|
|
251
|
-
child's tree out at a fraction, the input yoga divides a rounding residue
|
|
252
|
-
by (issue #411).
|
|
253
|
-
|
|
254
|
-
`test/grid.test.js` holds the cases worth a name — and the renderer's
|
|
255
|
-
side: the style, the disagreement between `display` and `layout`,
|
|
256
|
-
validation, the reports, a grid following its style from frame to frame.
|
|
257
|
-
Each of fifteen deliberate breaks of the implementation fails one of them.
|
|
258
|
-
|
|
259
|
-
## 5. What is left out
|
|
260
|
-
|
|
261
|
-
- **Subgrid.** Impossible on this seam: a subgrid's items take part in the
|
|
262
|
-
parent's track sizing, and an algorithm sees a child only as something to
|
|
263
|
-
measure. The seam would have to hand an algorithm its grandchildren. A
|
|
264
|
-
table whose rows share their columns is where it would pay.
|
|
265
|
-
- **Baselines** (`alignItems: 'baseline'` in a row): yoga's baseline
|
|
266
|
-
function, which the seam does not expose. Treated as `flex-start`.
|
|
267
|
-
- **Named lines** (`[full-start] 1fr`): an error naming them; an area is
|
|
268
|
-
the way to name a place.
|
|
269
|
-
- **The `grid`, `grid-template` and `place-*` shorthands**: one spelling
|
|
270
|
-
per concept, and the longhands are the ones every CSS-in-JS library
|
|
271
|
-
writes.
|
|
272
|
-
- **The second column pass** (§12.1 step 3): sizing the columns again when
|
|
273
|
-
an item's min-content contribution changed once the rows were sized. It
|
|
274
|
-
matters for items whose width follows their height.
|
|
275
|
-
- **An auto repeat counted against a maximum width**: CSS counts
|
|
276
|
-
`repeat(auto-fill, …)` against `max-width` when the width is indefinite;
|
|
277
|
-
here it repeats once there.
|
|
278
|
-
- **Percentage gaps**, and `order`.
|
|
279
|
-
|
|
280
|
-
## 6. Complexity
|
|
281
|
-
|
|
282
|
-
`src/grid.js` is about 1,400 lines of code, 1,650 with its comments: the
|
|
283
|
-
parsers, placement with implicit tracks on both sides, the track sizing
|
|
284
|
-
algorithm with the spec's distribution of a spanning item's space —
|
|
285
|
-
base sizes and growth limits in their separate steps, infinitely growable
|
|
286
|
-
tracks, `fit-content()` caps — and alignment. For scale, Taffy's grid
|
|
287
|
-
module is 7,508 lines (§8): the above, its corners and their tests.
|
|
288
|
-
|
|
289
|
-
## 7. Why not an engine
|
|
290
|
-
|
|
291
|
-
- **Not yoga's.** Its grid landed in March 2026 as an API with no layout
|
|
292
|
-
algorithm, the algorithm is in open PRs, and `yoga-layout` has not
|
|
293
|
-
released on npm since December 2024. When it ships, a grid named with
|
|
294
|
-
CSS's properties can hand it the work without a style changing.
|
|
295
|
-
- **Not Taffy.** Its JS and WASM builds are third-party and 0.9–1.3 MB,
|
|
296
|
-
against `yoga-layout`'s 224 KB, and it would be a second layout engine,
|
|
297
|
-
with its own text-measuring bridge, for one algorithm.
|
|
298
|
-
|
|
299
|
-
## 8. Outside facts
|
|
300
|
-
|
|
301
|
-
As of 2026-09-11.
|
|
302
|
-
|
|
303
|
-
- **The spec.** [css-grid-2 §12](https://www.w3.org/TR/css-grid-2/#algo-grid-sizing),
|
|
304
|
-
the grid sizing algorithm, and §8.5, placement.
|
|
305
|
-
- **Prior art for the style.**
|
|
306
|
-
- Tailwind v4: `grid-cols-<n>` compiles to `repeat(<n>, minmax(0, 1fr))`,
|
|
307
|
-
`col-span-full` to `1 / -1`; arbitrary track lists are
|
|
308
|
-
`grid-cols-[200px_1fr]`; there are no area utilities in core
|
|
309
|
-
([grid-template-columns](https://tailwindcss.com/docs/grid-template-columns),
|
|
310
|
-
[grid-column](https://tailwindcss.com/docs/grid-column),
|
|
311
|
-
[discussion #2634](https://github.com/tailwindlabs/tailwindcss/discussions/2634)).
|
|
312
|
-
- Radix Themes' `<Grid columns="3">` compiles a count to
|
|
313
|
-
`repeat(3, minmax(0, 1fr))`
|
|
314
|
-
([grid.props.tsx](https://github.com/radix-ui/themes/blob/HEAD/packages/radix-ui-themes/src/components/grid.props.tsx));
|
|
315
|
-
so do Chakra's `SimpleGrid`
|
|
316
|
-
([simple-grid.tsx](https://github.com/chakra-ui/chakra-ui/blob/HEAD/packages/react/src/components/simple-grid/simple-grid.tsx)),
|
|
317
|
-
Mantine's `SimpleGrid` and Panda's `grid()` pattern
|
|
318
|
-
([patterns](https://panda-css.com/docs/concepts/patterns)); Chakra's
|
|
319
|
-
and Panda's own docs write `gridTemplateColumns` in full in a style
|
|
320
|
-
object. Theme UI's `<Grid width={128}>` is
|
|
321
|
-
`repeat(auto-fit, minmax(128px, 1fr))`
|
|
322
|
-
([Grid](https://theme-ui.com/components/grid)).
|
|
323
|
-
- Yoga's grid, unreleased, names its style `gridTemplateColumns`
|
|
324
|
-
(`yoga/style/Style.h` on react/yoga's main).
|
|
325
|
-
- CSS's masonry shipped in Safari 26 as `display: grid-lanes`, reusing
|
|
326
|
-
`grid-template-columns`, `gap` and `grid-column: span`
|
|
327
|
-
([WebKit](https://webkit.org/blog/17660/introducing-css-grid-lanes/)).
|
|
328
|
-
- **Yoga.** Grid has been requested since 2019
|
|
329
|
-
([react/yoga#867](https://github.com/react/yoga/issues/867)). Part 1 of
|
|
330
|
-
nine (#1893: style types, the C and JS API, `Display.Grid`) landed on
|
|
331
|
-
2026-03-05 with no layout algorithm
|
|
332
|
-
([07524851f54b](https://github.com/react/yoga/commit/07524851f54b)); the
|
|
333
|
-
algorithm is in open PRs #1894, #1923 and
|
|
334
|
-
[#1994](https://github.com/react/yoga/pull/1994). `yoga-layout` 3.2.1
|
|
335
|
-
(2024-12-13) is still the latest release on
|
|
336
|
-
[npm](https://www.npmjs.com/package/yoga-layout).
|
|
337
|
-
- **Taffy.** `src/compute/grid` is 7,508 lines at
|
|
338
|
-
[a1c3f8f](https://github.com/DioxusLabs/taffy/tree/a1c3f8fb3e2c978c3a4f062873bdb16d5eaf6cfd/src/compute/grid);
|
|
339
|
-
subgrid ([#468](https://github.com/DioxusLabs/taffy/issues/468)) is an
|
|
340
|
-
open issue. There is no official JS package
|
|
341
|
-
([#394](https://github.com/DioxusLabs/taffy/pull/394)); third-party
|
|
342
|
-
builds are [`taffy-layout`](https://www.npmjs.com/package/taffy-layout),
|
|
343
|
-
902 KB unpacked, and
|
|
344
|
-
[`@taffyjs/wasm`](https://www.npmjs.com/package/@taffyjs/wasm), 1.34 MB.
|
|
345
|
-
- **Chromium.** LayoutNG caches a grid item's measure and layout passes
|
|
346
|
-
([LayoutNG](https://developer.chrome.com/docs/chromium/layoutng)); Firefox's
|
|
347
|
-
nested grids cost exponentially in their depth until measurement caches
|
|
348
|
-
fixed it ([1591366](https://bugzilla.mozilla.org/show_bug.cgi?id=1591366),
|
|
349
|
-
[1682686](https://bugzilla.mozilla.org/show_bug.cgi?id=1682686)).
|
|
350
|
-
|
|
351
|
-
## 9. The scripts
|
|
352
|
-
|
|
353
|
-
- `scripts/grid-fixture.mjs` regenerates the Chrome fixture (`CHROME=` to
|
|
354
|
-
point it at a browser).
|
|
355
|
-
- `scripts/bench/grid/bench.mjs` is the table in §3 (`npm run bench:grid`).
|
|
356
|
-
- `scripts/bench/grid/breakdown.mjs` is one frame taken apart, run by run,
|
|
357
|
-
with what the seam remembered for each child and what it had to ask
|
|
358
|
-
again.
|