Optimize initial loading speed (LazyLoad).
This commit is contained in:
@@ -7,6 +7,7 @@ import HowItWorks from "@/components/web/HowItWorks";
|
||||
import YouTubePlayer from "@/components/common/YouTubePlayer";
|
||||
import KeyFeatures from "@/components/web/KeyFeatures";
|
||||
import type { Messages } from "@/types/messages";
|
||||
import LazyLoadWrapper from "@/components/common/LazyLoadWrapper";
|
||||
|
||||
interface PageContentProps {
|
||||
messages: Messages;
|
||||
@@ -39,53 +40,65 @@ export default function HomeClient({ messages, lang }: PageContentProps) {
|
||||
</section>
|
||||
{/* Demo Video Section */}
|
||||
<section className="mb-12" aria-label="Product Demo">
|
||||
<h2 className="text-3xl font-bold mb-6 text-center">
|
||||
{messages.text.home.h2_demo}
|
||||
</h2>
|
||||
<p className="text-center mb-6 text-gray-600">
|
||||
{messages.text.home.h2P_demo}
|
||||
</p>
|
||||
<YouTubePlayer videoId={youtube_videoId} />
|
||||
<LazyLoadWrapper>
|
||||
<h2 className="text-3xl font-bold mb-6 text-center">
|
||||
{messages.text.home.h2_demo}
|
||||
</h2>
|
||||
<p className="text-center mb-6 text-gray-600">
|
||||
{messages.text.home.h2P_demo}
|
||||
</p>
|
||||
<YouTubePlayer videoId={youtube_videoId} />
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<p className="mb-3 text-gray-700">{messages.text.home.watch_tips}</p>
|
||||
<a
|
||||
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
|
||||
href={`https://www.youtube.com/watch?v=${youtube_videoId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{messages.text.home.youtube_tips}
|
||||
</a>
|
||||
<a
|
||||
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
|
||||
href={`https://www.bilibili.com/video/${bilibili_videoId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{messages.text.home.bilibili_tips}
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-4 text-center">
|
||||
<p className="mb-3 text-gray-700">
|
||||
{messages.text.home.watch_tips}
|
||||
</p>
|
||||
<a
|
||||
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
|
||||
href={`https://www.youtube.com/watch?v=${youtube_videoId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{messages.text.home.youtube_tips}
|
||||
</a>
|
||||
<a
|
||||
className="flex justify-center gap-4 text-blue-500 hover:underline transition-colors"
|
||||
href={`https://www.bilibili.com/video/${bilibili_videoId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{messages.text.home.bilibili_tips}
|
||||
</a>
|
||||
</div>
|
||||
</LazyLoadWrapper>
|
||||
</section>
|
||||
{/* How It Works Section */}
|
||||
<section aria-label="How It Works">
|
||||
<HowItWorks messages={messages} />
|
||||
<LazyLoadWrapper>
|
||||
<HowItWorks messages={messages} />
|
||||
</LazyLoadWrapper>
|
||||
</section>
|
||||
{/* System Architecture Section */}
|
||||
<section aria-label="System Architecture">
|
||||
<SystemDiagram messages={messages} />
|
||||
<LazyLoadWrapper>
|
||||
<SystemDiagram messages={messages} />
|
||||
</LazyLoadWrapper>
|
||||
</section>
|
||||
{/* Key Features */}
|
||||
<section aria-label="Key Features">
|
||||
<KeyFeatures messages={messages} />
|
||||
<LazyLoadWrapper>
|
||||
<KeyFeatures messages={messages} />
|
||||
</LazyLoadWrapper>
|
||||
</section>
|
||||
{/* FAQ Section */}
|
||||
<section aria-label="Frequently Asked Questions">
|
||||
<FAQSection
|
||||
messages={messages}
|
||||
isMainPage
|
||||
titleClassName="text-2xl md:text-3xl"
|
||||
/>
|
||||
<LazyLoadWrapper>
|
||||
<FAQSection
|
||||
messages={messages}
|
||||
isMainPage
|
||||
titleClassName="text-2xl md:text-3xl"
|
||||
/>
|
||||
</LazyLoadWrapper>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
interface LazyLoadWrapperProps {
|
||||
children: ReactNode;
|
||||
// 可以设置一个延迟,让组件在进入视口后稍微等一下再渲染,避免滚动过快时频繁渲染
|
||||
// rootMargin 可以让组件在距离视口还有 N 像素时就开始加载
|
||||
options?: {
|
||||
triggerOnce?: boolean;
|
||||
rootMargin?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function LazyLoadWrapper({
|
||||
children,
|
||||
options = { triggerOnce: true, rootMargin: "200px" },
|
||||
}: LazyLoadWrapperProps) {
|
||||
const { ref, inView } = useInView(options);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && !isLoaded) {
|
||||
setIsLoaded(true);
|
||||
}
|
||||
}, [inView, isLoaded]);
|
||||
|
||||
// 使用一个 div 包裹并附加 ref,同时可以设置最小高度,防止懒加载时页面布局跳动
|
||||
return (
|
||||
<div ref={ref} className="min-h-[200px]">
|
||||
{isLoaded ? children : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,48 +12,29 @@ const YouTubePlayer: React.FC<YouTubePlayerProps> = ({
|
||||
className = "",
|
||||
}) => {
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [thumbnailLoaded, setThumbnailLoaded] = useState<boolean>(false);
|
||||
const [currentThumbnail, setCurrentThumbnail] = useState<string>("");
|
||||
|
||||
const thumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
|
||||
const fallbackUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
|
||||
|
||||
const localThumbnail = "/inActionThumbnail.webp";
|
||||
const embedUrl = `https://www.youtube.com/embed/${videoId}?autoplay=1`;
|
||||
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setCurrentThumbnail(img.src);
|
||||
setThumbnailLoaded(true);
|
||||
};
|
||||
img.onerror = () => {
|
||||
img.src = fallbackUrl;
|
||||
};
|
||||
img.src = thumbnailUrl;
|
||||
}, [videoId, thumbnailUrl, fallbackUrl]);
|
||||
|
||||
return (
|
||||
<div className={`relative w-full max-w-5xl mx-auto ${className}`}>
|
||||
<div className="relative pb-[56.25%]">
|
||||
{!isPlaying ? (
|
||||
<div className="absolute top-0 left-0 w-full h-full">
|
||||
{currentThumbnail && (
|
||||
<img
|
||||
src={currentThumbnail}
|
||||
alt="Video thumbnail"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
{thumbnailLoaded && (
|
||||
<button
|
||||
onClick={() => setIsPlaying(true)}
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
bg-black bg-opacity-70 hover:bg-opacity-90 rounded-full p-4
|
||||
transition-all duration-300 ease-in-out z-10"
|
||||
aria-label="Play video"
|
||||
>
|
||||
<Play size={48} className="text-white" />
|
||||
</button>
|
||||
)}
|
||||
<img
|
||||
src={localThumbnail}
|
||||
alt="Video preview"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setIsPlaying(true)}
|
||||
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
bg-black bg-opacity-70 hover:bg-opacity-90 rounded-full p-4
|
||||
transition-all duration-300 ease-in-out z-10"
|
||||
aria-label="Play video"
|
||||
>
|
||||
<Play size={48} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
|
||||
@@ -70,15 +70,9 @@ export default function HowItWorks({ messages }: PageContentProps) {
|
||||
{/* Right Side - Demo Animation */}
|
||||
<div className="w-full md:w-1/2">
|
||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
{/* The default Next.js image optimizer does not support handling of GIF animations */}
|
||||
<Image
|
||||
src="/HowItWorks.gif"
|
||||
alt="How PrivyDrop Works"
|
||||
unoptimized
|
||||
width={700}
|
||||
height={921}
|
||||
className="mx-auto mb-6"
|
||||
/>
|
||||
<video autoPlay loop muted playsInline width="1920" height="75">
|
||||
<source src="/HowItWorks.webm" type="video/webm" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"qrcode.react": "^4.0.1",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io-client": "^4.7.5",
|
||||
|
||||
Generated
+18
@@ -104,6 +104,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-intersection-observer:
|
||||
specifier: ^9.16.0
|
||||
version: 9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@@ -2861,6 +2864,15 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-intersection-observer@9.16.0:
|
||||
resolution: {integrity: sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -6738,6 +6750,12 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-intersection-observer@9.16.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.22)(react@18.3.1):
|
||||
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 325 KiB |
Reference in New Issue
Block a user