From d0748442403a74826e311a75cbff354f7fa3e81e Mon Sep 17 00:00:00 2001 From: Hakadao Date: Wed, 22 May 2024 00:25:34 +0800 Subject: [PATCH] perf: video card, closes #760, #762 (#779) * perf: video card * refactor: video card * refactor(ForYou): remove loading icon * refactor(ForYou): remove unused 'url' property in video card component * refactor(ForYou): remove unused 'url' property in video card component * fix(VideoCard): prevent `a` tag bubble * perf: improve video card performance * refactor(ForYou): optimize video card loading and rending * refactor(VideoCard): optimize video card loading and rendering * perf(ForYou): use another method to generate a unique id * perf(VideoCard): remove hover animation * feat(VideoCard): add hover & active effect again... * feat(VideoCard): adjust hovering & activating effect * perf(VideoCard): remove the hover effect * refactor(VideoCard): optimize lazy loading for images * perf: optimize scrolling performance * perf(VideoCard): optimize video preview performance --- src/components/VideoCard/VideoCard.vue | 576 +++++++++--------- .../views/Home/components/Following.vue | 76 +-- .../views/Home/components/ForYou.vue | 178 +++--- .../Home/components/SubscribedSeries.vue | 80 +-- .../views/Home/components/Trending.vue | 82 +-- 5 files changed, 520 insertions(+), 472 deletions(-) diff --git a/src/components/VideoCard/VideoCard.vue b/src/components/VideoCard/VideoCard.vue index 1eec9248..3a9df0a8 100644 --- a/src/components/VideoCard/VideoCard.vue +++ b/src/components/VideoCard/VideoCard.vue @@ -67,7 +67,7 @@ const api = useApiClient() const isClick = ref(false) const videoUrl = computed(() => { - if (!isClick.value || !props.video) + if (props.removed || !isClick.value || !props.video) return undefined if (props.video.url) @@ -101,25 +101,23 @@ const wValue = computed((): string => { const isInWatchLater = ref(false) const isHover = ref(false) -const contentVisibility = ref<'auto' | 'visible'>('auto') const mouseEnterTimeOut = ref() const mouseLeaveTimeOut = ref() const previewVideoUrl = ref('') watch(() => isHover.value, (newValue) => { - if (!props.video) + if (!props.video || !newValue) return - if (props.showPreview && settings.value.enableVideoPreview) { - if (newValue && !previewVideoUrl.value && props.video.cid) { - api.video.getVideoPreview({ - bvid: props.video.bvid, - cid: props.video.cid, - }).then((res: VideoPreviewResult) => { - if (res.code === 0) - previewVideoUrl.value = res.data.durl[0].url - }) - } + if (props.showPreview && settings.value.enableVideoPreview + && !previewVideoUrl.value && props.video.cid) { + api.video.getVideoPreview({ + bvid: props.video.bvid, + cid: props.video.cid, + }).then((res: VideoPreviewResult) => { + if (res.code === 0) + previewVideoUrl.value = res.data.durl[0].url + }) } }) @@ -154,13 +152,13 @@ function handleMouseEnter() { mouseEnterTimeOut.value = setTimeout(() => { isHover.value = true clearTimeout(mouseLeaveTimeOut.value) - contentVisibility.value = 'visible' }, 1200) } else { - isHover.value = true - clearTimeout(mouseLeaveTimeOut.value) - contentVisibility.value = 'visible' + mouseEnterTimeOut.value = setTimeout(() => { + isHover.value = true + clearTimeout(mouseLeaveTimeOut.value) + }, 500) } } @@ -168,9 +166,6 @@ function handelMouseLeave() { isHover.value = false clearTimeout(mouseEnterTimeOut.value) clearTimeout(mouseLeaveTimeOut.value) - mouseLeaveTimeOut.value = setTimeout(() => { - contentVisibility.value = 'auto' - }, 300) } function switchClickState(flag: boolean) { @@ -195,316 +190,309 @@ function handleUndo() { diff --git a/src/contentScripts/views/Home/components/Following.vue b/src/contentScripts/views/Home/components/Following.vue index 60d9a670..7bb0b032 100644 --- a/src/contentScripts/views/Home/components/Following.vue +++ b/src/contentScripts/views/Home/components/Following.vue @@ -3,14 +3,18 @@ import type { Ref } from 'vue' import Button from '~/components/Button.vue' import Empty from '~/components/Empty.vue' -import Loading from '~/components/Loading.vue' import VideoCard from '~/components/VideoCard/VideoCard.vue' -import VideoCardSkeleton from '~/components/VideoCard/VideoCardSkeleton.vue' import { useApiClient } from '~/composables/api' import { useBewlyApp } from '~/composables/useAppProvider' import type { GridLayout } from '~/logic' import type { DataItem as MomentItem, MomentResult } from '~/models/moment/moment' +// https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L16 +interface VideoElement { + uniqueId: string + item?: MomentItem +} + const props = defineProps<{ gridLayout: GridLayout }>() @@ -30,7 +34,7 @@ const gridValue = computed((): string => { const api = useApiClient() -const videoList = reactive([]) +const videoList = ref([]) const isLoading = ref(false) const needToLoginFirst = ref(false) const containerRef = ref() as Ref @@ -68,7 +72,7 @@ function initPageAction() { async function initData() { offset.value = '' updateBaseline.value = '' - videoList.length = 0 + videoList.value.length = 0 noMoreContent.value = false await getData() @@ -91,6 +95,16 @@ async function getFollowedUsersVideos() { emit('beforeLoading') isLoading.value = true try { + let i = 0 + // https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L208 + // When video list is not empty, addthe number of pending videos is half of the page size + // is set to prevent user scrolling the page too fast and causing the page too laggy + const pendingVideos: VideoElement[] = Array.from({ length: videoList.value.length ? 10 : 30 }, () => ({ + uniqueId: `unique-id-${(videoList.value.length || 0) + i++})}`, + } satisfies VideoElement)) + let lastVideoListLength = videoList.value.length + videoList.value.push(...pendingVideos) + const response: MomentResult = await api.moment.getMoments({ type: 'video', offset: Number(offset.value), @@ -114,12 +128,16 @@ async function getFollowedUsersVideos() { }) // when videoList has length property, it means it is the first time to load - if (!videoList.length) { - Object.assign(videoList, resData) + if (!videoList.value.length) { + videoList.value = resData.map(item => ({ uniqueId: `${item.id_str}`, item })) } else { - // else we concat the new data to the old data - Object.assign(videoList, videoList.concat(resData)) + resData.forEach((item) => { + videoList.value[lastVideoListLength++] = { + uniqueId: `${item.id_str}`, + item, + } + }) } } else if (response.code === -101) { @@ -127,6 +145,7 @@ async function getFollowedUsersVideos() { } } finally { + videoList.value = videoList.value.filter(video => video.item) isLoading.value = false emit('afterLoading') } @@ -159,39 +178,28 @@ defineExpose({ initData }) > - - - - - - - diff --git a/src/contentScripts/views/Home/components/ForYou.vue b/src/contentScripts/views/Home/components/ForYou.vue index b2f90a30..f54d26ab 100644 --- a/src/contentScripts/views/Home/components/ForYou.vue +++ b/src/contentScripts/views/Home/components/ForYou.vue @@ -6,9 +6,7 @@ import { useToast } from 'vue-toastification' import Button from '~/components/Button.vue' import Dialog from '~/components/Dialog.vue' import Empty from '~/components/Empty.vue' -import Loading from '~/components/Loading.vue' import VideoCard from '~/components/VideoCard/VideoCard.vue' -import VideoCardSkeleton from '~/components/VideoCard/VideoCardSkeleton.vue' import { useApiClient } from '~/composables/api' import { useBewlyApp } from '~/composables/useAppProvider' import { LanguageType } from '~/enums/appEnums' @@ -20,6 +18,17 @@ import type { forYouResult, Item as VideoItem } from '~/models/video/forYou' import { getTvSign, TVAppKey } from '~/utils/authProvider' import { isVerticalVideo } from '~/utils/uriParse' +// https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L16 +interface VideoElement { + uniqueId: string + item?: VideoItem +} + +interface AppVideoElement { + uniqueId: string + item?: AppVideoItem +} + const props = defineProps<{ gridLayout: GridLayout }>() @@ -39,8 +48,8 @@ const gridValue = computed((): string => { const toast = useToast() const api = useApiClient() -const videoList = reactive([]) -const appVideoList = reactive([]) +const videoList = ref([]) +const appVideoList = ref([]) const isLoading = ref(true) const needToLoginFirst = ref(false) const containerRef = ref() as Ref @@ -112,8 +121,8 @@ onActivated(() => { }) async function initData() { - videoList.length = 0 - appVideoList.length = 0 + videoList.value.length = 0 + appVideoList.value.length = 0 await getData() } @@ -149,6 +158,16 @@ async function getRecommendVideos() { emit('beforeLoading') isLoading.value = true try { + let i = 0 + // https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L208 + // When video list is not empty, addthe number of pending videos is half of the page size + // is set to prevent user scrolling the page too fast and causing the page too laggy + const pendingVideos: VideoElement[] = Array.from({ length: videoList.value.length ? pageSize / 2 : pageSize }, () => ({ + uniqueId: `unique-id-${(videoList.value.length || 0) + i++})}`, + } satisfies VideoElement)) + let lastVideoListLength = videoList.value.length + videoList.value.push(...pendingVideos) + const response: forYouResult = await api.video.getRecommendVideos({ fresh_idx: refreshIdx.value++, ps: pageSize, @@ -167,18 +186,25 @@ async function getRecommendVideos() { }) // when videoList has length property, it means it is the first time to load - if (!videoList.length) { - Object.assign(videoList, resData) + if (!videoList.value.length) { + videoList.value = resData.map(item => ({ uniqueId: `${item.id}`, item })) } else { - // else we concat the new data to the old data - Object.assign(videoList, videoList.concat(resData)) + resData.forEach((item) => { + videoList.value[lastVideoListLength++] = { + uniqueId: `${item.id}`, + item, + } + }) } } else if (response.code === 62011) { needToLoginFirst.value = true } } + catch { + videoList.value = videoList.value.filter(video => video.item) + } finally { isLoading.value = false emit('afterLoading') @@ -189,12 +215,23 @@ async function getAppRecommendVideos() { emit('beforeLoading') isLoading.value = true try { + let i = 0 + // https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L208 + // When video list is not empty, there will be approximately 10 pending videos + // due to the app recommendation filtering ad cards. + // To prevent user scrolling the page too fast and causing the page too laggy + const pendingVideos: AppVideoElement[] = Array.from({ length: appVideoList.value.length ? 10 : pageSize }, () => ({ + uniqueId: `unique-id-${(appVideoList.value.length || 0) + i++})}`, + } satisfies AppVideoElement)) + let lastVideoListLength = appVideoList.value.length + appVideoList.value.push(...pendingVideos) + const response: AppForYouResult = await api.video.getAppRecommendVideos({ access_key: accessKey.value, s_locale: settings.value.language === LanguageType.Mandarin_TW || settings.value.language === LanguageType.Cantonese ? 'zh-Hant_TW' : 'zh-Hans_CN', c_locate: settings.value.language === LanguageType.Mandarin_TW || settings.value.language === LanguageType.Cantonese ? 'zh-Hant_TW' : 'zh-Hans_CN', appkey: TVAppKey.appkey, - idx: appVideoList.length > 0 ? appVideoList[appVideoList.length - 1].idx : 1, + idx: appVideoList.value.length > 0 ? appVideoList.value[appVideoList.value.length - 1].item?.idx : 1, }) if (response.code === 0) { @@ -207,12 +244,16 @@ async function getAppRecommendVideos() { }) // when videoList has length property, it means it is the first time to load - if (!appVideoList.length) { - Object.assign(appVideoList, resData) + if (!appVideoList.value.length) { + appVideoList.value = resData.map(item => ({ uniqueId: `${item.idx}`, item })) } else { - // else we concat the new data to the old data - Object.assign(appVideoList, appVideoList.concat(resData)) + resData.forEach((item) => { + appVideoList.value[lastVideoListLength++] = { + uniqueId: `${item.idx}`, + item, + } + }) } } else if (response.code === 62011) { @@ -220,6 +261,9 @@ async function getAppRecommendVideos() { } } finally { + // Since the video list in app recommendation mode will filter the ad cards, + // after loading, the video list will be filtered again to remove the empty cards + appVideoList.value = appVideoList.value.filter(video => video.item) isLoading.value = false emit('afterLoading') } @@ -358,7 +402,7 @@ function handleAppUndoDislike(video: AppVideoItem) { }) } -function getVideoUniqueKey(video: VideoItem) { +function getVideoUniqueKey(video: VideoItem): string { return video.id + (video.bvid || video.uri || '') } @@ -467,82 +511,72 @@ defineExpose({ initData }) - - - - - - + diff --git a/src/contentScripts/views/Home/components/SubscribedSeries.vue b/src/contentScripts/views/Home/components/SubscribedSeries.vue index 0e8158af..d4d973ec 100644 --- a/src/contentScripts/views/Home/components/SubscribedSeries.vue +++ b/src/contentScripts/views/Home/components/SubscribedSeries.vue @@ -3,14 +3,18 @@ import type { Ref } from 'vue' import Button from '~/components/Button.vue' import Empty from '~/components/Empty.vue' -import Loading from '~/components/Loading.vue' import VideoCard from '~/components/VideoCard/VideoCard.vue' -import VideoCardSkeleton from '~/components/VideoCard/VideoCardSkeleton.vue' import { useApiClient } from '~/composables/api' import { useBewlyApp } from '~/composables/useAppProvider' import type { GridLayout } from '~/logic' import type { DataItem as MomentItem, MomentResult } from '~/models/moment/moment' +// https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L16 +interface VideoElement { + uniqueId: string + item?: MomentItem +} + const props = defineProps<{ gridLayout: GridLayout }>() @@ -27,8 +31,10 @@ const gridValue = computed((): string => { return '~ cols-1 xl:cols-2 gap-4' return '~ cols-1 gap-4' }) + const api = useApiClient() -const momentList = reactive([]) + +const videoList = ref([]) const isLoading = ref(false) const needToLoginFirst = ref(false) const containerRef = ref() as Ref @@ -50,7 +56,7 @@ onActivated(() => { async function initData() { offset.value = '' updateBaseline.value = '' - momentList.length = 0 + videoList.value.length = 0 noMoreContent.value = false noMoreContentWarning.value = false @@ -93,6 +99,16 @@ async function getFollowedUsersVideos() { emit('beforeLoading') isLoading.value = true try { + let i = 0 + // https://github.com/starknt/BewlyBewly/blob/fad999c2e482095dc3840bb291af53d15ff44130/src/contentScripts/views/Home/components/ForYou.vue#L208 + // When video list is not empty, addthe number of pending videos is half of the page size + // is set to prevent user scrolling the page too fast and causing the page too laggy + const pendingVideos: VideoElement[] = Array.from({ length: videoList.value.length ? 10 : 30 }, () => ({ + uniqueId: `unique-id-${(videoList.value.length || 0) + i++})}`, + } satisfies VideoElement)) + let lastVideoListLength = videoList.value.length + videoList.value.push(...pendingVideos) + const response: MomentResult = await api.moment.getMoments({ type: 'pgc', offset: Number(offset.value), @@ -116,12 +132,16 @@ async function getFollowedUsersVideos() { }) // when videoList has length property, it means it is the first time to load - if (!momentList.length) { - Object.assign(momentList, resData) + if (!videoList.value.length) { + videoList.value = resData.map(item => ({ uniqueId: `${item.id_str}`, item })) } else { - // else we concat the new data to the old data - Object.assign(momentList, momentList.concat(resData)) + resData.forEach((item) => { + videoList.value[lastVideoListLength++] = { + uniqueId: `${item.id_str}`, + item, + } + }) } } else if (response.code === -101) { @@ -129,6 +149,7 @@ async function getFollowedUsersVideos() { } } finally { + videoList.value = videoList.value.filter(video => video.item) isLoading.value = false emit('afterLoading') } @@ -160,40 +181,29 @@ defineExpose({ initData }) :grid="gridValue" > - - - - - - - diff --git a/src/contentScripts/views/Home/components/Trending.vue b/src/contentScripts/views/Home/components/Trending.vue index 95aad0b4..a91e33c5 100644 --- a/src/contentScripts/views/Home/components/Trending.vue +++ b/src/contentScripts/views/Home/components/Trending.vue @@ -1,14 +1,18 @@