From 8a0ca181e7890e0af7e13fbf64a471844aeb2389 Mon Sep 17 00:00:00 2001 From: eric Date: Sat, 29 Aug 2026 02:22:55 -0500 Subject: [PATCH] Optimize homepage: defer below-fold mounts, lighter particles, faster loader. Co-authored-by: Cursor --- frontend/src/app/changelog/page.tsx | 2 + frontend/src/app/globals.css | 43 +++++++++++ frontend/src/components/FocusTimer.tsx | 20 +++++- frontend/src/components/HomeClient.tsx | 83 +++++++++++----------- frontend/src/components/Loader.tsx | 19 ++++- frontend/src/components/ParticleCanvas.tsx | 66 ++++++++++++----- frontend/src/components/WhenVisible.tsx | 43 +++++++++++ 7 files changed, 212 insertions(+), 64 deletions(-) create mode 100644 frontend/src/components/WhenVisible.tsx diff --git a/frontend/src/app/changelog/page.tsx b/frontend/src/app/changelog/page.tsx index b50c301..30f3084 100644 --- a/frontend/src/app/changelog/page.tsx +++ b/frontend/src/app/changelog/page.tsx @@ -30,6 +30,8 @@ const LOGS = [ "新增饮食饮水注意事项", "修复生产环境 CSS 解析失败导致的页面异常", "部署改为原生 systemd(非 Docker 优先),增量构建更快", + "首页视口内按需挂载下方模块,加快首屏;粒子动画减负;Loader 更快消失", + "专注时钟增加累计专注块统计", ], }, { diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index a54b8b1..feb56a9 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -1169,6 +1169,49 @@ img { max-width: 100%; display: block; } transform: translateY(0); } +.section { + content-visibility: auto; + contain-intrinsic-size: auto 480px; +} + +.deferred-skeleton { + height: 120px; + margin: 24px auto; + max-width: 1200px; + border-radius: var(--radius-xl); + background: linear-gradient(90deg, rgba(255,255,255,0.03), rgba(255,255,255,0.07), rgba(255,255,255,0.03)); + background-size: 200% 100%; + animation: deferredShimmer 1.2s ease-in-out infinite; +} + +@keyframes deferredShimmer { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } +} + +.focus-streak { + text-align: center; + color: var(--text-secondary); + font-size: 0.9rem; + margin: -4px 0 12px; +} + +.focus-streak b { + color: var(--accent-3); + font-size: 1.1rem; +} + +@media (prefers-reduced-motion: reduce) { + .reveal { + opacity: 1; + transform: none; + transition: none; + } + .deferred-skeleton { animation: none; } + .loader-ring, + .loader-progress { animation: none !important; } +} + /* ===== Responsive ===== */ @media (max-width: 1024px) { .hero-top { diff --git a/frontend/src/components/FocusTimer.tsx b/frontend/src/components/FocusTimer.tsx index d13c713..c3d5275 100644 --- a/frontend/src/components/FocusTimer.tsx +++ b/frontend/src/components/FocusTimer.tsx @@ -3,18 +3,32 @@ import { useEffect, useState } from "react"; const PRESETS = [25, 45, 60]; +const STORAGE_KEY = "nomadro-focus-streak"; export default function FocusTimer() { const [minutes, setMinutes] = useState(25); const [secondsLeft, setSecondsLeft] = useState(25 * 60); const [running, setRunning] = useState(false); const [done, setDone] = useState(false); + const [sessions, setSessions] = useState(0); + + useEffect(() => { + try { + const n = Number(localStorage.getItem(STORAGE_KEY) || "0"); + if (!Number.isNaN(n)) setSessions(n); + } catch { /* ignore */ } + }, []); useEffect(() => { if (!running) return; if (secondsLeft <= 0) { setRunning(false); setDone(true); + setSessions((prev) => { + const next = prev + 1; + try { localStorage.setItem(STORAGE_KEY, String(next)); } catch { /* ignore */ } + return next; + }); return; } const t = setTimeout(() => setSecondsLeft((s) => s - 1), 1000); @@ -55,6 +69,10 @@ export default function FocusTimer() { +
+ 今日累计专注块 {sessions} +
+
{PRESETS.map((m) => (
- {done &&

🎉 完成一个专注块,起来走走吧

} + {done &&

完成一个专注块,起来走走吧

} diff --git a/frontend/src/components/HomeClient.tsx b/frontend/src/components/HomeClient.tsx index 330ac2d..05ca004 100644 --- a/frontend/src/components/HomeClient.tsx +++ b/frontend/src/components/HomeClient.tsx @@ -28,6 +28,7 @@ import BackToTop from "./BackToTop"; import TripPlanner from "./TripPlanner"; import Calculator from "./Calculator"; import SpinGlobe from "./SpinGlobe"; +import WhenVisible from "./WhenVisible"; const lazy =

(loader: () => Promise<{ default: ComponentType

}>) => dynamic(loader, { ssr: false, loading: () => null }); @@ -167,49 +168,49 @@ export default function HomeClient(props: Props) { - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +