From 3e9a988aaf97d7b30c3a441223d3dbc6c3368725 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:35:52 +0200 Subject: [PATCH] =?UTF-8?q?perf(pitch-deck):=20smooth=20SDK=20demo=20carou?= =?UTF-8?q?sel=20=E2=80=94=20no=20blank=20frames,=20parallel=20preload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK Live Demo was janky: AnimatePresence mode="wait" unmounted the current Image before mounting the next, so every advance forced a cold fetch and left an empty black frame until the new image decoded. Only the first three screenshots had priority; the rest fetched lazily, so the first pass through the carousel repeatedly stalled. Replaces the single swap-in/swap-out Image with a stack of 23 images layered in an aspect-[1920/1080] container. Cross-fades are now pure CSS opacity on always-mounted nodes, so there is no unmount and no gap. Key details: - priority on the first 3 (triggers ); loading=eager on the remaining 20 so the browser starts all fetches at mount rather than deferring via IntersectionObserver. - sizes="(max-width: 1024px) 100vw, 1024px" lets next/image serve the actual displayed resolution instead of the 1920 hint — fewer bytes, faster first paint. - Load-gated reveal: a new `shown` state trails `current` until the target image fires onLoadingComplete. If the user clicks ahead of the network, the previous loaded screenshot stays visible — no more black flashes before images arrive. Second pass through the carousel is instant (images are in-cache). Co-Authored-By: Claude Sonnet 4.6 --- pitch-deck/components/slides/SDKDemoSlide.tsx | 66 +++++++++++++------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/pitch-deck/components/slides/SDKDemoSlide.tsx b/pitch-deck/components/slides/SDKDemoSlide.tsx index b12b9e2..4565085 100644 --- a/pitch-deck/components/slides/SDKDemoSlide.tsx +++ b/pitch-deck/components/slides/SDKDemoSlide.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { motion, AnimatePresence } from 'framer-motion' import Image from 'next/image' import { Language } from '@/lib/types' @@ -43,6 +43,26 @@ export default function SDKDemoSlide({ lang }: SDKDemoSlideProps) { const [fullscreen, setFullscreen] = useState(false) const [autoPlay, setAutoPlay] = useState(true) + // Track which images have actually loaded so we never cross-fade to a blank + // frame. While the target image is still fetching, `shown` stays on the + // previous loaded one — this eliminates the flash of empty canvas the user + // hit on the first pass through the carousel. + const loadedRef = useRef>(new Set()) + const [shown, setShown] = useState(0) + + const handleLoaded = useCallback((idx: number) => { + loadedRef.current.add(idx) + // If the user is currently waiting on this image, reveal it immediately. + // Otherwise the preceding loaded image keeps showing — no blank flash. + if (idx === current) setShown(idx) + }, [current]) + + useEffect(() => { + if (loadedRef.current.has(current)) { + setShown(current) + } + }, [current]) + const next = useCallback(() => { setCurrent(i => (i + 1) % SCREENSHOTS.length) }, []) @@ -101,25 +121,31 @@ export default function SDKDemoSlide({ lang }: SDKDemoSlideProps) { - {/* Screenshot */} - - - {de - - + {/* Screenshot stack — all images mount at once so we can cross-fade + between them by toggling opacity. AnimatePresence mode="wait" + unmounts before the next mounts, which forces a cold fetch and + produces a blank frame; the stack avoids both. */} +
+ {SCREENSHOTS.map((s, idx) => ( +
+ {de handleLoaded(idx)} + /> +
+ ))} +
{/* Navigation arrows */}