diff --git a/components/Demo.jsx b/components/Demo.jsx
new file mode 100644
index 0000000..67820f8
--- /dev/null
+++ b/components/Demo.jsx
@@ -0,0 +1,55 @@
+import { useEffect, useRef, useState } from "react";
+
+/**
+ * Embeds a self-contained interactive demo (a static HTML file under
+ * `public/demos/`) inside an iframe. The demo reports its own content height
+ * via `postMessage({ [heightKey]: number })`, so the iframe grows to fit
+ * without an inner scrollbar. Theme (light/dark) syncs automatically: the demo
+ * reads `localStorage.theme`, which Nextra also writes, and both share the same
+ * origin.
+ */
+export function Demo({
+ src,
+ title,
+ caption,
+ heightKey = "rafDemoHeight",
+ minHeight = 400,
+}) {
+ const iframeRef = useRef(null);
+ const [height, setHeight] = useState(minHeight);
+
+ useEffect(() => {
+ function handleMessage(event) {
+ if (event.source !== iframeRef.current?.contentWindow) return;
+ const value = event.data?.[heightKey];
+ if (typeof value === "number") {
+ setHeight(value + 24);
+ }
+ }
+ window.addEventListener("message", handleMessage);
+ return () => window.removeEventListener("message", handleMessage);
+ }, [heightKey]);
+
+ return (
+
+
+ {caption ? (
+
+ {caption}
+
+ ) : null}
+
+ );
+}
diff --git a/pages/Interaction/Forced-Synchronous-Layout.mdx b/pages/Interaction/Forced-Synchronous-Layout.mdx
index d845a38..da8caa1 100644
--- a/pages/Interaction/Forced-Synchronous-Layout.mdx
+++ b/pages/Interaction/Forced-Synchronous-Layout.mdx
@@ -1,5 +1,6 @@
import snippet from '../../snippets/Interaction/Forced-Synchronous-Layout.js?raw'
import { Snippet } from '../../components/Snippet'
+import { Demo } from '../../components/Demo'
# Forced Synchronous Layout Detector
@@ -29,6 +30,14 @@ requestAnimationFrame(() => { // 1st rAF: browser processes styles
});
```
+Step through the render pipeline to see _why_ the double `requestAnimationFrame` avoids the forced layout:
+
+
+
**What this snippet intercepts:**
Mutation sources — detected synchronously:
diff --git a/pages/Interaction/LongTask.mdx b/pages/Interaction/LongTask.mdx
index eaaf6e7..71b808e 100644
--- a/pages/Interaction/LongTask.mdx
+++ b/pages/Interaction/LongTask.mdx
@@ -1,5 +1,6 @@
import snippet from '../../snippets/Interaction/LongTask.js?raw'
import { Snippet } from '../../components/Snippet'
+import { Demo } from '../../components/Demo'
# Long Tasks
@@ -87,22 +88,18 @@ flowchart LR
style Block fill:#FFE5E5,stroke:#FF4444,stroke-width:2px
```
-**Severity Levels:**
+### Breaking Up Long Tasks
-```mermaid
-flowchart TB
- Task[Long Task Duration] --> Check{How long?}
-
- Check -->|50-100ms| Low["🟢 Low Severity Minor delay"]
- Check -->|100-150ms| Medium["🟡 Medium Severity Noticeable lag"]
- Check -->|150-250ms| High["🟠 High Severity Poor responsiveness"]
- Check -->|> 250ms| Critical["🔴 Critical Severity Severe blocking"]
-
- style Low fill:#E5FFE5,stroke:#44AA44,stroke-width:2px
- style Medium fill:#FFFBE5,stroke:#FFBB00,stroke-width:2px
- style High fill:#FFF3E5,stroke:#FF8800,stroke-width:2px
- style Critical fill:#FFE5E5,stroke:#FF4444,stroke-width:2px
-```
+Once a long task is detected, the fix is to _yield to the main thread_ so the browser can handle pending input between chunks of work. A common misconception is that `queueMicrotask` yields, it does not: microtasks run within the same task, so the main thread stays blocked. Only scheduling a new task (for example with `setTimeout`, or the newer `scheduler.yield()`) actually gives the browser a chance to respond.
+
+Step through the event loop to compare the three approaches:
+
+
### Limitations
diff --git a/public/demos/raf-pipeline.html b/public/demos/raf-pipeline.html
new file mode 100644
index 0000000..7654a5f
--- /dev/null
+++ b/public/demos/raf-pipeline.html
@@ -0,0 +1,543 @@
+
+
+