Remotion LabRemotion Lab
PlayerPlayer 最佳實踐

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
  • 事件監聽器是否在元件卸載時正確移除
  • component prop 是否指向穩定的元件參考
  • 網路資源是否考慮使用預載入
  • 是否為需要延遲入場的媒體啟用了 pauseWhenBuffering

相關資源