Skip to content

[MWPW-205194] - Perf Router Marquee: stop background autoplay on hidden viewports & sync slide across breakpoints - #6545

Open
DKos95 wants to merge 6 commits into
sr-perf-ddfrom
sr-perf-dd-rm-autoplay-leak-fix
Open

[MWPW-205194] - Perf Router Marquee: stop background autoplay on hidden viewports & sync slide across breakpoints#6545
DKos95 wants to merge 6 commits into
sr-perf-ddfrom
sr-perf-dd-rm-autoplay-leak-fix

Conversation

@DKos95

@DKos95 DKos95 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Background

The router marquee builds all three viewports (mobile/tablet/desktop) into the DOM at once, showing one at a time via CSS, with a separate autoplay controller per viewport created lazily on first view. Two related defects came out of that design.

Issue 1 — Autoplay leak on hidden viewports

Switching breakpoints (window resize, devtools open/close, orientation change) never stopped the previous viewport's controller. initViewportAutoplay only guarded against re-initializing the same viewport — it never paused the outgoing one. So each viewport a session ever touched kept its 5s autoplay timer and hero <video> running forever inside a display:none container, accumulating across breakpoint crossings.

Verified: resizing 1200 → 1400 → 500 left both the tablet and desktop videos playing (paused: false) while hidden. Directly counter to sr-perf-dd's perf goals (decoding/playing invisible video, re-running the slide-advance cycle indefinitely).

Issue 2 — Slide index not synced across breakpoints

Each viewport tracked its own independent slide index, so switching breakpoints landed on a different slide than the one that had been on screen (e.g. desktop auto-advanced to slide 2, resize to tablet showed slide 1). The viewports were never in sync.

Fixes

  1. One running controller at a time (leak fix). Replaced the initializedVps Set + autoplayControllers array with a controllersByVp Map. syncViewportAutoplay pauses the outgoing viewport's controller on every breakpoint change and resumes (or lazily creates) the incoming one, so only the visible viewport ever runs a timer/video. The scroll-based IntersectionObserver now pauses/resumes only the active viewport's controller.
  2. Carry the slide index across (sync fix). Controllers expose getActive() and syncTo(index); the handoff carries the outgoing viewport's live index into the incoming one via an instant, no-animation jump (a new instant flag reusing activate's reduced-motion path). Breakpoints now stay in lockstep.
  3. Skip first-frame gating for later viewports. The hero-video first-frame autoplay gate only matters for initial-load LCP, so a gateOnFirstFrame param applies it only to the very first viewport; viewports spun up later on resize start immediately.

Resolves: MWPW-205194

Test URLs:

Dusan Kosanovic and others added 2 commits August 24, 2026 11:33
…hanges

Switching viewports (mobile/tablet/desktop) never stopped the previous
viewport's autoplay controller, so its timer and hero video kept running
forever inside a display:none container. Track one controller per
viewport and pause the outgoing one / resume (or lazily create) the
incoming one instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each viewport (mobile/tablet/desktop) tracked its own slide index, so
switching breakpoints landed on a different slide than the one on screen
(e.g. desktop on slide 2 -> tablet showed slide 1). Controllers now expose
getActive()/syncTo(), and the viewport handoff carries the outgoing
viewport's active slide into the incoming one via an instant (no-animation)
jump. Viewports created later on resize also skip the hero first-frame gate,
which only matters for initial-load LCP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DKos95
DKos95 requested a review from a team August 24, 2026 11:30
@aem-code-sync

aem-code-sync Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

@rgclayton

Copy link
Copy Markdown
Contributor

The auto play timer doesn't seem to wait for the marquee to load/initialize. By the time it loads it's advancing to the 2nd slide. Is this expected or possibly a regression?

@DKos95

DKos95 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

The auto play timer doesn't seem to wait for the marquee to load/initialize. By the time it loads it's advancing to the 2nd slide. Is this expected or possibly a regression?

It's doing it with the site-redesign-foundation branch also so I would say this is already existing.

Comment thread libs/c2/blocks/router-marquee/router-marquee.js
@NadiiaSokolova
NadiiaSokolova requested a review from a team August 25, 2026 10:54
resume() lacked the prefersReducedMotion() check that beginAutoplay()
already has, so switching breakpoints back to a previously-created
viewport (e.g. mobile -> desktop -> mobile) could restart autoplay for
reduced-motion users.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DKos95
DKos95 requested a review from zagi25 August 25, 2026 11:54
const getActive = () => active;
// Jump straight to a slide with no transition, so a hidden viewport can be lined up
// with the one the user is leaving. clearFill resets the outgoing card's progress bar.
const syncTo = (index) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bounds check on index here. If one breakpoint has fewer authored slide rows than another and the user resizes over from a higher index, this calls activate() with an out-of-range index — newSlide (line 504) comes back undefined and newSlide.querySelector('video') throws.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks - clamped it: Math.min(index, slides.length - 1) before activating, so resizing into a viewport with fewer authored slides can't push it out of range anymore.


const resume = () => {
if (!paused) return;
if (!paused || prefersReducedMotion()) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the Play button permanently inert for reduced-motion users — it stays visible and clickable, but clicking it now silently does nothing. Intentional, or should manual resume still work under reduced motion (just don't auto-advance)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - resume() now takes a manual flag. A manual Play-button click still plays the video and flips the button under reduced motion, it just returns right after (before scheduling the auto-advance timer/progress-bar fill) so it doesn't auto-advance. System-triggered resumes (breakpoint switch, scroll back into view) are unaffected and still fully respect reduced motion.

controllersByVp.set(activeVp, controller);
}
if (carryIndex != null) controller.syncTo(carryIndex);
controller.resume();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This runs unconditionally on every breakpoint change, including for a controller the user explicitly paused before resizing. Doesn't this override an explicit user pause once they resize back? Might need a separate "user paused" flag that this checks before calling resume().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with a userPaused flag, set by every user-driven pause path (Play/Pause button, hover, swipe, reset/next buttons, click-through) and only cleared by a manual resume. A system resume (breakpoint switch or the scroll IntersectionObserver) now checks it and backs off instead of overriding an explicit pause.

if (gateOnFirstFrame && heroVideo && typeof heroVideo.requestVideoFrameCallback === 'function' && !prefersReducedMotion()) {
let started = false;
let fallbackTimer = null;
const kick = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and resume() (line 587) both schedule their own timer without clearing the other's. Scenario: a resize pauses this controller before its first-frame gate resolves, then a second resize brings it back (calling resume(), which sets a timer) — the still-pending kick() can later fire beginAutoplay() and schedule a second timer. The first one never gets cleared, so it fires on its own later too, causing an extra/early advance().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed - added clearTimeout(timer) here (and in resume()) before scheduling a new one, so a kick() that resolves after a resize has already resumed the controller clears the timer resume() set instead of leaving it to fire on its own later.

initViewportAutoplay();
syncViewportAutoplay();
requestAnimationFrame(() => dynamicLayoutUpdates(el));
let resizeRaf;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: libs/utils/action.js already exports debounce(), used the same way in pdf-space.js:1389 and menu.js:108 — could reuse that instead of hand-rolling the rAF throttle here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took the suggestion, with one adjustment: only dynamicLayoutUpdates() (pure layout measurement) is debounced. syncViewportAutoplay()/loadViewportVideos() stay un-debounced and run directly on every resize event - getActiveViewport() is just two cheap matchMedia checks, and debouncing the pause/resume sync itself would let the outgoing viewport's hidden video/timer keep running for the whole drag during a continuous resize, which is the exact leak this PR fixes.

@DKos95

DKos95 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor Author

@rgclayton Thanks a lot for the detailed review, will revisit performance PR's once the wave stuff is cleared.

@DKos95 DKos95 added the on hold label Aug 26, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

This PR has not been updated recently and will be closed in 7 days if no action is taken. Please ensure all checks are passing, https://github.com/orgs/adobecom/discussions/997 provides instructions. If the PR is ready to be merged, please mark it with the "Ready for Stage" label.

@github-actions github-actions Bot added the Stale label Sep 3, 2026
@DKos95 DKos95 removed the Stale label Sep 3, 2026
Clamp syncTo() to the incoming viewport's slide count, let a manual
Play click still play video under reduced motion (without scheduling
auto-advance), make a standing user pause survive breakpoint/scroll
resume via a userPaused flag, clear the autoplay timer before
rescheduling it to avoid a duplicate advance() firing, and split the
resize handler so viewport pause/resume sync stays immediate while
only the layout-measurement work is debounced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@DKos95
DKos95 requested a review from rgclayton September 3, 2026 15:57
@DKos95

DKos95 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@rgclayton finally got some time to update these performance tickets, thanks again for the suggestions!

@DKos95 DKos95 removed the on hold label Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants