From 4fceade37cd452b2de84bdc4e8967fa2d3b1ab6e Mon Sep 17 00:00:00 2001 From: Miguel Angel Nubla Date: Mon, 9 Mar 2026 04:35:52 +0100 Subject: [PATCH] perf(grid): improve layout responsiveness and remove selection transitions (#2400) This PR addresses performance issues in the media grid by disabling resource-intensive CSS transitions and ensuring immediate layout updates. Previously, the default `0.2s` transition duration for grid reshuffling was causing browser overloads on devices with multiple live video viewers, leading to stuttering and occasionally missing short-lived video events. ## Changes ### 1. Disable Masonry Transitions - Set `transitionDuration` to `0` in the Masonry configuration to eliminate CSS-driven animations during layout. - Set `stagger` to `0` to ensure all items move simultaneously and instantly. - Set `resize` to `false` in the Masonry config, as window resizing is already efficiently handled by the controller's `ResizeObserver`. ### 2. Immediate Layout Updates - Introduced a `_forceLayout()` method in `MediaGridController` that: - Cancels any pending throttled layout calls to avoid redundant work. - Triggers a browser reflow via `getBoundingClientRect()` to ensure all element dimensions are synchronized before the next draw. - Forces an immediate Masonry layout. - Integrated `_forceLayout()` into `selectCell()` and `unselectAll()` to guarantee the grid updates at the exact moment a user interacts with it. ### 3. Test Alignment - Updated `tests/components-lib/media-grid-controller.test.ts` to reflect the changes. ## Performance Impact By eliminating timed transitions, we significantly reduce the peak CPU and GPU load during grid selection changes. This results in a much snappier UI where video viewers snap into position instantly, preventing the "shuttering" effect and ensuring that time-critical video playback is not interrupted by heavy animation cycles. --------- Co-authored-by: dermotduffy --- src/components-lib/media-grid-controller.ts | 36 ++++++++++++------- .../media-grid-controller.test.ts | 8 +++-- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/components-lib/media-grid-controller.ts b/src/components-lib/media-grid-controller.ts index 7d4ac9fe..1c8b06cf 100644 --- a/src/components-lib/media-grid-controller.ts +++ b/src/components-lib/media-grid-controller.ts @@ -3,6 +3,7 @@ import Masonry from 'masonry-layout'; import { ViewDisplayConfig } from '../config/schema/common/display'; import { MediaLoadedInfo } from '../types'; import { + forceReflow, getChildrenFromElement, setOrRemoveAttribute, setOrRemoveStyleProperty, @@ -58,13 +59,10 @@ export class MediaGridController { private _idAttribute: string; private _widthFactorAttribute: string; - private _throttledLayout = throttle( - () => this._masonry?.layout?.(), - // Throttle layout calls to larger than the masonry.js transitionDuration - // value specified below. - 300, - { trailing: true, leading: false }, - ); + private _throttledLayout = throttle(() => this._masonry?.layout?.(), 300, { + leading: true, + trailing: true, + }); // If the order in which the observers are declared changes, the unittest must // be updated in triggerResizeObserver and triggerMutationObserver. @@ -186,10 +184,7 @@ export class MediaGridController { this._sortItemsInGrid(); this._updateSelectedStylesOnElements(); - // Sizes and positions may change when an element is selected, so re-do the - // layout (must come after the call to _updateStylesOnElements in order to - // ensure the right styles are applied first). - this._throttledLayout(); + this._forceLayout(); } public unselectAll() { @@ -199,6 +194,19 @@ export class MediaGridController { } this._selected = null; this._updateSelectedStylesOnElements(); + + this._forceLayout(); + } + + protected _forceLayout(): void { + // Cancel possible pending layout + this._throttledLayout.cancel(); + + // Force browser reflow so masonry measures the updated element size + forceReflow(this._host); + + // Sizes and positions may change when an element is selected, so re-do the layout + this._masonry?.layout?.(); } private _calculateGridContentsFromHost = (): void => { @@ -318,7 +326,11 @@ export class MediaGridController { columnWidth: this._getColumnSize(), initLayout: false, percentPosition: true, - transitionDuration: '0.2s', + transitionDuration: 0, + stagger: 0, + + // This controller handles resizes. + resize: false, gutter: MEDIA_GRID_HORIZONTAL_GUTTER_WIDTH, }) as ExtendedMasonry; this._masonry.addItems?.([...this._gridContents.values()]); diff --git a/tests/components-lib/media-grid-controller.test.ts b/tests/components-lib/media-grid-controller.test.ts index fc91034d..4e4c8edd 100644 --- a/tests/components-lib/media-grid-controller.test.ts +++ b/tests/components-lib/media-grid-controller.test.ts @@ -17,7 +17,11 @@ import { vi.mock('lodash-es', async () => ({ ...(await vi.importActual('lodash-es')), - throttle: vi.fn((fn) => fn), + throttle: vi.fn((fn) => { + const throttled = vi.fn(fn) as unknown as { cancel: () => void }; + throttled.cancel = vi.fn(); + return throttled; + }), })); const masonry = mock(); @@ -394,7 +398,7 @@ describe('MediaGridController', () => { expect.objectContaining({ initLayout: false, percentPosition: true, - transitionDuration: '0.2s', + transitionDuration: 0, }), ); });