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).
The #1 Open-Source CRM
Website ·
Documentation ·
Roadmap ·
Discord ·
Figma
Why Twenty
Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves. Twenty is the CRM you build, ship, and version like the rest of your stack.
Learn more about why we built Twenty
Installation
Cloud
The fastest way to get started. Sign up at twenty.com and spin up a workspace in under a minute, with no infrastructure to manage and always up to date.
Build an app
Scaffold a new app with the Twenty CLI:
npx create-twenty-app my-app
Define objects, fields, and views as code:
import { defineObject, FieldType } from 'twenty-sdk/define';
export default defineObject({
nameSingular: 'deal',
namePlural: 'deals',
labelSingular: 'Deal',
labelPlural: 'Deals',
fields: [
{ name: 'name', label: 'Name', type: FieldType.TEXT },
{ name: 'amount', label: 'Amount', type: FieldType.CURRENCY },
{ name: 'closeDate', label: 'Close Date', type: FieldType.DATE_TIME },
],
});
Then ship it to your workspace:
npx twenty app:publish --private
See the app development guide for objects, views, agents, and logic functions.
Self-hosting
Run Twenty on your own infrastructure with Docker Compose, or contribute locally via the local setup guide.
Everything you need
Twenty gives you the building blocks of a modern CRM (objects, views, workflows, and agents) and lets you extend them as code. Here's a tour of what's in the box.
Want to go deeper? Read the User Guide for product walkthroughs, or the
Documentation for developer reference.
|
|
|
|
|
|
Stack
TypeScript
Nx
NestJS, with BullMQ,
PostgreSQL,
Redis
React, with Jotai, Linaria and Lingui
Thanks
Thanks to these amazing services that we use and recommend for code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
Join the Community
Star the repo ·
Discord ·
Feature requests ·
Releases ·
X ·
LinkedIn ·
Crowdin ·
Contribute





