From 97ce65844f2f577c76e6e7f147cbb348c6889a88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20S=C3=A1nchez?= Date: Thu, 28 May 2026 22:27:59 -0500 Subject: [PATCH 1/8] =?UTF-8?q?perf:=20reduce=20client=20hydration=20work?= =?UTF-8?q?=20=E2=80=94=20server=20components=20and=20deferred=20effects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove "use client" from AnimatedMarquee and PageTransition — pure rendering, no browser APIs needed - Replace useLayoutEffect with useEffect in ExperienceList and ProjectList — avoids forcing synchronous layout before browser paint --- components/ui/animated-marquee.tsx | 2 -- components/ui/experience-list.tsx | 4 ++-- components/ui/page-transition.tsx | 6 ++---- components/ui/project-list.tsx | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/components/ui/animated-marquee.tsx b/components/ui/animated-marquee.tsx index 7b4824b..e532c2c 100644 --- a/components/ui/animated-marquee.tsx +++ b/components/ui/animated-marquee.tsx @@ -1,5 +1,3 @@ -"use client" - interface AnimatedMarqueeProps { items: string[] } diff --git a/components/ui/experience-list.tsx b/components/ui/experience-list.tsx index 24011b2..afc21de 100644 --- a/components/ui/experience-list.tsx +++ b/components/ui/experience-list.tsx @@ -1,6 +1,6 @@ "use client" -import { useRef, useState, useEffect, useLayoutEffect } from "react" +import { useRef, useState, useEffect } from "react" import { ExperienceViewModel } from "@/src/interface-adapters/presenters/experience-presenter" import { ExperienceCard } from "./experience-card" import { ChevronLeft, ChevronRight } from "lucide-react" @@ -36,7 +36,7 @@ export function ExperienceList({ experiences }: { experiences: ExperienceViewMod return () => observer.disconnect() }, [experiences.length]) - useLayoutEffect(() => { + useEffect(() => { const container = scrollRef.current if (!container) return const titles = Array.from(container.querySelectorAll('[data-exp-title]')) diff --git a/components/ui/page-transition.tsx b/components/ui/page-transition.tsx index f82e6fd..0874272 100644 --- a/components/ui/page-transition.tsx +++ b/components/ui/page-transition.tsx @@ -1,5 +1,3 @@ -"use client" +import { ReactNode } from "react" -export const PageTransition = ({ children }: { children: React.ReactNode }) => { - return <>{children} -} +export const PageTransition = ({ children }: { children: ReactNode }) => <>{children} diff --git a/components/ui/project-list.tsx b/components/ui/project-list.tsx index 5899489..4f9e64b 100644 --- a/components/ui/project-list.tsx +++ b/components/ui/project-list.tsx @@ -1,6 +1,6 @@ "use client" -import { useRef, useState, useEffect, useLayoutEffect } from "react" +import { useRef, useState, useEffect } from "react" import { ProjectViewModel } from "@/src/interface-adapters/presenters/project-presenter" import { ProjectCard } from "./project-card" import { ChevronLeft, ChevronRight } from "lucide-react" @@ -13,7 +13,7 @@ export function ProjectList({ projects }: { projects: ProjectViewModel[] }) { const scrollRef = useRef(null) const wrapperRef = useRef(null) - useLayoutEffect(() => { + useEffect(() => { const container = scrollRef.current if (!container) return const captions = Array.from(container.querySelectorAll('[data-caption]')) From f2e29374ce29119812758dd107e9216230a841fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20S=C3=A1nchez?= Date: Thu, 28 May 2026 22:31:43 -0500 Subject: [PATCH 2/8] =?UTF-8?q?perf:=20halve=20marquee=20item=20count=20?= =?UTF-8?q?=E2=80=94=204x=20duplication=20was=202x=20more=20than=20needed?= =?UTF-8?q?=20for=20seamless=20CSS=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/ui/animated-marquee.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ui/animated-marquee.tsx b/components/ui/animated-marquee.tsx index e532c2c..b1e730c 100644 --- a/components/ui/animated-marquee.tsx +++ b/components/ui/animated-marquee.tsx @@ -3,7 +3,7 @@ interface AnimatedMarqueeProps { } export function AnimatedMarquee({ items }: AnimatedMarqueeProps) { - const marqueeItems = [...items, ...items, ...items, ...items] + const marqueeItems = [...items, ...items] return (
Date: Fri, 29 May 2026 13:59:31 -0500 Subject: [PATCH 3/8] fix: replace CSS scroll-driven animation with JS; fix CV dialog dvh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove animation-timeline: scroll(root) from .scroll-progress — this CSS scroll-driven animation on a position:fixed element blocked Safari 18's initial composite/paint step on iPhone, causing an 11-second blank screen. Replace with a passive scroll listener in Navbar that applies transform: scaleX() directly. - Change CV dialog max-h-[92vh] → max-h-[92dvh] so the dialog respects the dynamic viewport height (shrinks correctly when Safari tab bar / address bar are visible, preventing close button and footer from being clipped). --- app/globals.css | 10 +++------- components/ui/cv-dialog.tsx | 2 +- components/ui/navbar.tsx | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/globals.css b/app/globals.css index a05fb5f..1951a70 100644 --- a/app/globals.css +++ b/app/globals.css @@ -101,14 +101,10 @@ body { animation: marqueeScroll 30s linear infinite; } -/* Scroll-driven progress bar (Safari 18 / iOS 18+) */ -@keyframes growX { - from { transform: scaleX(0); } - to { transform: scaleX(1); } -} +/* Scroll progress bar — updated via JS in Navbar */ .scroll-progress { - animation: growX linear both; - animation-timeline: scroll(root); + transform: scaleX(0); + will-change: transform; } @keyframes shine { diff --git a/components/ui/cv-dialog.tsx b/components/ui/cv-dialog.tsx index cc911aa..d5ee857 100644 --- a/components/ui/cv-dialog.tsx +++ b/components/ui/cv-dialog.tsx @@ -66,7 +66,7 @@ export function CvDialog() { /> {/* Dialog */} -
+
{/* Header */}
diff --git a/components/ui/navbar.tsx b/components/ui/navbar.tsx index 28a02ff..1b5abbd 100644 --- a/components/ui/navbar.tsx +++ b/components/ui/navbar.tsx @@ -14,6 +14,7 @@ export function Navbar() { const [activeSection, setActiveSection] = useState("") const [menuOpen, setMenuOpen] = useState(false) + const progressRef = useRef(null) const breadTopOpenRef = useRef(null) const breadTopCloseRef = useRef(null) const breadBotOpenRef = useRef(null) @@ -33,6 +34,17 @@ export function Navbar() { return () => observer.disconnect() }, []) + useEffect(() => { + const el = progressRef.current + if (!el) return + const onScroll = () => { + const scrolled = window.scrollY / (document.documentElement.scrollHeight - window.innerHeight) + el.style.transform = `scaleX(${Math.min(1, Math.max(0, scrolled))})` + } + window.addEventListener("scroll", onScroll, { passive: true }) + return () => window.removeEventListener("scroll", onScroll) + }, []) + useEffect(() => { const onResize = () => { if (window.innerWidth >= 768) setMenuOpen(false) } window.addEventListener("resize", onResize, { passive: true }) @@ -67,8 +79,7 @@ export function Navbar() { Skip to content - {/* Scroll Progress Bar — CSS scroll-driven, no JS */} -
+