Mirror host element geometry into front component workers (#23264)

Front components run in a Web Worker whose fake DOM has no layout APIs,
so any library that measures itself crashes. This is the reported
recharts bug: `ref.getBoundingClientRect is not a function`.

### Why this is needed

Layout only exists on the host: the worker builds a virtual tree, and
the host renders the real DOM nodes. Nothing in the worker knows how big
anything is.

```mermaid
flowchart LR
    COMP["Front component<br/>recharts, twenty-ui"] -->|"el.getBoundingClientRect()"| DOM["remote-dom fake DOM<br/>in the Web Worker"]
    DOM --> MISS["No layout APIs:<br/>method does not exist"]
    MISS --> BOOM["TypeError, component crashes"]
    HOST["Host: real DOM nodes<br/>with real sizes"] -.->|"never reaches the worker"| DOM
```

The worker cannot simply ask the host and wait: measurement APIs are
synchronous, and the worker must never block on a round trip.

### How the mirror works

The host measures and pushes; the worker only ever reads from a local
copy. Reads stay synchronous and are at most one frame behind.

```mermaid
flowchart TB
    subgraph HOST["Host - main thread, real DOM"]
        WAKE["Wake sources<br/>resize, scroll, mutations, animation events"]
        TRACK["createGeometryTracker<br/>rAF loop, idles after 20 unchanged frames"]
        NODES["Real DOM nodes<br/>registered per remote element id"]
    end

    subgraph WORKER["Web Worker - fake DOM"]
        STORE["workerGeometryStore<br/>snapshot mirror"]
        POLY["Element.prototype polyfill<br/>getBoundingClientRect, offset, client, scroll"]
        COMP2["Front component"]
    end

    WAKE -->|"wake"| TRACK
    NODES -->|"measure changed nodes only"| TRACK
    TRACK ==>|"pushGeometryUpdates over MessagePort"| STORE
    STORE -->|"synchronous read, one frame stale"| POLY
    POLY --> COMP2
    COMP2 -.->|"first read enrolls the element:<br/>observeElementGeometry"| TRACK
```

Enrollment is demand-driven: an element is only measured once the
component actually reads its geometry, so idle components cost nothing.

```mermaid
sequenceDiagram
    participant C as Front component
    participant P as Element polyfill
    participant S as Worker geometry store
    participant T as Host geometry tracker
    participant D as Real DOM

    C->>P: el.getBoundingClientRect
    P->>S: resolve snapshot
    S-->>P: none yet, returns zeros
    S->>T: observeElementGeometry, batched in a microtask
    T->>D: measure on the next animation frame
    D-->>T: rect, offset, client, scroll
    T->>S: pushGeometryUpdates with viewport and changed elements
    Note over T: the loop stops after 20 unchanged frames, any wake source restarts it
    C->>P: el.getBoundingClientRect on a later frame
    P->>S: resolve snapshot
    S-->>P: mirrored values
    P-->>C: real numbers
```

### What changed

- The host measures the real DOM nodes on animation frames while wake
sources report activity, and pushes snapshots over the existing
MessagePort. The loop goes idle when nothing changes, and both sides cap
observation at 500 elements.
- In the worker, `getBoundingClientRect`, the
`offset*`/`client*`/`scroll*` getters and
`window.innerWidth`/`innerHeight` read those snapshots from the
worker-local mirror.
- The worker also gains the small DOM APIs libraries expect:
`getComputedStyle` (returns the element's declared style),
`getElementsByClassName`, `document.getElementById`, and a working
per-element `style` on base elements (remote-dom ships a no-op stub
whose `getPropertyValue` returns undefined, which crashed twenty-ui's
ThemeProvider).

Result: a fixed-size recharts `AreaChart` story renders, and the four
twenty-ui gallery stories that used to fail on the missing
`getComputedStyle` now run in strict zero-failure mode.

Moved, not new: `FrontComponentRenderer` now renders its thread effects
directly instead of through a pass-through component, and its output is
wrapped in a `<div style="width:100%;height:100%">` instead of a
fragment so geometry has a measurable root (a real layout change for
embedders).

Deferred to the ResizeObserver follow-up: text measurement (axis-label
overlap thinning), `offsetParent` mirroring, animation in-flight
tracking, `ResponsiveContainer`, the tooltip, and the
`measureElementGeometry` RPC.

Last of the three PRs splitting the geometry mirror work, after #23262
(host wrapper hooks) and #23263 (style proxy).
This commit is contained in:
Raphaël Bosi
2026-07-30 14:02:18 +02:00
committed by GitHub
parent cdeebb1a18
commit 014b3cdc67
80 changed files with 4545 additions and 90 deletions
+192 -8
View File
@@ -15514,6 +15514,28 @@ __metadata:
languageName: node
linkType: hard
"@reduxjs/toolkit@npm:^1.9.0 || 2.x.x":
version: 2.12.0
resolution: "@reduxjs/toolkit@npm:2.12.0"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@standard-schema/utils": "npm:^0.3.0"
immer: "npm:^11.0.0"
redux: "npm:^5.0.1"
redux-thunk: "npm:^3.1.0"
reselect: "npm:^5.1.0"
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
checksum: 10c0/2b85e31294a7139994fd78b08eb3ce706ce3b03b5081c13403b93ba4883a152d637171e4d9d969766238851f8915b1daa9f36b5a0a458aff0ff7600ee38795c9
languageName: node
linkType: hard
"@remirror/core-constants@npm:3.0.0":
version: 3.0.0
resolution: "@remirror/core-constants@npm:3.0.0"
@@ -18180,7 +18202,7 @@ __metadata:
languageName: node
linkType: hard
"@standard-schema/spec@npm:^1.1.0":
"@standard-schema/spec@npm:^1.0.0, @standard-schema/spec@npm:^1.1.0":
version: 1.1.0
resolution: "@standard-schema/spec@npm:1.1.0"
checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526
@@ -19880,6 +19902,13 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-array@npm:^3.0.3":
version: 3.2.2
resolution: "@types/d3-array@npm:3.2.2"
checksum: 10c0/6137cb97302f8a4f18ca22c0560c585cfcb823f276b23d89f2c0c005d72697ec13bca671c08e68b4b0cabd622e3f0e91782ee221580d6774074050be96dd7028
languageName: node
linkType: hard
"@types/d3-color@npm:*, @types/d3-color@npm:^3.0.0":
version: 3.1.3
resolution: "@types/d3-color@npm:3.1.3"
@@ -19903,6 +19932,13 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-ease@npm:^3.0.0":
version: 3.0.2
resolution: "@types/d3-ease@npm:3.0.2"
checksum: 10c0/aff5a1e572a937ee9bff6465225d7ba27d5e0c976bd9eacdac2e6f10700a7cb0c9ea2597aff6b43a6ed850a3210030870238894a77ec73e309b4a9d0333f099c
languageName: node
linkType: hard
"@types/d3-format@npm:^1.4.1":
version: 1.4.5
resolution: "@types/d3-format@npm:1.4.5"
@@ -19910,7 +19946,7 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.4":
"@types/d3-interpolate@npm:*, @types/d3-interpolate@npm:^3.0.1, @types/d3-interpolate@npm:^3.0.4":
version: 3.0.4
resolution: "@types/d3-interpolate@npm:3.0.4"
dependencies:
@@ -19933,6 +19969,15 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-scale@npm:^4.0.2":
version: 4.0.9
resolution: "@types/d3-scale@npm:4.0.9"
dependencies:
"@types/d3-time": "npm:*"
checksum: 10c0/4ac44233c05cd50b65b33ecb35d99fdf07566bcdbc55bc1306b2f27d1c5134d8c560d356f2c8e76b096e9125ffb8d26d95f78d56e210d1c542cb255bdf31d6c8
languageName: node
linkType: hard
"@types/d3-scale@npm:^4.0.8":
version: 4.0.8
resolution: "@types/d3-scale@npm:4.0.8"
@@ -19949,6 +19994,15 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-shape@npm:^3.1.0":
version: 3.1.8
resolution: "@types/d3-shape@npm:3.1.8"
dependencies:
"@types/d3-path": "npm:*"
checksum: 10c0/49ec2172b9eb66fc1d036e2a23966216bb972e9af51ddbed134df24bd71aedf207bb1ef81903a119eb4e1f5e927cf44beacaf64c9af86474e5548594b102b574
languageName: node
linkType: hard
"@types/d3-shape@npm:^3.1.6":
version: 3.1.6
resolution: "@types/d3-shape@npm:3.1.6"
@@ -19986,6 +20040,20 @@ __metadata:
languageName: node
linkType: hard
"@types/d3-time@npm:^3.0.0":
version: 3.0.4
resolution: "@types/d3-time@npm:3.0.4"
checksum: 10c0/6d9e2255d63f7a313a543113920c612e957d70da4fb0890931da6c2459010291b8b1f95e149a538500c1c99e7e6c89ffcce5554dd29a31ff134a38ea94b6d174
languageName: node
linkType: hard
"@types/d3-timer@npm:^3.0.0":
version: 3.0.2
resolution: "@types/d3-timer@npm:3.0.2"
checksum: 10c0/c644dd9571fcc62b1aa12c03bcad40571553020feeb5811f1d8a937ac1e65b8a04b759b4873aef610e28b8714ac71c9885a4d6c127a048d95118f7e5b506d9e1
languageName: node
linkType: hard
"@types/d3-transition@npm:^3.0.8":
version: 3.0.8
resolution: "@types/d3-transition@npm:3.0.8"
@@ -27780,7 +27848,7 @@ __metadata:
languageName: node
linkType: hard
"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3":
"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3, d3-array@npm:^3.1.6":
version: 3.2.4
resolution: "d3-array@npm:3.2.4"
dependencies:
@@ -27822,7 +27890,7 @@ __metadata:
languageName: node
linkType: hard
"d3-ease@npm:1 - 3":
"d3-ease@npm:1 - 3, d3-ease@npm:^3.0.1":
version: 3.0.1
resolution: "d3-ease@npm:3.0.1"
checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0
@@ -27889,7 +27957,7 @@ __metadata:
languageName: node
linkType: hard
"d3-shape@npm:^3.2.0":
"d3-shape@npm:^3.1.0, d3-shape@npm:^3.2.0":
version: 3.2.0
resolution: "d3-shape@npm:3.2.0"
dependencies:
@@ -27925,7 +27993,7 @@ __metadata:
languageName: node
linkType: hard
"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3":
"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3, d3-time@npm:^3.0.0":
version: 3.1.0
resolution: "d3-time@npm:3.1.0"
dependencies:
@@ -27941,7 +28009,7 @@ __metadata:
languageName: node
linkType: hard
"d3-timer@npm:1 - 3":
"d3-timer@npm:1 - 3, d3-timer@npm:^3.0.1":
version: 3.0.1
resolution: "d3-timer@npm:3.0.1"
checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a
@@ -28314,6 +28382,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js-light@npm:^2.5.1":
version: 2.5.1
resolution: "decimal.js-light@npm:2.5.1"
checksum: 10c0/4fd33f535aac9e5bd832796831b65d9ec7914ad129c7437b3ab991b0c2eaaa5a57e654e6174c4a17f1b3895ea366f0c1ab4955cdcdf7cfdcf3ad5a58b456c020
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3, decimal.js@npm:^10.5.0":
version: 10.5.0
resolution: "decimal.js@npm:10.5.0"
@@ -29610,6 +29685,20 @@ __metadata:
languageName: node
linkType: hard
"es-toolkit@npm:^1.39.3":
version: 1.50.0
resolution: "es-toolkit@npm:1.50.0"
dependenciesMeta:
"@trivago/prettier-plugin-sort-imports@4.3.0":
unplugged: true
prettier-plugin-sort-re-exports@0.0.1:
unplugged: true
vitepress-plugin-sandpack@1.1.4:
unplugged: true
checksum: 10c0/80e3a212d337df6f20ed0330d1b62dda25f3ef6d4c83a33f13d521ccbf5b4ad1ac3e7e638572abd6080627f522707de76a5c9360350b713e5392739083ee1f24
languageName: node
linkType: hard
"esast-util-from-estree@npm:^2.0.0":
version: 2.0.0
resolution: "esast-util-from-estree@npm:2.0.0"
@@ -33498,6 +33587,13 @@ __metadata:
languageName: node
linkType: hard
"immer@npm:^11.0.0, immer@npm:^11.1.8":
version: 11.1.15
resolution: "immer@npm:11.1.15"
checksum: 10c0/5215eaec9641ad786ae8940a489514c26ce3726f8c49f0191a754902800f3ddd102b0653e894cabe7049d93e77b86d67d19953e778ffebba56848739a31b3b79
languageName: node
linkType: hard
"immer@npm:^9.0.6":
version: 9.0.21
resolution: "immer@npm:9.0.21"
@@ -43765,6 +43861,25 @@ __metadata:
languageName: node
linkType: hard
"react-redux@npm:8.x.x || 9.x.x":
version: 9.3.0
resolution: "react-redux@npm:9.3.0"
dependencies:
"@types/use-sync-external-store": "npm:^0.0.6"
use-sync-external-store: "npm:^1.4.0"
peerDependencies:
"@types/react": ^18.2.25 || ^19
react: ^18.0 || ^19
redux: ^5.0.0
peerDependenciesMeta:
"@types/react":
optional: true
redux:
optional: true
checksum: 10c0/b9f4efcfbfbc90cac9d1709ab3affb1e18a9dc9bd3cceda43bd2e1d9d2394ee0c29df36ec2202d52b566db774888b594c8c5aa86b64f27ef34fca607c687c9e3
languageName: node
linkType: hard
"react-remove-scroll-bar@npm:^2.3.7":
version: 2.3.8
resolution: "react-remove-scroll-bar@npm:2.3.8"
@@ -44094,6 +44209,29 @@ __metadata:
languageName: node
linkType: hard
"recharts@npm:^3.9.2":
version: 3.10.0
resolution: "recharts@npm:3.10.0"
dependencies:
"@reduxjs/toolkit": "npm:^1.9.0 || 2.x.x"
clsx: "npm:^2.1.1"
decimal.js-light: "npm:^2.5.1"
es-toolkit: "npm:^1.39.3"
eventemitter3: "npm:^5.0.1"
immer: "npm:^11.1.8"
react-redux: "npm:8.x.x || 9.x.x"
reselect: "npm:5.2.0"
tiny-invariant: "npm:^1.3.3"
use-sync-external-store: "npm:^1.2.2"
victory-vendor: "npm:^37.0.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
checksum: 10c0/223524f5db2bb660878c37d70a441b899e9db861dd47768ccccd3982b43d0233cbc3b4a01d32abb76d8782320839cf4a3dc3de9bc002f7c9177b656484efe506
languageName: node
linkType: hard
"recma-build-jsx@npm:^1.0.0":
version: 1.0.0
resolution: "recma-build-jsx@npm:1.0.0"
@@ -44184,6 +44322,22 @@ __metadata:
languageName: node
linkType: hard
"redux-thunk@npm:^3.1.0":
version: 3.1.0
resolution: "redux-thunk@npm:3.1.0"
peerDependencies:
redux: ^5.0.0
checksum: 10c0/21557f6a30e1b2e3e470933247e51749be7f1d5a9620069a3125778675ce4d178d84bdee3e2a0903427a5c429e3aeec6d4df57897faf93eb83455bc1ef7b66fd
languageName: node
linkType: hard
"redux@npm:^5.0.1":
version: 5.0.1
resolution: "redux@npm:5.0.1"
checksum: 10c0/b10c28357194f38e7d53b760ed5e64faa317cc63de1fb95bc5d9e127fab956392344368c357b8e7a9bedb0c35b111e7efa522210cfdc3b3c75e5074718e9069c
languageName: node
linkType: hard
"reflect-metadata@npm:0.2.2, reflect-metadata@npm:^0.2.2":
version: 0.2.2
resolution: "reflect-metadata@npm:0.2.2"
@@ -44671,6 +44825,13 @@ __metadata:
languageName: node
linkType: hard
"reselect@npm:5.2.0, reselect@npm:^5.1.0":
version: 5.2.0
resolution: "reselect@npm:5.2.0"
checksum: 10c0/d0a8497e404b25a769fe792eacae88b9b576c30870450cd243d76ec9427d91a59c4a2cc00d824d283448b444c4c698a8f961d771c8ae39c759313afb31950e2e
languageName: node
linkType: hard
"reselect@npm:^4.1.7":
version: 4.1.8
resolution: "reselect@npm:4.1.8"
@@ -48940,6 +49101,7 @@ __metadata:
react: "npm:^19.2.0"
react-dom: "npm:^19.2.0"
react-error-boundary: "npm:^4.0.11"
recharts: "npm:^3.9.2"
storybook: "npm:^10.4.6"
styled-components: "npm:^6.1.0"
ts-morph: "npm:^25.0.0"
@@ -50784,7 +50946,7 @@ __metadata:
languageName: node
linkType: hard
"use-sync-external-store@npm:1.6.0, use-sync-external-store@npm:^1.5.0, use-sync-external-store@npm:^1.6.0":
"use-sync-external-store@npm:1.6.0, use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.5.0, use-sync-external-store@npm:^1.6.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
@@ -51038,6 +51200,28 @@ __metadata:
languageName: node
linkType: hard
"victory-vendor@npm:^37.0.2":
version: 37.3.6
resolution: "victory-vendor@npm:37.3.6"
dependencies:
"@types/d3-array": "npm:^3.0.3"
"@types/d3-ease": "npm:^3.0.0"
"@types/d3-interpolate": "npm:^3.0.1"
"@types/d3-scale": "npm:^4.0.2"
"@types/d3-shape": "npm:^3.1.0"
"@types/d3-time": "npm:^3.0.0"
"@types/d3-timer": "npm:^3.0.0"
d3-array: "npm:^3.1.6"
d3-ease: "npm:^3.0.1"
d3-interpolate: "npm:^3.0.1"
d3-scale: "npm:^4.0.2"
d3-shape: "npm:^3.1.0"
d3-time: "npm:^3.0.0"
d3-timer: "npm:^3.0.1"
checksum: 10c0/0a9628db30b898160409628f26d457ba15adc86472531817d73fbeb847de498d94f91dee5f4caa969195744e91be93e100eeff7a392b591ac5349343a36dec29
languageName: node
linkType: hard
"vinyl-file@npm:^5.0.0":
version: 5.0.0
resolution: "vinyl-file@npm:5.0.0"