Player 最佳實踐
使用 Remotion Player 的最佳實踐指南,包括避免不必要的重新渲染、正確傳遞使用者互動事件、memoize inputProps 等效能優化技巧。
Player 最佳實踐
避免 <Player> 的不必要重新渲染
以下模式並不理想,因為每次時間更新時,<Player> 都會重新渲染:
// 有問題的寫法
export const App: React.FC = () => {
const playerRef = useRef<PlayerRef>(null);
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
playerRef.current?.addEventListener('timeupdate', (e) => {
setCurrentTime(e.detail.frame);
});
}, []);
return (
<div>
<Player ref={playerRef} component={MyVideo} {...otherProps} />
<div>目前時間:{currentTime}</div>
</div>
);
};問題說明:當 currentTime 狀態更新時,整個 App 元件(包含 <Player>)都會重新渲染。由於 timeupdate 事件每影格都會觸發,這意味著每秒 30 次(或更多)的完整重新渲染,造成嚴重的效能問題。
正確做法:將控制項和 Player 分離
建議將控制項和 UI 渲染為 <Player> 的兄弟元件,並透過 props 傳遞 ref:
// 較佳的寫法
const PlayerOnly: React.FC<{
playerRef: React.RefObject<PlayerRef | null>;
}> = ({ playerRef }) => {
return (
<Player
ref={playerRef}
component={MyVideo}
{...otherProps}
/>
);
};
const ControlsOnly: React.FC<{
playerRef: React.RefObject<PlayerRef | null>;
}> = ({ playerRef }) => {
const [currentTime, setCurrentTime] = useState(0);
useEffect(() => {
playerRef.current?.addEventListener('timeupdate', (e) => {
setCurrentTime(e.detail.frame);
});
}, []);
return <div>目前時間:{currentTime}</div>;
};
export const App: React.FC = () => {
const playerRef = useRef<PlayerRef>(null);
return (
<>
<PlayerOnly playerRef={playerRef} />
<ControlsOnly playerRef={playerRef} />
</>
);
};這樣做更有效率,因為 <Player> 較少被重新渲染。ControlsOnly 元件更新時,不會影響到 PlayerOnly 元件。
注意:此建議主要針對頻繁更新的狀態(如目前時間)。把像「是否循環播放」這類不會頻繁改變的狀態放在父元件中是沒問題的。
傳遞使用者互動事件至 play()
當你監聽 onClick() 事件時,瀏覽器會提供一個事件參數。將此事件傳遞給 .play() 和 .toggle() 可以最大程度地降低瀏覽器自動播放限制的影響:
import { Player, PlayerRef } from '@remotion/player';
import { useRef, useCallback } from 'react';
import { MyVideo } from './remotion/MyVideo';
export const App: React.FC = () => {
const playerRef = useRef<PlayerRef>(null);
// 將 onClick 事件傳遞給 play(),以降低自動播放限制的機率
const handlePlayClick = useCallback((e: React.MouseEvent) => {
playerRef.current?.play(e);
}, []);
const handleToggleClick = useCallback((e: React.MouseEvent) => {
playerRef.current?.toggle(e);
}, []);
return (
<div>
<Player
ref={playerRef}
component={MyVideo}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
/>
<div>
<button onClick={handlePlayClick}>播放</button>
<button onClick={handleToggleClick}>切換播放/暫停</button>
</div>
</div>
);
};Memoize inputProps
不 memoize inputProps 可能導致整個元件樹頻繁重新渲染,造成效能瓶頸。
// Player.tsx
import { Player } from '@remotion/player';
import { useState, useMemo } from 'react';
import { MyVideo } from './remotion/MyVideo';
export const App: React.FC = () => {
const [text, setText] = useState('world');
// 使用 useMemo 確保 inputProps 只在相依值改變時才更新
const inputProps = useMemo(() => {
return {
text,
};
}, [text]);
return (
<>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<Player
component={MyVideo}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={inputProps}
/>
</>
);
};不要這樣做(每次渲染都會建立新物件):
// 有問題的寫法:每次渲染都建立新的 inputProps 物件
export const App: React.FC = () => {
const [text, setText] = useState('world');
return (
<Player
component={MyVideo}
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
inputProps={{ text }} // 每次渲染都是新物件!
/>
);
};在 Player 外部定義元件
component prop 應傳入在 Player 外部定義的穩定元件參考,而不是內聯定義的元件或箭頭函式:
// 在模組頂層定義元件(正確)
const MyVideo = () => {
return <AbsoluteFill style={{ backgroundColor: 'blue' }} />;
};
export const App: React.FC = () => {
return (
<Player
component={MyVideo} // 穩定的參考
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
/>
);
};// 避免這樣做:在渲染中內聯定義元件
export const App: React.FC = () => {
return (
<Player
component={() => <AbsoluteFill />} // 每次渲染都是新函式!
durationInFrames={120}
compositionWidth={1920}
compositionHeight={1080}
fps={30}
/>
);
};移除事件監聽器
在 useEffect 中加入的事件監聽器應在清除函式中移除,以防止記憶體洩漏:
import { Player, PlayerRef } from '@remotion/player';
import { useRef, useEffect, useState } from 'react';
import { MyVideo } from './remotion/MyVideo';
const Controls: React.FC<{ playerRef: React.RefObject<PlayerRef | null> }> = ({ playerRef }) => {
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
const { current } = playerRef;
if (!current) return;
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
current.addEventListener('play', onPlay);
current.addEventListener('pause', onPause);
// 清除函式:移除事件監聽器
return () => {
current.removeEventListener('play', onPlay);
current.removeEventListener('pause', onPause);
};
}, [playerRef]);
return (
<div>狀態:{isPlaying ? '播放中' : '已暫停'}</div>
);
};設定合理的緩衝等待時間
若影片包含網路載入的媒體,建議使用緩衝狀態管理避免畫面閃爍:
import { Video } from 'remotion';
const MyComp: React.FC = () => {
return (
<Video
src="https://example.com/video.mp4"
pauseWhenBuffering // 在緩衝期間自動暫停播放器
/>
);
};詳細說明請參閱緩衝狀態管理。
使用 premountFor 預先掛載
對於需要延遲入場的元件,可使用 premountFor prop 提前掛載以確保資源預先載入:
import { Sequence } from 'remotion';
const MyComp: React.FC = () => {
return (
// 在實際出現前 60 格就開始掛載,讓資源有時間載入
<Sequence from={120} premountFor={60}>
<VideoWithHeavyAssets />
</Sequence>
);
};效能檢查清單
在部署使用 <Player> 的應用程式前,請確認以下事項:
inputProps是否使用useMemo進行 memoize- 是否避免在父元件中使用頻繁更新的狀態(如
timeupdate) - 事件監聽器是否在元件卸載時正確移除
componentprop 是否指向穩定的元件參考- 網路資源是否考慮使用預載入
- 是否為需要延遲入場的媒體啟用了
pauseWhenBuffering