mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 12:28:12 +00:00
perf(player): share PLAYER_STYLES via adoptedStyleSheets (#394)
## Summary Replace per-instance `<style>` injection in `<hyperframes-player>` with a lazily constructed `CSSStyleSheet` adopted via `shadowRoot.adoptedStyleSheets`. One parsed stylesheet, many adopters — the studio thumbnail grid renders dozens of players concurrently and was paying for N parses of the same CSS. ## Why Step `P1-1` of the player perf proposal. The previous implementation appended a `<style>` element to every shadow root, which means: - N shadow roots → N copies of the same CSS string parsed into N independent style sheets. - Each `<style>` lives in the DOM and contributes to layout/style invalidation work when its shadow root churns. - The studio's project grid mounts ~30 players on initial load — that's 30 redundant parses of the same ~1 KB stylesheet on the critical path. `adoptedStyleSheets` flips this: parse once at module load, hand the same `CSSStyleSheet` reference to every shadow root. ## What changed - New `getSharedPlayerStyleSheet()` in `packages/player/src/styles.ts` — module-scoped and memoized; the sheet is built once per process and returned to every adopter. - New `applyPlayerStyles(shadow)` is the single integration point. It **appends** (never replaces) the shared sheet so any pre-adopted sheets — host themes, scoped overrides, future caller-side injections — survive intact, and is idempotent so repeated calls don't multiply adoptions. - SSR-safe via a `typeof CSSStyleSheet` guard. Failures (e.g. `replaceSync` throw, no constructor) are cached as `null` so we don't retry constructor failures forever. - Defensive fallback path creates a per-instance `<style>` element when `adoptedStyleSheets` is unavailable (older runtimes, hostile environments). Behavior on those paths is unchanged from before. - `PLAYER_STYLES`, `PLAY_ICON`, and `PAUSE_ICON` exports preserved — no public API change. ## Test plan - [x] Unit tests in `styles.test.ts` cover sharing across instances, fallback when `CSSStyleSheet` is undefined or `replaceSync` throws, fallback when `adoptedStyleSheets` is unsupported on the shadow root, idempotency, and preservation of pre-existing adopted sheets. - [x] Integration test in `hyperframes-player.test.ts` confirms two real `<hyperframes-player>` elements adopt the same `CSSStyleSheet` instance and inject zero `<style>` elements. - [x] Build size delta is negligible (utility code replaces `container.appendChild` calls). ## Stack Step `P1-1` of the player perf proposal. Followed by `P1-2` (scoping the media `MutationObserver`) and `P1-4` (coalescing parent media-time mirror writes) — all three target the studio multi-player render path.
This commit is contained in:
@@ -199,3 +199,80 @@ export const PLAYER_STYLES = /* css */ `
|
||||
|
||||
export const PLAY_ICON = `<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><polygon points="4,2 16,9 4,16"/></svg>`;
|
||||
export const PAUSE_ICON = `<svg width="24" height="24" viewBox="0 0 18 18" fill="currentColor"><rect x="3" y="2" width="4" height="14"/><rect x="11" y="2" width="4" height="14"/></svg>`;
|
||||
|
||||
/**
|
||||
* Process-wide cache for the constructed PLAYER_STYLES sheet. Lazy so the
|
||||
* module stays SSR-safe (CSSStyleSheet is window-scoped) and so a single
|
||||
* sheet can be shared across every shadow root via `adoptedStyleSheets` —
|
||||
* the studio thumbnail grid renders dozens of players, and avoiding N
|
||||
* duplicate `<style>` parses + style-recalc invalidations is the win here.
|
||||
*
|
||||
* `null` after a failed construction attempt = "fall back forever in this
|
||||
* process" (the usual cause is a missing constructor in older runtimes;
|
||||
* retrying every call would just throw the same way).
|
||||
*/
|
||||
let sharedSheet: CSSStyleSheet | null | undefined;
|
||||
|
||||
/**
|
||||
* Returns the shared player stylesheet, or `null` if constructable
|
||||
* stylesheets aren't available in this environment.
|
||||
*
|
||||
* The result is memoized for the life of the module — every shadow root
|
||||
* adopts the same `CSSStyleSheet` instance.
|
||||
*/
|
||||
export function getSharedPlayerStyleSheet(): CSSStyleSheet | null {
|
||||
if (sharedSheet !== undefined) return sharedSheet;
|
||||
|
||||
if (typeof CSSStyleSheet === "undefined") {
|
||||
sharedSheet = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const sheet = new CSSStyleSheet();
|
||||
sheet.replaceSync(PLAYER_STYLES);
|
||||
sharedSheet = sheet;
|
||||
return sheet;
|
||||
} catch {
|
||||
sharedSheet = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal hook for tests to clear the memoized sheet. Not part of the
|
||||
* public API.
|
||||
*/
|
||||
export function _resetSharedPlayerStyleSheet(): void {
|
||||
sharedSheet = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install PLAYER_STYLES into a player shadow root. Prefers the shared
|
||||
* constructable stylesheet (one parse, one rule tree, N adopters) and
|
||||
* falls back to a per-instance `<style>` element when the host runtime
|
||||
* lacks `adoptedStyleSheets` support.
|
||||
*
|
||||
* Idempotent: re-applying to a root that already adopts the shared sheet
|
||||
* is a no-op. Pre-existing adopted sheets are preserved (we append, never
|
||||
* replace), so callers further up the chain can keep their styles.
|
||||
*/
|
||||
export function applyPlayerStyles(shadow: ShadowRoot): void {
|
||||
const sheet = getSharedPlayerStyleSheet();
|
||||
const adopted = (shadow as ShadowRoot & { adoptedStyleSheets?: CSSStyleSheet[] })
|
||||
.adoptedStyleSheets;
|
||||
|
||||
if (sheet && Array.isArray(adopted)) {
|
||||
if (!adopted.includes(sheet)) {
|
||||
(shadow as ShadowRoot & { adoptedStyleSheets: CSSStyleSheet[] }).adoptedStyleSheets = [
|
||||
...adopted,
|
||||
sheet,
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.textContent = PLAYER_STYLES;
|
||||
shadow.appendChild(style);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user