Understand and work with WebF's async rendering model - handle onscreen/offscreen events and element measurements correctly...
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.
When you modify the DOM, the browser immediately performs layout calculations:
// 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).
When you modify the DOM, WebF batches the changes and processes them in the next rendering frame:
// 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.
Performance: WebF's async rendering is 20x cheaper than browser synchronous layout!
DocumentFragment optimizationsTrade-off: You must explicitly wait for layout to complete before measuring elements.
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.
// 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
// 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);
For React developers, WebF provides a convenient hook:
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 <div ref={ref}>Content</div>;
}
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 <div ref={ref}>Content</div>;
}
Only call these inside onscreen callback or useFlutterAttached:
element.getBoundingClientRect()window.getComputedStyle(element)element.offsetWidth / element.offsetHeightelement.clientWidth / element.clientHeightelement.scrollWidth / element.scrollHeightelement.offsetTop / element.offsetLeftconst 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
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);
}
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 (
<div ref={ref} style={{ padding: '20px', border: '1px solid' }}>
<p>This box is {dimensions.width}px wide</p>
<p>and {dimensions.height}px tall</p>
</div>
);
}
WebF's async rendering provides significant advantages:
Compare to browsers where you'd need to carefully batch operations:
// 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!
// ā WRONG
const div = document.createElement('div');
document.body.appendChild(div);
initializeWidget(div); // Assumes div is laid out - will fail!
// ā
CORRECT
const div = document.createElement('div');
div.addEventListener('onscreen', () => {
initializeWidget(div); // Now it's safe!
}, { once: true });
document.body.appendChild(div);
// ā 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);
// ā 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
});
If you're getting zero or incorrect dimensions:
display: none elements don't layoutelement.addEventListener('onscreen', () => {
console.log('ā
onscreen fired');
console.log(element.getBoundingClientRect());
}, { once: true });
npm install @openwebf/react-core-uiā DO:
onscreen event or useFlutterAttached hook{ once: true } for one-time measurementsā DON'T: