Skip to content

Commit 21164f1

Browse files
authored
fix(perf): Chunk and lazy load BackgroundFiltersProvider (#2040)
With the introduction of the revamped video filters in #1977, our SDK grew by ~250KB after minification. While filters are an optional feature, many bundlers were unnecessarily bundling them in the main app chunk, slightly increasing the JS download and parsing time. With this PR, `BackgroundFiltersProvider` and the content of the `@stream-io/video-filters-web` package live in a separate, lazily loaded chunk of our SDK. This trick moves those ~250KB outside of the main bundle, hence restoring the previous performance and size constraints. ### Implementation notes Some code restructuring was necessary, so now we have: - `types.ts` - a shared module containing all types, used in both chunks - `BackgroundFiltersProvider.tsx` - code splitting happens here - `BackgroundFilters.tsx` - the original, slightly slimmed down implementation
1 parent c4adc2a commit 21164f1

File tree

6 files changed

+281
-227
lines changed

6 files changed

+281
-227
lines changed

packages/react-sdk/rollup.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const chunkFileNames = (chunkInfo) => {
1111
if (chunkInfo.name.includes('CallStatsLatencyChart')) {
1212
return 'latency-chart-[hash].[format].js';
1313
}
14+
if (chunkInfo.name.includes('BackgroundFilters')) {
15+
return 'background-filters-[hash].[format].js';
16+
}
1417
return '[name]-[hash].[format].js';
1518
};
1619

packages/react-sdk/src/components/BackgroundFilters/BackgroundFilters.tsx

Lines changed: 50 additions & 219 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {
2-
createContext,
2+
Context,
33
PropsWithChildren,
44
useCallback,
5-
useContext,
65
useEffect,
76
useMemo,
87
useRef,
@@ -13,19 +12,23 @@ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings';
1312
import { Call, disposeOfMediaStream } from '@stream-io/video-client';
1413
import {
1514
BackgroundBlurLevel,
16-
BackgroundFilter,
1715
createRenderer,
18-
isPlatformSupported,
1916
isMediaPipePlatformSupported,
20-
loadTFLite,
17+
isPlatformSupported,
2118
loadMediaPipe,
22-
PlatformSupportFlags,
23-
VirtualBackground,
19+
loadTFLite,
20+
PerformanceStats,
2421
Renderer,
2522
TFLite,
26-
PerformanceStats,
23+
VirtualBackground,
2724
} from '@stream-io/video-filters-web';
2825
import clsx from 'clsx';
26+
import type {
27+
BackgroundFiltersPerformance,
28+
BackgroundFiltersProps,
29+
BackgroundFiltersContextValue,
30+
PerformanceDegradationReason,
31+
} from './types';
2932

3033
/**
3134
* Constants for FPS warning calculation.
@@ -39,32 +42,6 @@ const DEFAULT_FPS = 30;
3942
const DEVIATION_LIMIT = 0.5;
4043
const OUTLIER_PERSISTENCE = 5;
4144

42-
/**
43-
* Configuration for performance metric thresholds.
44-
*/
45-
export type BackgroundFiltersPerformanceThresholds = {
46-
/**
47-
* The lower FPS threshold for triggering a performance warning.
48-
* When the EMA FPS falls below this value, a warning is shown.
49-
* @default 23
50-
*/
51-
fpsWarningThresholdLower?: number;
52-
53-
/**
54-
* The upper FPS threshold for clearing a performance warning.
55-
* When the EMA FPS rises above this value, the warning is cleared.
56-
* @default 25
57-
*/
58-
fpsWarningThresholdUpper?: number;
59-
60-
/**
61-
* The default FPS value used as the initial value for the EMA (Exponential Moving Average)
62-
* calculation and when stats are unavailable or when resetting the filter.
63-
* @default 30
64-
*/
65-
defaultFps?: number;
66-
};
67-
6845
/**
6946
* Represents the available background filter processing engines.
7047
*/
@@ -74,160 +51,6 @@ enum FilterEngine {
7451
NONE,
7552
}
7653

77-
/**
78-
* Represents the possible reasons for background filter performance degradation.
79-
*/
80-
export enum PerformanceDegradationReason {
81-
FRAME_DROP = 'frame-drop',
82-
CPU_THROTTLING = 'cpu-throttling',
83-
}
84-
85-
export type BackgroundFiltersProps = PlatformSupportFlags & {
86-
/**
87-
* A list of URLs to use as background images.
88-
*/
89-
backgroundImages?: string[];
90-
91-
/**
92-
* The background filter to apply to the video (by default).
93-
* @default undefined no filter applied
94-
*/
95-
backgroundFilter?: BackgroundFilter;
96-
97-
/**
98-
* The URL of the image to use as the background (by default).
99-
*/
100-
backgroundImage?: string;
101-
102-
/**
103-
* The level of blur to apply to the background (by default).
104-
* @default 'high'.
105-
*/
106-
backgroundBlurLevel?: BackgroundBlurLevel;
107-
108-
/**
109-
* The base path for the TensorFlow Lite files.
110-
* @default 'https://unpkg.com/@stream-io/video-filters-web/mediapipe'.
111-
*/
112-
basePath?: string;
113-
114-
/**
115-
* The path to the TensorFlow Lite WebAssembly file.
116-
*
117-
* Override this prop to use a custom path to the TensorFlow Lite WebAssembly file
118-
* (e.g., if you choose to host it yourself).
119-
*/
120-
tfFilePath?: string;
121-
122-
/**
123-
* The path to the MediaPipe model file.
124-
* Override this prop to use a custom path to the MediaPipe model file
125-
* (e.g., if you choose to host it yourself).
126-
*/
127-
modelFilePath?: string;
128-
129-
/**
130-
* When true, the filter uses the legacy TensorFlow-based segmentation model.
131-
* When false, it uses the default MediaPipe Tasks Vision model.
132-
*
133-
* Only enable this if you need to mimic the behavior of older SDK versions.
134-
*/
135-
useLegacyFilter?: boolean;
136-
137-
/**
138-
* When a started filter encounters an error, this callback will be executed.
139-
* The default behavior (not overridable) is unregistering a failed filter.
140-
* Use this callback to display UI error message, disable the corresponding stream,
141-
* or to try registering the filter again.
142-
*/
143-
onError?: (error: any) => void;
144-
145-
/**
146-
* Configuration for performance metric thresholds.
147-
* Use this to customize when performance warnings are triggered.
148-
*/
149-
performanceThresholds?: BackgroundFiltersPerformanceThresholds;
150-
};
151-
152-
/**
153-
* Performance degradation information for background filters.
154-
*
155-
* Performance is calculated using an Exponential Moving Average (EMA) of FPS values
156-
* to smooth out quick spikes and provide stable performance warnings.
157-
*/
158-
export type BackgroundFiltersPerformance = {
159-
/**
160-
* Whether performance is currently degraded.
161-
*/
162-
degraded: boolean;
163-
/**
164-
* Reasons for performance degradation.
165-
*/
166-
reason?: Array<PerformanceDegradationReason>;
167-
};
168-
169-
export type BackgroundFiltersAPI = {
170-
/**
171-
* Whether the current platform supports the background filters.
172-
*/
173-
isSupported: boolean;
174-
175-
/**
176-
* Indicates whether the background filters engine is loaded and ready.
177-
*/
178-
isReady: boolean;
179-
180-
/**
181-
* Performance information for background filters.
182-
*/
183-
performance: BackgroundFiltersPerformance;
184-
185-
/**
186-
* Disables all background filters applied to the video.
187-
*/
188-
disableBackgroundFilter: () => void;
189-
190-
/**
191-
* Applies a background blur filter to the video.
192-
*
193-
* @param blurLevel the level of blur to apply to the background.
194-
*/
195-
applyBackgroundBlurFilter: (blurLevel: BackgroundBlurLevel) => void;
196-
197-
/**
198-
* Applies a background image filter to the video.
199-
*
200-
* @param imageUrl the URL of the image to use as the background.
201-
*/
202-
applyBackgroundImageFilter: (imageUrl: string) => void;
203-
};
204-
205-
/**
206-
* The context value for the background filters context.
207-
*/
208-
export type BackgroundFiltersContextValue = BackgroundFiltersProps &
209-
BackgroundFiltersAPI;
210-
211-
/**
212-
* The context for the background filters.
213-
*/
214-
const BackgroundFiltersContext = createContext<
215-
BackgroundFiltersContextValue | undefined
216-
>(undefined);
217-
218-
/**
219-
* A hook to access the background filters context API.
220-
*/
221-
export const useBackgroundFilters = () => {
222-
const context = useContext(BackgroundFiltersContext);
223-
if (!context) {
224-
throw new Error(
225-
'useBackgroundFilters must be used within a BackgroundFiltersProvider',
226-
);
227-
}
228-
return context;
229-
};
230-
23154
/**
23255
* Determines which filter engine is available.
23356
* MEDIA_PIPE is the default unless legacy filters are requested or MediaPipe is unsupported.
@@ -239,12 +62,11 @@ const determineEngine = async (
23962
forceSafariSupport: boolean | undefined,
24063
forceMobileSupport: boolean | undefined,
24164
): Promise<FilterEngine> => {
242-
const isTfPlatformSupported = await isPlatformSupported({
243-
forceSafariSupport,
244-
forceMobileSupport,
245-
});
246-
24765
if (useLegacyFilter) {
66+
const isTfPlatformSupported = await isPlatformSupported({
67+
forceSafariSupport,
68+
forceMobileSupport,
69+
});
24870
return isTfPlatformSupported ? FilterEngine.TF : FilterEngine.NONE;
24971
}
25072

@@ -263,9 +85,15 @@ const determineEngine = async (
26385
* in your project before using this component.
26486
*/
26587
export const BackgroundFiltersProvider = (
266-
props: PropsWithChildren<BackgroundFiltersProps>,
88+
props: PropsWithChildren<BackgroundFiltersProps> & {
89+
// for code splitting. Prevents circular dependency issues where
90+
// this Context needs to be present in the main chunk, but also
91+
// imported by the background filters chunk.
92+
ContextProvider: Context<BackgroundFiltersContextValue | undefined>;
93+
},
26794
) => {
26895
const {
96+
ContextProvider,
26997
children,
27098
backgroundImages = [],
27199
backgroundFilter: bgFilterFromProps = undefined,
@@ -340,7 +168,7 @@ export const BackgroundFiltersProvider = (
340168
const reasons: Array<PerformanceDegradationReason> = [];
341169

342170
if (showLowFpsWarning) {
343-
reasons.push(PerformanceDegradationReason.FRAME_DROP);
171+
reasons.push('frame-drop');
344172
}
345173

346174
const qualityLimitationReasons =
@@ -351,7 +179,7 @@ export const BackgroundFiltersProvider = (
351179
qualityLimitationReasons &&
352180
qualityLimitationReasons?.includes('cpu')
353181
) {
354-
reasons.push(PerformanceDegradationReason.CPU_THROTTLING);
182+
reasons.push('cpu-throttling');
355183
}
356184

357185
return {
@@ -458,52 +286,54 @@ export const BackgroundFiltersProvider = (
458286
);
459287

460288
const isReady = useLegacyFilter ? !!tfLite : !!mediaPipe;
289+
const contextValue: BackgroundFiltersContextValue = {
290+
isSupported,
291+
performance,
292+
isReady,
293+
backgroundImage,
294+
backgroundBlurLevel,
295+
backgroundFilter,
296+
disableBackgroundFilter,
297+
applyBackgroundBlurFilter,
298+
applyBackgroundImageFilter,
299+
backgroundImages,
300+
tfFilePath,
301+
modelFilePath,
302+
basePath,
303+
onError: handleError,
304+
};
461305
return (
462-
<BackgroundFiltersContext.Provider
463-
value={{
464-
isSupported,
465-
performance,
466-
isReady,
467-
backgroundImage,
468-
backgroundBlurLevel,
469-
backgroundFilter,
470-
disableBackgroundFilter,
471-
applyBackgroundBlurFilter,
472-
applyBackgroundImageFilter,
473-
backgroundImages,
474-
tfFilePath,
475-
modelFilePath,
476-
basePath,
477-
onError: handleError,
478-
}}
479-
>
306+
<ContextProvider.Provider value={contextValue}>
480307
{children}
481308
{isReady && (
482309
<BackgroundFilters
310+
api={contextValue}
483311
tfLite={tfLite}
484312
engine={engine}
485313
onStats={handleStats}
486314
/>
487315
)}
488-
</BackgroundFiltersContext.Provider>
316+
</ContextProvider.Provider>
489317
);
490318
};
491319

492320
const BackgroundFilters = (props: {
321+
api: BackgroundFiltersContextValue;
493322
tfLite?: TFLite;
494323
engine: FilterEngine;
495324
onStats: (stats: PerformanceStats) => void;
496325
}) => {
497326
const call = useCall();
498-
const { children, start } = useRenderer(props.tfLite, call, props.engine);
499-
const { onError, backgroundFilter } = useBackgroundFilters();
327+
const { engine, api, tfLite, onStats } = props;
328+
const { children, start } = useRenderer(api, tfLite, call, engine);
329+
const { onError, backgroundFilter } = api;
500330
const handleErrorRef = useRef<((error: any) => void) | undefined>(undefined);
501331
handleErrorRef.current = onError;
502332

503333
const handleStatsRef = useRef<
504334
((stats: PerformanceStats) => void) | undefined
505335
>(undefined);
506-
handleStatsRef.current = props.onStats;
336+
handleStatsRef.current = onStats;
507337

508338
useEffect(() => {
509339
if (!call || !backgroundFilter) return;
@@ -524,6 +354,7 @@ const BackgroundFilters = (props: {
524354
};
525355

526356
const useRenderer = (
357+
api: BackgroundFiltersContextValue,
527358
tfLite: TFLite | undefined,
528359
call: Call | undefined,
529360
engine: FilterEngine,
@@ -534,7 +365,7 @@ const useRenderer = (
534365
backgroundImage,
535366
modelFilePath,
536367
basePath,
537-
} = useBackgroundFilters();
368+
} = api;
538369

539370
const videoRef = useRef<HTMLVideoElement>(null);
540371
const canvasRef = useRef<HTMLCanvasElement>(null);

0 commit comments

Comments
 (0)