优化play页面首次加载性能

This commit is contained in:
mtvpls
2025-12-29 22:12:55 +08:00
parent cae84559ea
commit 86d260a4c2
2 changed files with 130 additions and 13 deletions

View File

@@ -732,6 +732,7 @@ function PlayPageClient() {
const [sourceSearchError, setSourceSearchError] = useState<string | null>(
null
);
const [backgroundSourcesLoading, setBackgroundSourcesLoading] = useState(false);
// 优选和测速开关
const [optimizationEnabled] = useState<boolean>(() => {
@@ -1966,6 +1967,43 @@ function PlayPageClient() {
const fetchSourcesData = async (query: string): Promise<SearchResult[]> => {
// 根据搜索词获取全部源信息
try {
// 先检查 sessionStorage 中是否有缓存
const cacheKey = `search_cache_${query.trim()}`;
let results: SearchResult[] = [];
if (typeof window !== 'undefined') {
try {
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
console.log('[Play] 使用 sessionStorage 缓存的搜索结果');
const cachedData = JSON.parse(cached);
// 处理缓存的搜索结果,根据规则过滤
results = cachedData.filter(
(result: SearchResult) =>
result.title.replaceAll(' ', '').toLowerCase() ===
videoTitleRef.current.replaceAll(' ', '').toLowerCase() &&
(videoYearRef.current
? result.year.toLowerCase() === videoYearRef.current.toLowerCase()
: true) &&
(searchType
? // openlist 源跳过 episodes 长度检查,因为搜索时不返回详细播放列表
result.source === 'openlist' ||
(searchType === 'tv' && result.episodes.length > 1) ||
(searchType === 'movie' && result.episodes.length === 1)
: true)
);
setAvailableSources(results);
return results;
}
} catch (error) {
console.error('[Play] 读取缓存失败:', error);
// 继续执行 API 调用
}
}
// 如果没有缓存,调用 API
const response = await fetch(
`/api/search?q=${encodeURIComponent(query.trim())}`
);
@@ -1975,7 +2013,7 @@ function PlayPageClient() {
const data = await response.json();
// 处理搜索结果,根据规则过滤
const results = data.results.filter(
results = data.results.filter(
(result: SearchResult) =>
result.title.replaceAll(' ', '').toLowerCase() ===
videoTitleRef.current.replaceAll(' ', '').toLowerCase() &&
@@ -2014,23 +2052,53 @@ function PlayPageClient() {
: '🔍 正在搜索播放源...'
);
let sourcesInfo = await fetchSourcesData(searchTitle || videoTitle);
if (
currentSource &&
currentId &&
!sourcesInfo.some(
(source) => source.source === currentSource && source.id === currentId
)
) {
sourcesInfo = await fetchSourceDetail(currentSource, currentId);
// 如果已经有了source和id优先通过单个详情接口快速获取
let detailData: SearchResult | null = null;
let sourcesInfo: SearchResult[] = [];
if (currentSource && currentId) {
// 先快速获取当前源的详情
try {
const currentSourceDetail = await fetchSourceDetail(currentSource, currentId);
if (currentSourceDetail.length > 0) {
detailData = currentSourceDetail[0];
sourcesInfo = currentSourceDetail;
}
} catch (err) {
console.error('获取当前源详情失败:', err);
}
// 异步获取其他源信息,不阻塞播放
setBackgroundSourcesLoading(true);
fetchSourcesData(searchTitle || videoTitle).then((sources) => {
// 合并当前源和搜索到的其他源
const allSources = [...sourcesInfo];
sources.forEach((source) => {
// 避免重复添加当前源
if (!(source.source === currentSource && source.id === currentId)) {
allSources.push(source);
}
});
setAvailableSources(allSources);
setBackgroundSourcesLoading(false);
}).catch((err) => {
console.error('异步获取其他源失败:', err);
setBackgroundSourcesLoading(false);
});
} else {
// 没有source和id正常搜索流程
sourcesInfo = await fetchSourcesData(searchTitle || videoTitle);
}
if (sourcesInfo.length === 0) {
if (!detailData && sourcesInfo.length === 0) {
setError('未找到匹配结果');
setLoading(false);
return;
}
let detailData: SearchResult = sourcesInfo[0];
if (!detailData) {
detailData = sourcesInfo[0];
}
// 指定源和id且无需优选
if (currentSource && currentId && !needPreferRef.current) {
const target = sourcesInfo.find(
@@ -5057,7 +5125,7 @@ function PlayPageClient() {
<div className='mt-3 px-2 lg:flex-shrink-0'>
<div className='bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm rounded-lg p-2 border border-gray-200/50 dark:border-gray-700/50 w-full lg:w-auto overflow-x-auto'>
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap items-center'>
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap'>
<div className='flex gap-1.5 flex-nowrap lg:flex-wrap lg:justify-end lg:flex-1'>
{/* 下载按钮 */}
<button
onClick={(e) => {
@@ -5306,6 +5374,7 @@ function PlayPageClient() {
availableSources={availableSources}
sourceSearchLoading={sourceSearchLoading}
sourceSearchError={sourceSearchError}
backgroundSourcesLoading={backgroundSourcesLoading}
precomputedVideoInfo={precomputedVideoInfo}
onDanmakuSelect={handleDanmakuSelect}
currentDanmakuSelection={currentDanmakuSelection}

View File

@@ -44,6 +44,8 @@ interface EpisodeSelectorProps {
availableSources?: SearchResult[];
sourceSearchLoading?: boolean;
sourceSearchError?: string | null;
/** 后台源加载状态 */
backgroundSourcesLoading?: boolean;
/** 预计算的测速结果,避免重复测速 */
precomputedVideoInfo?: Map<string, VideoInfo>;
/** 弹幕相关 */
@@ -73,6 +75,7 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
availableSources = [],
sourceSearchLoading = false,
sourceSearchError = null,
backgroundSourcesLoading = false,
precomputedVideoInfo,
onDanmakuSelect,
currentDanmakuSelection,
@@ -296,6 +299,42 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
// 依赖项保持与之前一致
}, [activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
// 监听后台加载完成,触发自动测速
const prevBackgroundLoadingRef = useRef<boolean>(false);
useEffect(() => {
// 当后台加载从 true 变为 false 时(即加载完成)
if (prevBackgroundLoadingRef.current && !backgroundSourcesLoading) {
// 如果当前选项卡在换源位置,触发测速
if (activeTab === 'sources' && optimizationEnabled && currentSource !== 'openlist') {
// 筛选出尚未测速的播放源
const pendingSources = availableSources.filter((source) => {
const sourceKey = `${source.source}-${source.id}`;
return !attemptedSourcesRef.current.has(sourceKey);
});
if (pendingSources.length > 0) {
const batchSize = Math.ceil(pendingSources.length / 2);
const fetchInBatches = async () => {
for (let start = 0; start < pendingSources.length; start += batchSize) {
const batch = pendingSources.slice(start, start + batchSize);
await Promise.all(batch.map(getVideoInfo));
}
if (!initialTestingCompleted) {
setInitialTestingCompleted(true);
}
};
fetchInBatches();
}
}
}
// 更新前一次的加载状态
prevBackgroundLoadingRef.current = backgroundSourcesLoading;
}, [backgroundSourcesLoading, activeTab, availableSources, getVideoInfo, optimizationEnabled, initialTestingCompleted, currentSource]);
// 升序分页标签
const categoriesAsc = useMemo(() => {
return Array.from({ length: pageCount }, (_, i) => {
@@ -881,6 +920,15 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
</div>
);
})}
{/* 后台加载提示 */}
{backgroundSourcesLoading && (
<div className='flex items-center justify-center py-6 border-t border-gray-300 dark:border-gray-700'>
<div className='animate-spin rounded-full h-6 w-6 border-b-2 border-green-500'></div>
<span className='ml-2 text-sm text-gray-600 dark:text-gray-300'>
...
</span>
</div>
)}
<div className='flex-shrink-0 mt-auto pt-2 border-t border-gray-400 dark:border-gray-700'>
<button
onClick={() => {