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 */}
-
-
-
-
-
+ {/* 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. */}
+