程式碼在這邊的 feature/preload
分支: https://github.com/sh1un/Nextjs-Musive-app
此專案是一個 Next.js 專案,原作者為 Ansh Rathod,我已經過作者本人授權使用,因此 Fork 過來稍微改了一下。
我本身不是一名專精於前端技術的開發者,所以我所修改的 Code 都是由 ChatGPT-4 產生的。我今天這樣做的主要目的是想實驗 preload 是否能做到效能優化,以此來做一個簡單的 PoC。
補充一下,這篇文章的內容本來是要放在 「2024 AWS Educate 陪跑計畫的獎勵課程 - 雲端串流挑戰:復刻 Spotify 的技術旅程」中教學,此工作坊旨在教學如何利用 CloudFront 優化速度與降低延遲,但礙於當天工作坊的內容太滿已經塞不下了,所以當天工作坊就沒有講到 Preload,如果看到這篇文章,想要學學 CloudFront 歡迎參觀我們當天的教材 (連結)
什麼是 Preload?
Preload 是一種網頁性能優化技術,讓瀏覽器在頁面加載時預先加載指定的資源,這樣在使用這些資源時可以更快地顯示或播放。這可以減少等待時間,提升用戶體驗。
你可以想成:原本你是要“按下去播放按鈕”才會開始下載音樂,現在我們提前幫你下載,你之後“按下去播放按鈕”就會立即播放。
舉例
如果你在網頁上有一段影片或音樂,你可以用 preload 告訴瀏覽器提前下載這些文件,這樣當用戶點擊播放按鈕時,影片或音樂就會立即播放,而不是先等待下載。
基本用法:
在 HTML 中,你可以在 <link>
標籤中使用 rel="preload"
來預加載資源。例如:
<link rel="preload" href="path/to/your/file.mp3" as="audio">
這樣,瀏覽器會在頁面加載時預先下載 file.mp3
,提高用戶播放音樂的速度。
動態 Preload 的實現思路
每個用戶的歌單都不一樣,所以我們當然不希望把 preload 的 href
寫死成一個 URL。
解決方案
我們可以通過以下幾個步驟來實現動態的 preload:
- API 獲取歌單: 伺服器端提供一個 API,根據用戶的請求返回該用戶的歌單。這個 API 返回一個 JSON 結構,其中包含了音樂檔案的 URL 列表。
- JavaScript 動態生成 Preload 標籤:
使用 JavaScript 在頁面加載後動態地從 API 獲取歌單,並為每個音樂文件創建 preload 標籤,將其添加到頁面的
<head>
中。
具體實現
注意⚠️ 我的寫法並非添加 preload 到頁面的
<head>
,而是直接用 React 和 JS 本身提供的內建物件來達到同樣效果,詳見本段說明。
因為我們的 Ansh 寫的後端應用有提供這支“取得指定數量隨機音樂 API”,我會以這支 API 來取得隨機歌單:
-
GET /api/songs/random/{songs_count}
-
Response (200)
{ "success": true, "data": [ { "id": 99123, "duration": 197.877531, "track_name": "Marching Music -The Crusader By John Philip Sousa", "src": "https://cdn.pixabay.com/audio/2022/03/21/audio_1a944368a1.mp3", "cover_image": { "url": "https://images.unsplash.com/photo-1482954363933-4bed6bbea570?ixid=MnwzODAxMTZ8MHwxfHNlYXJjaHwxMjd8fGx1eHVyeXxlbnwwfHx8fDE2NjgyNjIzOTI&ixlib=rb-4.0.3", "color": "#0c2640" }, "artist_name": "Jane Howe", "artist_id": 25232863 }, ... ] }
先來看一些核心的程式碼,最後會附上完整的 _app.tsx
。
這段程式碼在 MyApp
組件的 useEffect
中實現了動態 preload:
useEffect(() => {
// Fetch random songs and preload them
const fetchAndPreloadSongs = async () => {
try {
const { topHits } = await homePageApi.getRandomArtists();
preloadSongs(topHits);
} catch (error) {
console.error("Error fetching random songs:", error);
}
};
const preloadSongs = (songs: Song[]) => {
songs.forEach((song) => {
const audio = new Audio();
audio.src = song.src;
audio.load();
audioElements.current.set(song.id, audio);
console.log(`Preloading song: ${song.track_name}`);
});
};
fetchAndPreloadSongs();
}, []);
- useEffect
- 這段程式碼在組件掛載 (mounted) 後執行,確保在頁面加載後立刻執行 preload 邏輯。
- 空的依賴陣列
[]
確保這段程式碼只在組件初始化時執行一次。
- fetchAndPreloadSongs
- 該異步函數使用
homePageApi.getRandomArtists()
從 API 獲取隨機的音樂檔案。 - 獲取音樂後,調用
preloadSongs
函數進行 preload。
- 該異步函數使用
- preloadSongs
- 該函數接收一個音樂列表,對每個音樂創建一個新的
Audio
對象。 - 設置
audio.src
為音樂的 URL,並調用audio.load()
以便瀏覽器開始加載音樂文件。 - 將創建的
Audio
對象存儲在audioElements
這個Map
中,方便以後引用和控制。
- 該函數接收一個音樂列表,對每個音樂創建一個新的
_app.tsx
完整程式碼:
import "../styles/globals.css";
import type { AppProps } from "next/app";
import { Provider } from "react-redux";
import store from "../stores/store";
import NextNProgress from "nextjs-progressbar";
import { useRouter } from "next/router";
import AudioPlayer from "../components/AudioPlayer/AudioPlayer";
import SidebarItem from "../components/sidebarItem";
import Head from "next/head";
import "react-toastify/dist/ReactToastify.css";
import useDetectKeyboardOpen from "use-detect-keyboard-open";
import AddToCollectionModel from "@/components/AddToCollectionModel";
import { ToastContainer } from "react-toastify";
import { useEffect, useRef } from "react";
import homePageApi from "../stores/homePage/homePageApi";
import { Song } from "@/interfaces/Track";
function MyApp({ Component, pageProps }: AppProps) {
const audioElements = useRef<Map<number, HTMLAudioElement>>(new Map());
useEffect(() => {
// Fetch random songs and preload them
const fetchAndPreloadSongs = async () => {
try {
const { topHits } = await homePageApi.getRandomArtists();
preloadSongs(topHits);
} catch (error) {
console.error("Error fetching random songs:", error);
}
};
const preloadSongs = (songs: Song[]) => {
songs.forEach((song) => {
const audio = new Audio();
audio.src = song.src;
audio.load();
audioElements.current.set(song.id, audio);
console.log(`Preloading song: ${song.track_name}`);
});
};
fetchAndPreloadSongs();
}, []);
return (
<Provider store={store}>
<Head>
<link
rel="preload"
href="/musive-icons.ttf"
as="font"
crossOrigin=""
type="font/ttf"
/>
<link
rel="preload"
href="/ProximaNova/Proxima Nova Reg.otf"
as="font"
crossOrigin=""
type="font/otf"
/>
<link
rel="preload"
href="/ProximaNova/Proxima Nova Bold.otf"
as="font"
crossOrigin=""
type="font/otf"
/>
</Head>
<NextNProgress
color="#2bb540"
stopDelayMs={10}
height={3}
options={{ showSpinner: false }}
/>
<Component {...pageProps} />
<AudioPlayerComponent />
</Provider>
);
}
function AudioPlayerComponent() {
const router = useRouter();
const isKeyboardOpen = useDetectKeyboardOpen();
return (
<div>
<ToastContainer
position="top-center"
autoClose={1000}
hideProgressBar
newestOnTop={
false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
<AddToCollectionModel />
{router.pathname !== "/login" &&
router.pathname !== "/register" &&
router.pathname !== "/_error" &&
router.pathname !== "/" ? (
<AudioPlayer className={isKeyboardOpen ? "invisible" : "visible"} />
) : (
<div></div>
)}
{router.pathname !== "/login" &&
router.pathname !== "/register" &&
router.pathname !== "/_error" &&
router.pathname !== "/playing" &&
router.pathname !== "/" && (
<div
className={`bg-[#121212] hidden mobile:block tablet:block
fixed bottom-0 left-0 right-0 w-full pt-2 pb-1 z-20 ${
isKeyboardOpen ? "invisible" : "visible"
}`}
>
<div className="flex flex-row justify-center ">
<SidebarItem name="home" label="Home" />
<SidebarItem name="search" label="Search" />
<SidebarItem name="library" label="Library" />
</div>
</div>
)}
</div>
);
}
export default MyApp;
使用 Preload 前 vs 使用後
使用前
請注意,第二首是按下播放按鈕後才開始下載音樂:
使用後
補充 - 在舊有的機器改成 Preload
這邊內容僅適用於有照著工作坊內容實作的同學,以下步驟將教學你如何把原有的 Application 套用 Dynamic Preload:
- 連線上去 EC2
cd Nextjs-Musive-app
git checkout feature/preload
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
export MUSIVE_API_URL="http://$(curl -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/public-ipv4):4444/api"
npm install
npm run build
pm2 restart nextjs-app