---
name: webf-async-rendering
description: Understand and work with WebF's async rendering model - handle onscreen/offscreen events and element measurements correctly. Use when getBoundingClientRect returns zeros, computed styles are incorrect, measurements fail, or elements don't layout as expected.
---
# WebF Async Rendering
> **Note**: WebF development is nearly identical to web development - you use the same tools (Vite, npm, Vitest), same frameworks (React, Vue, Svelte), and same deployment services (Vercel, Netlify). This skill covers **one of the 3 key differences**: WebF's async rendering model. The other two differences are API compatibility and routing.
**This is the #1 most important concept to understand when moving from browser development to WebF.**
## The Fundamental Difference
### In Browsers (Synchronous Layout)
When you modify the DOM, the browser **immediately** performs layout calculations:
```javascript
// Browser behavior
const div = document.createElement('div');
document.body.appendChild(div);
console.log(div.getBoundingClientRect()); // ✅ Returns real dimensions
```
Layout happens **synchronously** - you get dimensions right away, but this can cause performance issues (layout thrashing).
### In WebF (Asynchronous Layout)
When you modify the DOM, WebF **batches** the changes and processes them in the next rendering frame:
```javascript
// WebF behavior
const div = document.createElement('div');
document.body.appendChild(div);
console.log(div.getBoundingClientRect()); // ❌ Returns zeros! Not laid out yet.
```
Layout happens **asynchronously** - elements exist in the DOM tree but haven't been measured/positioned yet.
## Why Async Rendering?
**Performance**: WebF's async rendering is **20x cheaper** than browser synchronous layout!
- DOM updates are batched together
- Multiple changes processed in one optimized pass
- Eliminates layout thrashing
- No need for `DocumentFragment` optimizations
**Trade-off**: You must explicitly wait for layout to complete before measuring elements.
## The Solution: onscreen/offscreen Events
WebF provides two non-standard events to handle the async lifecycle:
| Event | When It Fires | Purpose |
|-------|---------------|---------|
| `onscreen` | Element has been laid out and rendered | Safe to measure dimensions, get computed styles |
| `offscreen` | Element removed from render tree | Cleanup and resource management |
**Think of these like `IntersectionObserver` but for layout lifecycle, not viewport visibility.**
## How to Measure Elements Correctly
### ❌ WRONG: Measuring Immediately
```javascript
// DON'T DO THIS - Will return 0 or incorrect values
const div = document.createElement('div');
div.textContent = 'Hello WebF';
document.body.appendChild(div);
const rect = div.getBoundingClientRect(); // ❌ Returns zeros!
console.log(rect.width); // 0
console.log(rect.height); // 0
```
### ✅ CORRECT: Wait for onscreen Event
```javascript
// DO THIS - Wait for layout to complete
const div = document.createElement('div');
div.textContent = 'Hello WebF';
div.addEventListener('onscreen', () => {
// Element is now laid out - safe to measure!
const rect = div.getBoundingClientRect(); // ✅ Real dimensions
console.log(`Width: ${rect.width}, Height: ${rect.height}`);
});
document.body.appendChild(div);
```
## React: useFlutterAttached Hook
For React developers, WebF provides a convenient hook:
### ❌ WRONG: Using useEffect
```jsx
import { useEffect, useRef } from 'react';
function MyComponent() {
const ref = useRef(null);
useEffect(() => {
// ❌ Element not laid out yet!
const rect = ref.current.getBoundingClientRect();
console.log(rect); // Will be zeros
}, []);
return
Content
;
}
```
### ✅ CORRECT: Using useFlutterAttached
```jsx
import { useFlutterAttached } from '@openwebf/react-core-ui';
function MyComponent() {
const ref = useFlutterAttached(
() => {
// ✅ onAttached callback - element is laid out!
const rect = ref.current.getBoundingClientRect();
console.log(`Width: ${rect.width}, Height: ${rect.height}`);
},
() => {
// onDetached callback (optional)
console.log('Component removed from render tree');
}
);
return Content
;
}
```
## Layout-Dependent APIs
**Only call these inside onscreen callback or useFlutterAttached:**
- `element.getBoundingClientRect()`
- `window.getComputedStyle(element)`
- `element.offsetWidth` / `element.offsetHeight`
- `element.clientWidth` / `element.clientHeight`
- `element.scrollWidth` / `element.scrollHeight`
- `element.offsetTop` / `element.offsetLeft`
- Any logic that depends on element position or size
## Common Scenarios
### Scenario 1: Measuring After Style Changes
```javascript
const div = document.getElementById('myDiv');
// ❌ WRONG
div.style.width = '500px';
const rect = div.getBoundingClientRect(); // Old dimensions!
// ✅ CORRECT
div.style.width = '500px';
div.addEventListener('onscreen', () => {
const rect = div.getBoundingClientRect(); // New dimensions!
}, { once: true }); // Use 'once' to remove listener after first call
```
### Scenario 2: Positioning Tooltips/Popovers
```javascript
function showTooltip(targetElement) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = 'Tooltip text';
tooltip.addEventListener('onscreen', () => {
// Now we can safely position the tooltip
const targetRect = targetElement.getBoundingClientRect();
const tooltipRect = tooltip.getBoundingClientRect();
tooltip.style.left = `${targetRect.left}px`;
tooltip.style.top = `${targetRect.bottom + 5}px`;
}, { once: true });
document.body.appendChild(tooltip);
}
```
### Scenario 3: React Component with Measurement
```jsx
import { useFlutterAttached } from '@openwebf/react-core-ui';
import { useState } from 'react';
function MeasuredBox() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const ref = useFlutterAttached(() => {
const rect = ref.current.getBoundingClientRect();
setDimensions({
width: rect.width,
height: rect.height
});
});
return (
This box is {dimensions.width}px wide
and {dimensions.height}px tall
);
}
```
## Performance Benefits
WebF's async rendering provides significant advantages:
1. **Batched Updates**: Multiple DOM changes processed together
2. **No Layout Thrashing**: Eliminates read-write-read-write patterns
3. **Optimized Rendering**: Single pass through the render tree
4. **No DocumentFragment Needed**: Batching is automatic
Compare to browsers where you'd need to carefully batch operations:
```javascript
// Browser optimization (not needed in WebF!)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const div = document.createElement('div');
fragment.appendChild(div);
}
document.body.appendChild(fragment); // Single layout
```
In WebF, just append directly - it's automatically optimized!
## Common Mistakes
### Mistake 1: Forgetting to Wait
```javascript
// ❌ WRONG
const div = document.createElement('div');
document.body.appendChild(div);
initializeWidget(div); // Assumes div is laid out - will fail!
```
```javascript
// ✅ CORRECT
const div = document.createElement('div');
div.addEventListener('onscreen', () => {
initializeWidget(div); // Now it's safe!
}, { once: true });
document.body.appendChild(div);
```
### Mistake 2: Not Cleaning Up Listeners
```javascript
// ❌ WRONG - Memory leak
element.addEventListener('onscreen', handleLayout);
// Listener never removed!
// ✅ CORRECT
element.addEventListener('onscreen', handleLayout, { once: true });
// OR
element.addEventListener('onscreen', handleLayout);
// Later...
element.removeEventListener('onscreen', handleLayout);
```
### Mistake 3: Using IntersectionObserver for Layout
```javascript
// ❌ WRONG - IntersectionObserver is for viewport visibility, not layout
const observer = new IntersectionObserver((entries) => {
// This fires based on viewport, not layout completion!
});
// ✅ CORRECT - Use onscreen for layout lifecycle
element.addEventListener('onscreen', () => {
// Element is laid out
});
```
## Debugging Tips
If you're getting zero or incorrect dimensions:
1. **Check if you're waiting for onscreen**: Most common issue
2. **Verify element is actually added to DOM**: Must be in document tree
3. **Confirm element has display style**: `display: none` elements don't layout
4. **Use console.log in onscreen callback**: Verify callback fires
```javascript
element.addEventListener('onscreen', () => {
console.log('✅ onscreen fired');
console.log(element.getBoundingClientRect());
}, { once: true });
```
## Resources
- **Core Concepts - Async Rendering**: https://openwebf.com/en/docs/developer-guide/core-concepts#async-rendering
- **Debugging & Performance**: https://openwebf.com/en/docs/developer-guide/debugging-performance
- **@openwebf/react-core-ui**: Install with `npm install @openwebf/react-core-ui`
## Key Takeaways
✅ **DO**:
- Use `onscreen` event or `useFlutterAttached` hook
- Wait for layout before measuring elements
- Use `{ once: true }` for one-time measurements
❌ **DON'T**:
- Measure immediately after appendChild()
- Rely on synchronous layout like browsers
- Use IntersectionObserver for layout detection
- Forget to clean up event listeners