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 <dermot.duffy@gmail.com>
This commit is contained in:
co-authored by
dermotduffy
parent
ae8b1134c9
commit
4fceade37c
@@ -3,6 +3,7 @@ import Masonry from 'masonry-layout';
|
|||||||
import { ViewDisplayConfig } from '../config/schema/common/display';
|
import { ViewDisplayConfig } from '../config/schema/common/display';
|
||||||
import { MediaLoadedInfo } from '../types';
|
import { MediaLoadedInfo } from '../types';
|
||||||
import {
|
import {
|
||||||
|
forceReflow,
|
||||||
getChildrenFromElement,
|
getChildrenFromElement,
|
||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
setOrRemoveStyleProperty,
|
setOrRemoveStyleProperty,
|
||||||
@@ -58,13 +59,10 @@ export class MediaGridController {
|
|||||||
private _idAttribute: string;
|
private _idAttribute: string;
|
||||||
private _widthFactorAttribute: string;
|
private _widthFactorAttribute: string;
|
||||||
|
|
||||||
private _throttledLayout = throttle(
|
private _throttledLayout = throttle(() => this._masonry?.layout?.(), 300, {
|
||||||
() => this._masonry?.layout?.(),
|
leading: true,
|
||||||
// Throttle layout calls to larger than the masonry.js transitionDuration
|
trailing: true,
|
||||||
// value specified below.
|
});
|
||||||
300,
|
|
||||||
{ trailing: true, leading: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the order in which the observers are declared changes, the unittest must
|
// If the order in which the observers are declared changes, the unittest must
|
||||||
// be updated in triggerResizeObserver and triggerMutationObserver.
|
// be updated in triggerResizeObserver and triggerMutationObserver.
|
||||||
@@ -186,10 +184,7 @@ export class MediaGridController {
|
|||||||
this._sortItemsInGrid();
|
this._sortItemsInGrid();
|
||||||
this._updateSelectedStylesOnElements();
|
this._updateSelectedStylesOnElements();
|
||||||
|
|
||||||
// Sizes and positions may change when an element is selected, so re-do the
|
this._forceLayout();
|
||||||
// layout (must come after the call to _updateStylesOnElements in order to
|
|
||||||
// ensure the right styles are applied first).
|
|
||||||
this._throttledLayout();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public unselectAll() {
|
public unselectAll() {
|
||||||
@@ -199,6 +194,19 @@ export class MediaGridController {
|
|||||||
}
|
}
|
||||||
this._selected = null;
|
this._selected = null;
|
||||||
this._updateSelectedStylesOnElements();
|
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 => {
|
private _calculateGridContentsFromHost = (): void => {
|
||||||
@@ -318,7 +326,11 @@ export class MediaGridController {
|
|||||||
columnWidth: this._getColumnSize(),
|
columnWidth: this._getColumnSize(),
|
||||||
initLayout: false,
|
initLayout: false,
|
||||||
percentPosition: true,
|
percentPosition: true,
|
||||||
transitionDuration: '0.2s',
|
transitionDuration: 0,
|
||||||
|
stagger: 0,
|
||||||
|
|
||||||
|
// This controller handles resizes.
|
||||||
|
resize: false,
|
||||||
gutter: MEDIA_GRID_HORIZONTAL_GUTTER_WIDTH,
|
gutter: MEDIA_GRID_HORIZONTAL_GUTTER_WIDTH,
|
||||||
}) as ExtendedMasonry;
|
}) as ExtendedMasonry;
|
||||||
this._masonry.addItems?.([...this._gridContents.values()]);
|
this._masonry.addItems?.([...this._gridContents.values()]);
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
|
|
||||||
vi.mock('lodash-es', async () => ({
|
vi.mock('lodash-es', async () => ({
|
||||||
...(await vi.importActual('lodash-es')),
|
...(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<ExtendedMasonry>();
|
const masonry = mock<ExtendedMasonry>();
|
||||||
@@ -394,7 +398,7 @@ describe('MediaGridController', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
initLayout: false,
|
initLayout: false,
|
||||||
percentPosition: true,
|
percentPosition: true,
|
||||||
transitionDuration: '0.2s',
|
transitionDuration: 0,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user