add lazy loading attributes to images and fix potential runaway timers in main page scroll logic
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { Box, Image, Button, HStack, VStack, Text } from '@chakra-ui/react'
|
||||
import { useState, useEffect, CSSProperties } from 'react'
|
||||
import { useState, useEffect, useMemo, useRef, CSSProperties } from 'react'
|
||||
|
||||
interface CyclingImageProps {
|
||||
src: string
|
||||
@@ -49,6 +49,12 @@ const CyclingImage = ({
|
||||
const [currentIntensity, setCurrentIntensity] = useState(intensity)
|
||||
const [currentImage, setCurrentImage] = useState(src)
|
||||
|
||||
// Keep refs to latest src/src2 so the setInterval callback is never stale
|
||||
const srcRef = useRef(src)
|
||||
const src2Ref = useRef(src2)
|
||||
useEffect(() => { srcRef.current = src }, [src])
|
||||
useEffect(() => { src2Ref.current = src2 }, [src2])
|
||||
|
||||
useEffect(() => {
|
||||
setSpeed(cycleDuration)
|
||||
}, [cycleDuration])
|
||||
@@ -58,11 +64,12 @@ const CyclingImage = ({
|
||||
if (!src2 || !isPlaying) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentImage(prev => prev === src ? src2 : src)
|
||||
// Use refs to read latest prop values, avoiding stale closures
|
||||
setCurrentImage(prev => prev === srcRef.current ? src2Ref.current! : srcRef.current)
|
||||
}, speed * 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [src, src2, speed, isPlaying])
|
||||
}, [src2, speed, isPlaying])
|
||||
|
||||
const togglePlayPause = () => {
|
||||
setIsPlaying(!isPlaying)
|
||||
@@ -76,9 +83,10 @@ const CyclingImage = ({
|
||||
setCurrentIntensity(value)
|
||||
}
|
||||
|
||||
// CSS keyframes animation
|
||||
// CSS keyframes animation — memoized so the <style> tag is only
|
||||
// re-written when the intensity value actually changes.
|
||||
const animationName = 'lightToDarkCycle'
|
||||
const animationStyle = `
|
||||
const animationStyle = useMemo(() => `
|
||||
@keyframes ${animationName} {
|
||||
0% {
|
||||
filter: brightness(${1 - currentIntensity * 1.5});
|
||||
@@ -90,7 +98,7 @@ const CyclingImage = ({
|
||||
filter: brightness(${1 - currentIntensity * 1.5});
|
||||
}
|
||||
}
|
||||
`
|
||||
`, [currentIntensity])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -208,6 +208,7 @@ function Advantages() {
|
||||
src='/images/new/heart.webp'
|
||||
title="獅球嘜營萃護心油"
|
||||
alt="獅球嘜營萃護心油"
|
||||
loading="lazy"
|
||||
w={{ base: '75vw', sm: '75vw', md: '40vw', lg: '40vw', xl: '40vw' }}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Box, Image, Stack, Text } from '@chakra-ui/react'
|
||||
import CyclingImage from './CyclingImage'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
const MotionImage = motion.create(Image)
|
||||
@@ -9,6 +9,7 @@ const MotionText = motion.create(Text)
|
||||
function Hero1() {
|
||||
const [headerHeight, setHeaderHeight] = useState(80);
|
||||
const bigWarningSize = { base: "17vw", sm: "15vw", md: "9vw", lg: "9vw", xl: "8vw" };
|
||||
const delayedUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeaderHeight = () => {
|
||||
@@ -20,13 +21,15 @@ function Hero1() {
|
||||
};
|
||||
|
||||
const delayedUpdate = () => {
|
||||
setTimeout(updateHeaderHeight, 100);
|
||||
clearTimeout(delayedUpdateTimerRef.current);
|
||||
delayedUpdateTimerRef.current = setTimeout(updateHeaderHeight, 100);
|
||||
};
|
||||
|
||||
delayedUpdate();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
setTimeout(updateHeaderHeight, 50);
|
||||
clearTimeout(delayedUpdateTimerRef.current);
|
||||
delayedUpdateTimerRef.current = setTimeout(updateHeaderHeight, 50);
|
||||
});
|
||||
|
||||
const header = document.querySelector('[data-header="true"]');
|
||||
@@ -38,6 +41,7 @@ function Hero1() {
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', delayedUpdate);
|
||||
clearTimeout(delayedUpdateTimerRef.current);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -34,6 +34,7 @@ function Hero2() {
|
||||
left="50%"
|
||||
title="獅球嘜營萃護心油"
|
||||
alt="獅球嘜營萃護心油"
|
||||
loading="lazy"
|
||||
top={{ base: "10px", sm: "20px", md: "30px", lg: "30px", xl: "40px" }}
|
||||
w={{ base: "70vw", sm: "70vw", md: "35vw", lg: "35vw", xl: "28vw" }}
|
||||
initial={{ opacity: 0, y: -30, x: '-50%' }}
|
||||
@@ -45,6 +46,7 @@ function Hero2() {
|
||||
src="/images/new/hero2subtitle.webp"
|
||||
position={'absolute'}
|
||||
zIndex={1}
|
||||
loading="lazy"
|
||||
top={{ base: "35vw", sm: "35vw", md: "17vw", lg: "15vw", xl: "12vw" }}
|
||||
right={{ base: "55vw", sm: "55vw", md: "24vw", lg: "25vw", xl: "30vw" }}
|
||||
w={{ base: "34vw", sm: "34vw", md: "22vw", lg: "22vw", xl: "18vw" }}
|
||||
@@ -59,6 +61,7 @@ function Hero2() {
|
||||
alt="獅球嘜營萃護心油"
|
||||
title="獅球嘜營萃護心油"
|
||||
zIndex={0}
|
||||
loading="lazy"
|
||||
position={'absolute'}
|
||||
left="50%"
|
||||
bottom={0}
|
||||
|
||||
@@ -85,6 +85,7 @@ function Info() {
|
||||
src='/images/new/info_title.webp'
|
||||
w={{ base: '90vw', sm: '80vw', md: '45vw', lg: '35vw', xl: '22vw' }}
|
||||
mt={'50px'}
|
||||
loading="lazy"
|
||||
initial={{ opacity: 0, y: -30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
@@ -156,7 +157,7 @@ function Info() {
|
||||
src={infoData.find(info => info.id === selectedInfo)?.image}
|
||||
w="100%"
|
||||
p={5}
|
||||
|
||||
loading="lazy"
|
||||
/>
|
||||
<Box px={{ base: 5, sm: 5, md: 6, lg: 5, xl: 5 }}
|
||||
pb={{ base: 5, sm: 5, md: 6, lg: 9, xl: 9 }}>
|
||||
|
||||
@@ -68,6 +68,7 @@ function OilInfo() {
|
||||
src='/images/cookmethods.webp'
|
||||
alt='烹調方法'
|
||||
title='烹調方法'
|
||||
loading="lazy"
|
||||
w={{ base: '100%', sm: '90%', md: '85%' }}
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
@@ -109,6 +110,7 @@ function OilInfo() {
|
||||
<MotionImage
|
||||
src='/images/new/buttons.webp'
|
||||
w={{ base: '100%', sm: '100%', md: '90%' }}
|
||||
loading="lazy"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={isMainInView ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.5, delay: 1.0, ease: "easeOut" }}
|
||||
|
||||
@@ -91,7 +91,11 @@ function Truth() {
|
||||
// Parse XFBML when SDK is loaded
|
||||
const handleSDKLoad = () => {
|
||||
if ((window as any).FB && fbContainerRef.current) {
|
||||
(window as any).FB.XFBML.parse(fbContainerRef.current);
|
||||
try {
|
||||
(window as any).FB.XFBML.parse(fbContainerRef.current);
|
||||
} catch (_e) {
|
||||
// FB SDK may fail (e.g. blocked by ad-blockers) — ignore silently
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useRouterState } from '@tanstack/react-router'
|
||||
import Hero1 from '../components/new_ui/hero1'
|
||||
import Hero2 from '../components/new_ui/hero2'
|
||||
@@ -16,23 +16,26 @@ import Header from '@/components/header'
|
||||
function MainPage() {
|
||||
const { location } = useRouterState()
|
||||
|
||||
// Refs must live at component top level (Rules of Hooks).
|
||||
// Storing timer IDs in refs means the cleanup closure always reads the
|
||||
// latest value, preventing runaway timers after the component unmounts.
|
||||
const animationFrameRef = useRef(0)
|
||||
const retryTimeoutRef = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== '/40plus') {
|
||||
return
|
||||
}
|
||||
|
||||
let animationFrame = 0
|
||||
let retryTimeout: number | undefined
|
||||
|
||||
// Replay pending section scroll requests written to sessionStorage
|
||||
const clearTimers = () => {
|
||||
if (animationFrame) {
|
||||
window.cancelAnimationFrame(animationFrame)
|
||||
animationFrame = 0
|
||||
if (animationFrameRef.current) {
|
||||
window.cancelAnimationFrame(animationFrameRef.current)
|
||||
animationFrameRef.current = 0
|
||||
}
|
||||
if (retryTimeout) {
|
||||
window.clearTimeout(retryTimeout)
|
||||
retryTimeout = undefined
|
||||
if (retryTimeoutRef.current) {
|
||||
window.clearTimeout(retryTimeoutRef.current)
|
||||
retryTimeoutRef.current = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,17 +47,17 @@ function MainPage() {
|
||||
if (element) {
|
||||
window.sessionStorage.removeItem('pendingScrollSection')
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
animationFrame = 0
|
||||
retryTimeout = undefined
|
||||
animationFrameRef.current = 0
|
||||
retryTimeoutRef.current = undefined
|
||||
return
|
||||
}
|
||||
|
||||
retryTimeout = window.setTimeout(() => {
|
||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
||||
retryTimeoutRef.current = window.setTimeout(() => {
|
||||
animationFrameRef.current = window.requestAnimationFrame(scrollToTarget)
|
||||
}, 50)
|
||||
}
|
||||
|
||||
animationFrame = window.requestAnimationFrame(scrollToTarget)
|
||||
animationFrameRef.current = window.requestAnimationFrame(scrollToTarget)
|
||||
}
|
||||
|
||||
const runScroll = () => {
|
||||
|
||||
Reference in New Issue
Block a user