Skip to content

[Feat]: Add asyncInit option for non-blocking dimension measurement#1328

Open
dlabadini wants to merge 5 commits intodavidjerleke:masterfrom
dlabadini:feature/async-measurement
Open

[Feat]: Add asyncInit option for non-blocking dimension measurement#1328
dlabadini wants to merge 5 commits intodavidjerleke:masterfrom
dlabadini:feature/async-measurement

Conversation

@dlabadini
Copy link
Copy Markdown

Motivation

Embla reads offsetWidth/offsetHeight synchronously during activate() to compute slide dimensions. In frameworks like React, these reads happen inside useEffect — after React's DOM commit has written class/style changes from other components. This cross-component write-then-read forces a synchronous browser reflow, blocking the main thread.

This is increasingly problematic as pages render more carousels (e.g., discovery pages with 8+ carousels), and as frameworks move toward concurrent/deferred rendering where effects interleave.

Related: #1321 (sync reInit inside ResizeObserver), #1326 (SSR plugin extraction)

Solution

Add an asyncInit option (default: false) that uses ResizeObserver for initial dimension measurement instead of synchronous offsetWidth/offsetHeight reads.

When asyncInit: true:

  1. activate() sets up a ResizeObserver on the container + slides
  2. The observer fires with borderBoxSize dimensions (no layout read)
  3. Engine is created with those dimensions
  4. Carousel becomes interactive

The one-frame delay (~16ms) between mount and interactive is invisible to users — the carousel is already visually rendered via CSS, just not yet draggable/snappable.

When asyncInit: false (default): current sync behavior, fully backward compatible.

Design decisions

  • Opt-in, not default: Preserves backward compatibility. Consumers that depend on sync API availability after mount are unaffected.
  • ResizeObserver over rAF: ResizeObserver reports dimensions without triggering layout recalculation. requestAnimationFrame would still require offsetWidth reads.
  • borderBoxSize: Available in all modern browsers (Chrome 84+, Firefox 92+, Safari 15.4+). More accurate than offsetWidth for elements with box-sizing variations.
  • Cleanup via mediaHandlers: The observer disconnects on deActivate() using the existing cleanup infrastructure.

Test plan

  • Existing tests pass (sync path unchanged)
  • asyncInit: false behaves identically to current behavior
  • asyncInit: true produces same snap positions after observer fires
  • Carousel is interactive after one frame with asyncInit: true
  • destroy() during async init cancels observer cleanly

🤖 Generated with Claude Code

dlabadini and others added 5 commits April 2, 2026 18:11
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move createEngine + SsrHandler inside the ResizeObserver callback
  so no synchronous offsetWidth/Height reads occur with asyncInit
- Emit 'reinit' after async init completes so framework wrappers
  re-evaluate canGoToNext/canGoToPrev (fixes hidden nav arrows)
- Guard cloneEngine against missing engine
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant