Merge pull request #4177 from element-hq/ignore-layout-updates
Performance: Ignore redundant layout updates
This commit is contained in:
@@ -79,7 +79,7 @@ import {
|
|||||||
type ReactionInfo,
|
type ReactionInfo,
|
||||||
type ReactionOption,
|
type ReactionOption,
|
||||||
} from "../../reactions";
|
} from "../../reactions";
|
||||||
import { shallowEquals } from "../../utils/array";
|
import { shallowEquals as shallowArrayEquals } from "../../utils/array";
|
||||||
import { type MediaDevices } from "../MediaDevices";
|
import { type MediaDevices } from "../MediaDevices";
|
||||||
import { constant, type Behavior } from "../Behavior";
|
import { constant, type Behavior } from "../Behavior";
|
||||||
import { E2eeType } from "../../e2ee/e2eeType";
|
import { E2eeType } from "../../e2ee/e2eeType";
|
||||||
@@ -89,6 +89,7 @@ import { getUrlParams, HeaderStyle } from "../../UrlParams";
|
|||||||
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
import { type ProcessorState } from "../../livekit/TrackProcessorContext";
|
||||||
import { ElementWidgetActions, widget } from "../../widget";
|
import { ElementWidgetActions, widget } from "../../widget";
|
||||||
import {
|
import {
|
||||||
|
layoutShallowEquals,
|
||||||
type Alignment,
|
type Alignment,
|
||||||
type GridLayoutMedia,
|
type GridLayoutMedia,
|
||||||
type Layout,
|
type Layout,
|
||||||
@@ -941,7 +942,7 @@ export function createCallViewModel$(
|
|||||||
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m),
|
bins.sort(([, bin1], [, bin2]) => bin1 - bin2).map(([m]) => m),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
distinctUntilChanged(shallowEquals),
|
distinctUntilChanged(shallowArrayEquals),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1000,7 +1001,7 @@ export function createCallViewModel$(
|
|||||||
const spotlight$ = scope.behavior<MediaViewModel[]>(
|
const spotlight$ = scope.behavior<MediaViewModel[]>(
|
||||||
spotlightAndPip$.pipe(
|
spotlightAndPip$.pipe(
|
||||||
map(({ spotlight }) => spotlight),
|
map(({ spotlight }) => spotlight),
|
||||||
distinctUntilChanged<MediaViewModel[]>(shallowEquals),
|
distinctUntilChanged<MediaViewModel[]>(shallowArrayEquals),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1209,6 +1210,7 @@ export function createCallViewModel$(
|
|||||||
}
|
}
|
||||||
return layout;
|
return layout;
|
||||||
}),
|
}),
|
||||||
|
distinctUntilChanged(),
|
||||||
scope.bind(),
|
scope.bind(),
|
||||||
)
|
)
|
||||||
.subscribe((orientation) => {
|
.subscribe((orientation) => {
|
||||||
@@ -1574,7 +1576,11 @@ export function createCallViewModel$(
|
|||||||
* The layout of tiles in the call interface.
|
* The layout of tiles in the call interface.
|
||||||
*/
|
*/
|
||||||
const layout$ = scope.behavior<Layout>(
|
const layout$ = scope.behavior<Layout>(
|
||||||
layoutInternals$.pipe(map(({ layout }) => layout)),
|
layoutInternals$.pipe(
|
||||||
|
map(({ layout }) => layout),
|
||||||
|
// Drop redundant layout updates before they would hit React.
|
||||||
|
distinctUntilChanged<Layout>(layoutShallowEquals),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const overflowing$ = scope.behavior<boolean>(
|
const overflowing$ = scope.behavior<boolean>(
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2026 Element Creations Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
type Alignment,
|
||||||
|
layoutShallowEquals,
|
||||||
|
type Layout,
|
||||||
|
} from "./layout-types";
|
||||||
|
import {
|
||||||
|
type SpotlightTileViewModel,
|
||||||
|
type GridTileViewModel,
|
||||||
|
} from "./TileViewModel";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
const spotlightTile = {} as unknown as SpotlightTileViewModel;
|
||||||
|
const gridTile = {} as unknown as GridTileViewModel;
|
||||||
|
const pipAlignment$ = new BehaviorSubject<Alignment>({
|
||||||
|
inline: "end",
|
||||||
|
block: "end",
|
||||||
|
});
|
||||||
|
|
||||||
|
const spotlightExpanded: Layout = {
|
||||||
|
type: "spotlight-expanded",
|
||||||
|
spotlight: spotlightTile,
|
||||||
|
pipAlignment$,
|
||||||
|
};
|
||||||
|
|
||||||
|
const spotlightPortrait: Layout = {
|
||||||
|
type: "spotlight-portrait",
|
||||||
|
spotlight: spotlightTile,
|
||||||
|
grid: [gridTile],
|
||||||
|
setVisibleTiles: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
test("layoutShallowEquals considers a layout to be equal to its shallow clone", () =>
|
||||||
|
expect(layoutShallowEquals(spotlightExpanded, { ...spotlightExpanded })).toBe(
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
test("layoutShallowEquals detects a missing key", () => {
|
||||||
|
expect(
|
||||||
|
layoutShallowEquals(spotlightExpanded, {
|
||||||
|
...spotlightExpanded,
|
||||||
|
pip: gridTile,
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
layoutShallowEquals(
|
||||||
|
{ ...spotlightExpanded, pip: gridTile },
|
||||||
|
spotlightExpanded,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("layoutShallowEquals considers grid arrays with equal contents to be equal", () =>
|
||||||
|
expect(
|
||||||
|
layoutShallowEquals(spotlightPortrait, {
|
||||||
|
...spotlightPortrait,
|
||||||
|
grid: [...spotlightPortrait.grid],
|
||||||
|
}),
|
||||||
|
).toBe(true));
|
||||||
|
|
||||||
|
test("layoutShallowEquals detects grid arrays with different contents", () =>
|
||||||
|
expect(
|
||||||
|
layoutShallowEquals(spotlightPortrait, {
|
||||||
|
...spotlightPortrait,
|
||||||
|
grid: [...spotlightPortrait.grid, gridTile],
|
||||||
|
}),
|
||||||
|
).toBe(false));
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
type SpotlightTileViewModel,
|
type SpotlightTileViewModel,
|
||||||
} from "./TileViewModel.ts";
|
} from "./TileViewModel.ts";
|
||||||
import { type Behavior } from "./Behavior.ts";
|
import { type Behavior } from "./Behavior.ts";
|
||||||
|
import { shallowEquals as arrayShallowEquals } from "../utils/array.ts";
|
||||||
|
|
||||||
export interface GridLayoutMedia {
|
export interface GridLayoutMedia {
|
||||||
type: "grid";
|
type: "grid";
|
||||||
@@ -140,3 +141,30 @@ export type Layout =
|
|||||||
| OneOnOneDesktopLayout
|
| OneOnOneDesktopLayout
|
||||||
| OneOnOneMobileLayout
|
| OneOnOneMobileLayout
|
||||||
| PipLayout;
|
| PipLayout;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests whether the top-level properties and array elements of layout `a` are
|
||||||
|
* equal to those of layout `b`. Useful for deduping redundant layout updates.
|
||||||
|
*/
|
||||||
|
export function layoutShallowEquals(a: Layout, b: Layout): boolean {
|
||||||
|
// If a and b have the same number of keys and every key in a is also in b,
|
||||||
|
// then they have the same keys.
|
||||||
|
const aKeys = Object.keys(a);
|
||||||
|
const bKeys = Object.keys(b);
|
||||||
|
if (aKeys.length !== bKeys.length) return false;
|
||||||
|
|
||||||
|
for (const key of aKeys) {
|
||||||
|
if (!(key in b)) return false;
|
||||||
|
|
||||||
|
// Now check that they have the same values.
|
||||||
|
const aValue = (a as any)[key];
|
||||||
|
const bValue = (b as any)[key];
|
||||||
|
if (Array.isArray(aValue) && Array.isArray(bValue)) {
|
||||||
|
// Special case for arrays so we can detect when the grid tiles arrays are
|
||||||
|
// essentially the same.
|
||||||
|
if (!arrayShallowEquals(aValue, bValue)) return false;
|
||||||
|
} else if (aValue !== bValue) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user