From 405a3dfb31235c1f2174f9539954cfe0c40fd0bb Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 3 Jun 2026 14:00:06 +0200 Subject: [PATCH 1/2] perf(VideoPlayer): lazy-load react-player to keep it out of the main bundle The default VideoPlayer statically imported react-player (~2 MB), pulling it into the SDK's eager import graph via index -> Attachment -> VideoAttachment. It is now code-split behind React.lazy: the react-player render moves to a default-exported ReactPlayerWrapper, dynamically imported so the consumer's bundler emits it as a separate chunk fetched only when a default video player renders. Consumers who override VideoPlayer via ComponentContext never load it. While the chunk loads, a Suspense fallback shows the overridable LoadingIndicator centered to fill the player box. --- .../VideoPlayer/ReactPlayerWrapper.tsx | 31 +++++++++++++ src/components/VideoPlayer/VideoPlayer.tsx | 43 +++++++++-------- .../__tests__/VideoPlayer.test.tsx | 46 +++++++++++++++++++ .../VideoPlayer/styling/VideoPlayer.scss | 11 +++++ src/components/VideoPlayer/styling/index.scss | 1 + 5 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 src/components/VideoPlayer/ReactPlayerWrapper.tsx create mode 100644 src/components/VideoPlayer/__tests__/VideoPlayer.test.tsx create mode 100644 src/components/VideoPlayer/styling/VideoPlayer.scss diff --git a/src/components/VideoPlayer/ReactPlayerWrapper.tsx b/src/components/VideoPlayer/ReactPlayerWrapper.tsx new file mode 100644 index 0000000000..851894efe1 --- /dev/null +++ b/src/components/VideoPlayer/ReactPlayerWrapper.tsx @@ -0,0 +1,31 @@ +import ReactPlayerImport from 'react-player'; + +import type { VideoPlayerProps } from './VideoPlayer'; + +// react-player ships as CJS with the component on `exports.default`. Some +// bundler/interop setups (e.g. Vite serving our built ESM as a linked workspace +// dependency) hand back the module namespace `{ default }` instead of the +// component itself, which makes React throw "Element type is invalid ... got: +// object". Unwrap the default defensively so it works regardless of interop. +const ReactPlayer = + (ReactPlayerImport as unknown as { default?: typeof ReactPlayerImport }).default ?? + ReactPlayerImport; + +/** + * Default-exported so `VideoPlayer` can code-split it via `React.lazy`, keeping + * `react-player` (~2 MB) out of the SDK's eager import graph — it is fetched + * only when a default video player actually renders. + */ +const ReactPlayerWrapper = ({ isPlaying, thumbnailUrl, videoUrl }: VideoPlayerProps) => ( + +); + +export default ReactPlayerWrapper; diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index 47d70799b3..7f8de93229 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -1,15 +1,13 @@ -import { useComponentContext } from '../../context'; -import ReactPlayerImport from 'react-player'; import React from 'react'; -// react-player ships as CJS with the component on `exports.default`. Some -// bundler/interop setups (e.g. Vite serving our built ESM as a linked workspace -// dependency) hand back the module namespace `{ default }` instead of the -// component itself, which makes React throw "Element type is invalid ... got: -// object". Unwrap the default defensively so it works regardless of interop. -const ReactPlayer = - (ReactPlayerImport as unknown as { default?: typeof ReactPlayerImport }).default ?? - ReactPlayerImport; +import { useComponentContext } from '../../context'; +import { LoadingIndicator as DefaultLoadingIndicator } from '../Loading/LoadingIndicator'; + +// `react-player` (~2 MB) is loaded lazily so it stays out of the SDK's eager +// import graph; the consumer's bundler emits it as a separate chunk that is +// fetched only when a default video player renders. Consumers who override +// `VideoPlayer` via `ComponentContext` never load it at all. +const ReactPlayer = React.lazy(() => import('./ReactPlayerWrapper')); export type VideoPlayerProps = { isPlaying?: boolean; @@ -18,19 +16,24 @@ export type VideoPlayerProps = { }; export const VideoPlayer = ({ isPlaying, thumbnailUrl, videoUrl }: VideoPlayerProps) => { - const { VideoPlayer: VideoPlayerContext } = useComponentContext(); + const { LoadingIndicator = DefaultLoadingIndicator, VideoPlayer: VideoPlayerContext } = + useComponentContext(); return VideoPlayerContext ? ( ) : ( - + + + + } + > + + ); }; diff --git a/src/components/VideoPlayer/__tests__/VideoPlayer.test.tsx b/src/components/VideoPlayer/__tests__/VideoPlayer.test.tsx new file mode 100644 index 0000000000..6aefd4a9d9 --- /dev/null +++ b/src/components/VideoPlayer/__tests__/VideoPlayer.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from '@testing-library/react'; +import { vi } from 'vitest'; + +import { ComponentProvider } from '../../../context'; +import { mockComponentContext } from '../../../mock-builders'; +import { VideoPlayer } from '../VideoPlayer'; + +// Stub react-player so the test exercises the lazy/Suspense wiring (not the real +// player internals). The wrapper unwraps the CJS `default`, so exposing the +// component on `default` mirrors the real module shape. +vi.mock('react-player', () => ({ + default: ({ url }: { url?: string }) => ( +
+ {url} +
+ ), +})); + +describe('VideoPlayer', () => { + it('lazily renders the default react-player when no override is set', async () => { + render( + + + , + ); + + // Resolves asynchronously because react-player is behind React.lazy. + const player = await screen.findByTestId('react-player'); + expect(player).toHaveTextContent('https://example.com/clip.mp4'); + }); + + it('renders the ComponentContext override without loading react-player', () => { + const CustomVideoPlayer = ({ videoUrl }: { videoUrl?: string }) => ( +
{videoUrl}
+ ); + + render( + + + , + ); + + expect(screen.getByTestId('custom-video-player')).toBeInTheDocument(); + expect(screen.queryByTestId('react-player')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/VideoPlayer/styling/VideoPlayer.scss b/src/components/VideoPlayer/styling/VideoPlayer.scss new file mode 100644 index 0000000000..a8b00e3d43 --- /dev/null +++ b/src/components/VideoPlayer/styling/VideoPlayer.scss @@ -0,0 +1,11 @@ +// Shown as the Suspense fallback while the lazily-loaded react-player chunk is +// fetched. Fills the player box (which has a defined height) and centers the +// spinner so it doesn't flash as a stray icon or shift layout once the player +// mounts. +.str-chat__video-player-loading { + display: flex; + align-items: center; + justify-content: center; + inline-size: 100%; + block-size: 100%; +} diff --git a/src/components/VideoPlayer/styling/index.scss b/src/components/VideoPlayer/styling/index.scss index 366a25a95d..b09461eb13 100644 --- a/src/components/VideoPlayer/styling/index.scss +++ b/src/components/VideoPlayer/styling/index.scss @@ -1 +1,2 @@ +@use 'VideoPlayer'; @use 'VideoThumbnail'; From 92c45937324869bb0d35ceeb89c8035bc8828788 Mon Sep 17 00:00:00 2001 From: Oliver Lazoroski Date: Wed, 3 Jun 2026 14:14:02 +0200 Subject: [PATCH 2/2] chore: vendor Vercel agent skills used for the performance audit Adds the three vercel-labs/agent-skills used during the performance audit (vercel-composition-patterns, vercel-react-best-practices, web-design-guidelines) under .agents/skills, symlinked from .claude/skills and pinned via skills-lock.json. The content is third-party, so it is added to .prettierignore rather than reformatted to repo style. --- .../vercel-composition-patterns/AGENTS.md | 946 ++++ .../vercel-composition-patterns/README.md | 60 + .../vercel-composition-patterns/SKILL.md | 89 + .../vercel-composition-patterns/metadata.json | 11 + .../rules/_sections.md | 29 + .../rules/_template.md | 24 + .../rules/architecture-avoid-boolean-props.md | 100 + .../rules/architecture-compound-components.md | 112 + .../patterns-children-over-render-props.md | 87 + .../rules/patterns-explicit-variants.md | 100 + .../rules/react19-no-forwardref.md | 42 + .../rules/state-context-interface.md | 191 + .../rules/state-decouple-implementation.md | 113 + .../rules/state-lift-state.md | 125 + .../vercel-react-best-practices/AGENTS.md | 3810 +++++++++++++++++ .../vercel-react-best-practices/README.md | 123 + .../vercel-react-best-practices/SKILL.md | 149 + .../vercel-react-best-practices/metadata.json | 15 + .../rules/_sections.md | 46 + .../rules/_template.md | 28 + .../rules/advanced-effect-event-deps.md | 56 + .../rules/advanced-event-handler-refs.md | 55 + .../rules/advanced-init-once.md | 42 + .../rules/advanced-use-latest.md | 39 + .../rules/async-api-routes.md | 38 + .../async-cheap-condition-before-await.md | 37 + .../rules/async-defer-await.md | 82 + .../rules/async-dependencies.md | 51 + .../rules/async-parallel.md | 28 + .../rules/async-suspense-boundaries.md | 99 + .../rules/bundle-analyzable-paths.md | 63 + .../rules/bundle-barrel-imports.md | 60 + .../rules/bundle-conditional.md | 31 + .../rules/bundle-defer-third-party.md | 49 + .../rules/bundle-dynamic-imports.md | 35 + .../rules/bundle-preload.md | 50 + .../rules/client-event-listeners.md | 74 + .../rules/client-localstorage-schema.md | 71 + .../rules/client-passive-event-listeners.md | 48 + .../rules/client-swr-dedup.md | 56 + .../rules/js-batch-dom-css.md | 107 + .../rules/js-cache-function-results.md | 80 + .../rules/js-cache-property-access.md | 28 + .../rules/js-cache-storage.md | 70 + .../rules/js-combine-iterations.md | 32 + .../rules/js-early-exit.md | 50 + .../rules/js-flatmap-filter.md | 60 + .../rules/js-hoist-regexp.md | 45 + .../rules/js-index-maps.md | 37 + .../rules/js-length-check-first.md | 49 + .../rules/js-min-max-loop.md | 82 + .../rules/js-request-idle-callback.md | 105 + .../rules/js-set-map-lookups.md | 24 + .../rules/js-tosorted-immutable.md | 57 + .../rules/rendering-activity.md | 26 + .../rules/rendering-animate-svg-wrapper.md | 47 + .../rules/rendering-conditional-render.md | 40 + .../rules/rendering-content-visibility.md | 38 + .../rules/rendering-hoist-jsx.md | 46 + .../rules/rendering-hydration-no-flicker.md | 82 + .../rendering-hydration-suppress-warning.md | 30 + .../rules/rendering-resource-hints.md | 85 + .../rules/rendering-script-defer-async.md | 68 + .../rules/rendering-svg-precision.md | 28 + .../rules/rendering-usetransition-loading.md | 75 + .../rules/rerender-defer-reads.md | 39 + .../rules/rerender-dependencies.md | 45 + .../rules/rerender-derived-state-no-effect.md | 40 + .../rules/rerender-derived-state.md | 29 + .../rules/rerender-functional-setstate.md | 74 + .../rules/rerender-lazy-state-init.md | 58 + .../rules/rerender-memo-with-default-value.md | 38 + .../rules/rerender-memo.md | 44 + .../rules/rerender-move-effect-to-event.md | 45 + .../rules/rerender-no-inline-components.md | 82 + .../rerender-simple-expression-in-memo.md | 35 + .../rules/rerender-split-combined-hooks.md | 64 + .../rules/rerender-transitions.md | 40 + .../rules/rerender-use-deferred-value.md | 59 + .../rerender-use-ref-transient-values.md | 73 + .../rules/server-after-nonblocking.md | 73 + .../rules/server-auth-actions.md | 96 + .../rules/server-cache-lru.md | 41 + .../rules/server-cache-react.md | 76 + .../rules/server-dedup-props.md | 65 + .../rules/server-hoist-static-io.md | 149 + .../rules/server-no-shared-module-state.md | 50 + .../rules/server-parallel-fetching.md | 83 + .../rules/server-parallel-nested-fetching.md | 34 + .../rules/server-serialization.md | 38 + .agents/skills/web-design-guidelines/SKILL.md | 39 + .claude/skills/vercel-composition-patterns | 1 + .claude/skills/vercel-react-best-practices | 1 + .claude/skills/web-design-guidelines | 1 + .prettierignore | 5 + skills-lock.json | 23 + 96 files changed, 10195 insertions(+) create mode 100644 .agents/skills/vercel-composition-patterns/AGENTS.md create mode 100644 .agents/skills/vercel-composition-patterns/README.md create mode 100644 .agents/skills/vercel-composition-patterns/SKILL.md create mode 100644 .agents/skills/vercel-composition-patterns/metadata.json create mode 100644 .agents/skills/vercel-composition-patterns/rules/_sections.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/_template.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/state-context-interface.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md create mode 100644 .agents/skills/vercel-composition-patterns/rules/state-lift-state.md create mode 100644 .agents/skills/vercel-react-best-practices/AGENTS.md create mode 100644 .agents/skills/vercel-react-best-practices/README.md create mode 100644 .agents/skills/vercel-react-best-practices/SKILL.md create mode 100644 .agents/skills/vercel-react-best-practices/metadata.json create mode 100644 .agents/skills/vercel-react-best-practices/rules/_sections.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/_template.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-init-once.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-api-routes.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-defer-await.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-parallel.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-conditional.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/bundle-preload.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-cache-storage.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-early-exit.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-index-maps.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-length-check-first.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-request-idle-callback.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-activity.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-transitions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-auth-actions.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-lru.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-cache-react.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-dedup-props.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md create mode 100644 .agents/skills/vercel-react-best-practices/rules/server-serialization.md create mode 100644 .agents/skills/web-design-guidelines/SKILL.md create mode 120000 .claude/skills/vercel-composition-patterns create mode 120000 .claude/skills/vercel-react-best-practices create mode 120000 .claude/skills/web-design-guidelines create mode 100644 skills-lock.json diff --git a/.agents/skills/vercel-composition-patterns/AGENTS.md b/.agents/skills/vercel-composition-patterns/AGENTS.md new file mode 100644 index 0000000000..558bf9aa1e --- /dev/null +++ b/.agents/skills/vercel-composition-patterns/AGENTS.md @@ -0,0 +1,946 @@ +# React Composition Patterns + +**Version 1.0.0** +Engineering +January 2026 + +> **Note:** +> This document is mainly for agents and LLMs to follow when maintaining, +> generating, or refactoring React codebases using composition. Humans +> may also find it useful, but guidance here is optimized for automation +> and consistency by AI-assisted workflows. + +--- + +## Abstract + +Composition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale. + +--- + +## Table of Contents + +1. [Component Architecture](#1-component-architecture) — **HIGH** + - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation) + - 1.2 [Use Compound Components](#12-use-compound-components) +2. [State Management](#2-state-management) — **MEDIUM** + - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui) + - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection) + - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components) +3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM** + - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants) + - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props) +4. [React 19 APIs](#4-react-19-apis) — **MEDIUM** + - 4.1 [React 19 API Changes](#41-react-19-api-changes) + +--- + +## 1. Component Architecture + +**Impact: HIGH** + +Fundamental patterns for structuring components to avoid prop +proliferation and enable flexible composition. + +### 1.1 Avoid Boolean Prop Proliferation + +**Impact: CRITICAL (prevents unmaintainable component variants)** + +Don't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize + +component behavior. Each boolean doubles possible states and creates + +unmaintainable conditional logic. Use composition instead. + +**Incorrect: boolean props create exponential complexity** + +```tsx +function Composer({ + onSubmit, + isThread, + channelId, + isDMThread, + dmId, + isEditing, + isForwarding, +}: Props) { + return ( +
+
+ + {isDMThread ? ( + + ) : isThread ? ( + + ) : null} + {isEditing ? ( + + ) : isForwarding ? ( + + ) : ( + + )} +