You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+28-14Lines changed: 28 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -84,15 +84,17 @@ yarn
84
84
yarn example ios # or yarn example android
85
85
```
86
86
87
-
The example showcases:
87
+
The example showcases four scenarios the auto-sizing pipeline must handle correctly:
88
88
89
-
- Auto-sizing dynamic HTML with toggled sections.
90
-
- Live external sites (Marvel, NFL, Google, Wikipedia, The Verge) embedded without layout thrash.
91
-
- Real-time height readouts so you can verify your own endpoints quickly.
92
-
- One code path that works the same on iOS, Android, and Expo Go.
89
+
1.**Local HTML demo** — toggle a switch to mutate the document and watch the WebView re-size live.
90
+
2.**Remote site picker** — swap between Marvel, NFL, Google, Wikipedia, and The Verge to verify CMS-driven layouts resolve cleanly.
91
+
3.**Custom Google Font** — a local HTML page that imports the `Lobster` font over the network. The WebView hangs briefly while the font downloads, then the bridge re-measures via `document.fonts.loadingdone` and snaps to the final height with no clipping.
92
+
4.**Long-form article** — a CMS-style payload with lazy images and trailing margins that exercises the multi-source measurement path (`scrollHeight` + `getBoundingClientRect().bottom + computedMarginBottom`).
93
+
94
+
The demo is also wired up with [`babel-plugin-react-compiler`](https://react.dev/learn/react-compiler) so you can see how the library composes inside a React Compiler–enabled app.
93
95
94
96
> [!NOTE]
95
-
> 🧪 The demo is built with Expo; swap the`uri` to test your own pages instantly.
97
+
> 🧪 The demo is built with Expo; swap any`uri` to test your own pages instantly.
96
98
97
99
## ⚙️ API
98
100
@@ -147,12 +149,23 @@ Fix it with `loadingContainerStyle`:
147
149
148
150
## 🧠 How It Works
149
151
150
-
- Injected bridge re-parents all body children into a dedicated wrapper, trims trailing blanks, and observes DOM mutations, layout changes, font loads, and viewport shifts.
151
-
- Media events (images / iframes / video) trigger immediate + next-frame samples so late assets still report accurate heights.
152
-
- Media elements stay observed via `ResizeObserver` + decode promises, catching intrinsic size changes without duplicate network requests.
153
-
- Height calculations are debounced via `requestAnimationFrame` and a short idle timer to prevent resize storms.
154
-
- Measurements arrive through `postMessage`, then `useAutoHeight` coalesces them into a single render per frame.
155
-
- Package exports the bridge, hook, and helpers individually, making it easy to build bespoke wrappers when needed.
152
+
The injected bridge is idempotent and may run at both `injectedJavaScriptBeforeContentLoaded` and `injectedJavaScript` (the second injection is a no-op when the first already published the global handle, but covers iOS inline `source.html` cases where the early hook is skipped). It turns the WebView into a self-measuring component:
153
+
154
+
-**Measures the page in place.** The bridge does not re-parent `<body>` children into a wrapper and does not inject inline styles; it reads layout directly from the document's existing flow so framework- or CMS-generated DOM stays untouched (preserving margin collapse, author CSS, and any structure the page expects).
155
+
-**Multi-source measurement (the production-grade fix).** Each measurement is the `Math.max` of authoritative layout sources:
156
+
1.`document.body.scrollHeight` / `document.body.offsetHeight` — primary document metrics for normal block flow.
157
+
2.`document.documentElement.scrollHeight` / `document.documentElement.offsetHeight` — backstop when frameworks style `html` directly or when the root box exceeds `body`.
158
+
3. The last non-inert in-flow child's `getBoundingClientRect().bottom + computedMarginBottom` — catches margin-collapse, late image reflow, and end-of-document cases where scroll metrics momentarily under-report on iOS WKWebView.
159
+
160
+
Inert siblings (`SCRIPT`, `STYLE`, `META`, `LINK`, `TITLE`, `HEAD`, `NOSCRIPT`) and out-of-flow positions (`fixed` / `sticky` / `absolute`) are skipped during the last-in-flow-child walk so they never short-circuit the probe.
161
+
-**Bootstrap-grace adaptive fallback.** A timer re-arms itself only while either condition holds:
162
+
-`pendingLoads > 0` (an image / iframe / video is still loading), **or**
163
+
-`Date.now() - bootstrapAt < BOOTSTRAP_GRACE_MS` (5 s grace window from script start, refreshed on `markLoading`, font `loadingdone`, and external `state.refresh` calls).
164
+
165
+
Once both expire only signal-driven re-measures (mutation, resize, font, viewport, message) trigger work — the steady-state CPU cost is zero.
166
+
-**Signal-driven observers.**`MutationObserver`, `ResizeObserver`, `visualViewport`, font-load events, and a namespaced `postMessage` channel each schedule a single rAF-batched measure.
167
+
-**Rendered through `useAutoHeight`.** Heights are validated, clamped to `MAX_COMMITTED_HEIGHT` (120 000 dp), diff-thresholded, and committed at most once per animation frame.
168
+
-**Public surface stays small.** The package exports the bridge string, the hook, and helpers individually, so you can build bespoke wrappers (e.g. around a custom WebView component) without forking.
156
169
157
170
## ⚖️ Performance Snapshot
158
171
@@ -173,10 +186,11 @@ Every hot path is designed to run at its theoretical complexity floor — no all
| Height commit (rAF-batched) |**O(1)** amortized per frame | Sub-pixel diffs are dropped; at most one React render per animation frame. |
175
188
| DOM mutation callback |**O(added nodes)**| Scans only each mutation's `addedNodes`, not the whole tree. Media elements are deduped via a `WeakSet`. |
176
-
|`measureHeight`|**1 forced reflow / call**| Reads the wrapper element only — its box is authoritative because every `<body>` child lives inside it. |
189
+
|`measureHeight`|**O(k)**, single forced reflow |`Math.max` of `scrollHeight`/`offsetHeight` (constant) + a `getBoundingClientRect()` on the last non-inert child (`k` = number of trailing inert siblings, typically 0–2). |
177
190
| Trailing-node prune DFS | Runs only when the DOM is **dirty**| A mutation-driven dirty flag skips the recursive walk on resize / font / viewport ticks when nothing structural changed. |
191
+
| Late web-font reflow |**Bootstrap-grace + adaptive**| Font `loadingdone` refreshes the bootstrap window; the fallback timer keeps re-arming with 1.5× backoff until layout settles, then disarms automatically. |
178
192
179
-
The net effect: resize storms, font loads, and viewport changes cost a single `getBoundingClientRect()`per frame — nothing more. Paired with `sideEffects: false` and named-only exports, the library stays fast *and* small in the final bundle.
193
+
The net effect: resize storms, font loads, and viewport changes cost a single layout flush per frame — nothing more. Paired with `sideEffects: false` and named-only exports, the library stays fast *and* small in the final bundle. The library is also compiled with [`babel-plugin-react-compiler`](https://react.dev/learn/react-compiler), so memoization is automatic and free of stale closures.
0 commit comments