Enable Next.js image optimization across ~60 remaining unoptimized <Image> spots

- Remove `unoptimized` from every Image that was pointing at a real remote/
  static file (Reels image carousel, story avatars/viewer, shop product
  grids/details, chat shared-post/product/story bubbles, notification/
  visitor rows, tag/mention pickers, etc.) so Next's own resize+avif/webp
  pipeline (already configured in next.config.ts) actually applies —
  previously only a handful of components used it, everything else shipped
  full-resolution originals regardless of the tiny thumbnail size shown
- Add a `sizes` prop wherever `fill` was used without one, so the optimizer
  picks an appropriately small srcset entry instead of defaulting to 100vw
- Fix ProfileAvatar to request the actual displayed size instead of always
  requesting 280x280 regardless of the `size` prop
- Left `unoptimized` on the handful of genuinely local/ephemeral sources
  (blob:/data: URIs from in-progress uploads, third-party QR API images,
  static SVG icons) where Next's optimizer can't or shouldn't apply
- Add next-story prefetch to StoryViewer (previously only Reels prefetched
  upcoming media; swiping to the next story always cold-started before)
- Fix .claude/launch.json's dev server port (3000 -> 3004) to match
  package.json's actual `next dev --port 3004`

Verified via `tsc --noEmit` (no new errors) and by diffing this app's
console/network behavior against a git-stashed baseline in the browser
preview — the one pre-existing hydration warning and 401s (unauthenticated
session) are present on both, and /_next/image requests now succeed for
avatars/thumbnails that previously bypassed optimization entirely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
payacom
2026-09-15 18:40:45 +03:30
parent 9310062616
commit d2f3a96ddf
44 changed files with 54 additions and 59 deletions

View File

@@ -97,7 +97,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "280px" : "120px"}
unoptimized
/>
)
) : (
@@ -144,7 +143,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "64px" : "36px"}
unoptimized
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-neutral-700 text-white">

View File

@@ -152,7 +152,6 @@ function ArchivePage() {
fill
className="object-cover"
sizes="50vw"
unoptimized
/>
)}
</button>

View File

@@ -66,8 +66,8 @@ function BlockedUsersPage() {
}
alt=""
fill
sizes="44px"
className="object-cover"
unoptimized
/>
</div>
<div className="min-w-0">

View File

@@ -296,7 +296,6 @@ export default function ProductManagePage() {
fill
className="object-cover"
sizes="400px"
unoptimized
/>
</div>
))}

View File

@@ -96,7 +96,6 @@ export default function CheckoutPage() {
fill
className="object-cover"
sizes="64px"
unoptimized
/>
</div>
)}

View File

@@ -347,7 +347,6 @@ export default function ListingDetailClient() {
fill
className="object-cover"
sizes="400px"
unoptimized
/>
</button>
))}

View File

@@ -356,7 +356,6 @@ export default function OrderDetailPage() {
fill
className="object-cover"
sizes="64px"
unoptimized
/>
</div>
)}

View File

@@ -85,7 +85,6 @@ export default function ProductComparisonPage() {
fill
className="object-cover"
sizes="64px"
unoptimized
/>
{discountPercent != null && (
<DiscountBadge

View File

@@ -350,7 +350,6 @@ export default function ShopProfileClient() {
fill
className="object-cover"
sizes="33vw"
unoptimized
/>
)}
</button>

View File

@@ -138,7 +138,6 @@ function MainBillboardCard({
height={120}
alt={billboard?.title}
src={`${IMAGE_BASE_URL}${billboard.images[0]}`}
unoptimized
/>
) : (
<div className="rounded-full bg-gray-300 flex items-center justify-center h-[86px] w-[86px] md:h-[120px] md:w-[120px] mt-4">

View File

@@ -52,8 +52,8 @@ export default function SharedPostBubble({ data }: { data: SharedPostPayload })
src={preview}
alt=""
fill
sizes="240px"
className="object-cover"
unoptimized
/>
)}
</div>

View File

@@ -38,7 +38,7 @@ export default function SharedProductBubble({ data }: { data: SharedProductPaylo
>
{preview && (
<div className="relative aspect-[4/3] w-full min-w-[200px] max-w-[240px] bg-neutral-900">
<Image src={preview} alt="" fill className="object-cover" unoptimized />
<Image src={preview} alt="" fill sizes="240px" className="object-cover" />
</div>
)}
<div className="px-2 py-1.5">

View File

@@ -79,8 +79,8 @@ export default function SharedStoryBubble({
src={preview}
alt=""
fill
sizes="120px"
className="object-cover"
unoptimized
/>
)}
</div>

View File

@@ -96,7 +96,6 @@ export default function ExploreVideoThumb({
fill
className="object-cover"
sizes="50vw"
unoptimized
/>
) : !frameReady ? (
<div className="absolute inset-0 animate-pulse bg-neutral-800" />

View File

@@ -321,8 +321,8 @@ export default function ReelsAcademyCard({
src={buildStorageUrl(item.profile_image)}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -67,9 +67,8 @@ function ProfileAvatar({
<Image
src={imageSrc}
alt={alt}
width={280}
height={280}
unoptimized
width={sizePxMap[size]}
height={sizePxMap[size]}
className={cn(
"profile-avatar aspect-square object-cover shrink-0",
sizeMap[size],

View File

@@ -40,7 +40,6 @@ const ServiceItem: React.FC<ServiceItemProps> = ({ service, onDelete }) => {
height={50}
src={buildStorageUrl(image) || image}
alt={title}
unoptimized
className="min-w-[50px] min-h-[50px] rounded-lg object-cover"
/>
) : (

View File

@@ -43,8 +43,8 @@ export default function AppearanceColorSelect({
src={selected.src}
alt={selected.label}
fill
sizes="36px"
className="object-cover"
unoptimized
/>
</div>
) : (
@@ -93,8 +93,8 @@ export default function AppearanceColorSelect({
src={opt.src}
alt={opt.label}
fill
sizes="56px"
className="object-cover"
unoptimized
/>
</div>
<span className="text-[10px] text-neutral-500">{opt.label}</span>

View File

@@ -222,9 +222,9 @@ function MainModelCard({
}
alt={user_name}
fill
sizes="72px"
className="aspect-square rounded-2xl object-cover"
priority
unoptimized
/>
</div>
</StoryRing>

View File

@@ -120,7 +120,6 @@ const SwipeImageSlider: React.FC<SwipeImageSliderProps> = ({
loading={idx === 0 ? "eager" : "lazy"}
className="min-h-[30vh] w-full rounded-2xl object-contain shadow-lg transition-opacity duration-300"
onLoadingComplete={() => setImageLoading?.(false)}
unoptimized
/>
)}
</div>

View File

@@ -143,7 +143,6 @@ function ModelContentPosts({
height={300}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105 group-active:scale-100"
loading="lazy"
unoptimized
/>
) : poster ? (
<Image
@@ -157,7 +156,6 @@ function ModelContentPosts({
height={300}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105 group-active:scale-100"
loading="lazy"
unoptimized
/>
) : (
<video

View File

@@ -111,8 +111,8 @@ export default function ProfileUserListModal({
}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
</div>
<div className="min-w-0">

View File

@@ -63,8 +63,8 @@ function LinkListItem({
src={imageSrc}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : (
<BoldIcon name={fallbackIcon} size={20} className="dark:invert" />

View File

@@ -153,11 +153,11 @@ export default function ReelsImageCarousel({
src={mediaFiles[0].src}
alt=""
fill
sizes="100vw"
className="object-contain select-none"
priority={priority}
draggable={false}
onLoad={() => onLoadingComplete?.()}
unoptimized
/>
</div>
);
@@ -182,6 +182,7 @@ export default function ReelsImageCarousel({
src={file.src}
alt=""
fill
sizes="100vw"
className="object-contain select-none"
priority={priority && Math.abs(idx - index) <= 1}
loading={
@@ -191,7 +192,6 @@ export default function ReelsImageCarousel({
onLoad={() => {
if (idx === index) onLoadingComplete?.();
}}
unoptimized
/>
</div>
))}

View File

@@ -59,8 +59,8 @@ function ReelsProfileFollowButton({
src={profileImage ? buildStorageUrl(profileImage) : "/images/fake-avatar.png"}
alt={userName}
fill
sizes="48px"
className="rounded-full object-cover"
unoptimized
/>
</div>
);

View File

@@ -130,8 +130,8 @@ export default function ReelsPostAnalyticsModal({
src={previewSrc}
alt=""
fill
sizes="112px"
className="object-cover"
unoptimized
/>
</div>
) : null}

View File

@@ -437,8 +437,8 @@ export default function ReelsPostCard({
src={videoPoster}
alt=""
fill
sizes="100vw"
className="object-contain"
unoptimized
priority={isActive || preloadPriority === "nearby"}
/>
)}
@@ -611,8 +611,8 @@ export default function ReelsPostCard({
}
alt={user_name}
fill
sizes="40px"
className="rounded-full object-cover"
unoptimized
/>
</div>
</StoryRing>

View File

@@ -137,8 +137,8 @@ export default function SendPostModal({
src={preview}
alt=""
fill
sizes="48px"
className="object-cover"
unoptimized
/>
</div>
)}
@@ -181,8 +181,8 @@ export default function SendPostModal({
}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -114,8 +114,8 @@ export default function TagUsersPicker({
src={src}
alt=""
fill
sizes="28px"
className="object-cover"
unoptimized
/>
) : null}
</div>
@@ -178,8 +178,8 @@ export default function TagUsersPicker({
src={src}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -107,7 +107,6 @@ function SpecialistCard({
loading="lazy"
className="object-cover"
sizes="(min-width: 1024px) 220px, (min-width: 640px) 30vw, 78vw"
unoptimized
/>
)
) : (
@@ -126,7 +125,6 @@ function SpecialistCard({
loading="lazy"
className="object-cover"
sizes="56px"
unoptimized
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-neutral-700 text-white">

View File

@@ -82,7 +82,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "280px" : "120px"}
unoptimized
/>
)
) : (
@@ -129,7 +128,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "64px" : "36px"}
unoptimized
/>
) : (
<div className="flex h-full w-full items-center justify-center bg-neutral-700 text-white">

View File

@@ -149,7 +149,6 @@ export default function ActivityPostGrid({ endpoint }: Props) {
fill
className="object-cover"
sizes="50vw"
unoptimized
/>
)}
</button>

View File

@@ -58,8 +58,8 @@ export default function NotificationRow({
src={avatarSrc}
alt={actor?.user_name || t("common.user")}
fill
sizes="48px"
className="object-cover"
unoptimized
/>
) : (
<div className="flex h-full w-full items-center justify-center">

View File

@@ -40,8 +40,8 @@ export default function VisitorRow({ visitor }: Props) {
src={avatarSrc}
alt={visitor.user_name || t("common.user")}
fill
sizes="48px"
className="object-cover"
unoptimized
/>
</Link>

View File

@@ -71,7 +71,6 @@ export default function ProductImageLightbox({
fill
className="object-contain"
sizes="100vw"
unoptimized
/>
</div>
))}

View File

@@ -125,8 +125,8 @@ export default function SendProductModal({
src={IMAGE_BASE_URL + preview}
alt=""
fill
sizes="48px"
className="object-cover"
unoptimized
/>
</div>
)}
@@ -169,8 +169,8 @@ export default function SendProductModal({
}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -114,8 +114,8 @@ export default function ShopProductCommentsModal({
src={IMAGE_BASE_URL + c.creator.profile_image}
alt=""
fill
sizes="36px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -69,7 +69,6 @@ export default function ShopProductListRow({ listing, shopId }: ShopProductListR
fill
className="object-cover"
sizes="80px"
unoptimized
/>
)}
{discountPercent != null && (

View File

@@ -252,7 +252,6 @@ export default function ShopReelsView({
fill
className="object-contain"
sizes="100vw"
unoptimized
/>
)}

View File

@@ -144,8 +144,8 @@ export default function SendStoryModal({
src={preview}
alt=""
fill
sizes="48px"
className="object-cover"
unoptimized
/>
)}
</div>
@@ -185,8 +185,8 @@ export default function SendStoryModal({
src={buildStorageUrl(u.profile_image)}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
) : null}
</div>

View File

@@ -30,7 +30,6 @@ function StoryAvatar({
width={STORY_AVATAR_SIZE}
height={STORY_AVATAR_SIZE}
className="block h-full w-full object-cover"
unoptimized
/>
);
}

View File

@@ -98,7 +98,7 @@ export default function StoryMentionPicker({
>
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-full bg-neutral-200">
{src ? (
<Image src={src} alt="" fill className="object-cover" unoptimized />
<Image src={src} alt="" fill sizes="40px" className="object-cover" />
) : null}
</div>
<div className="min-w-0 flex-1 text-right">

View File

@@ -18,6 +18,7 @@ import {
} from "@/api/fetchStories";
import { storyFont, STORY_FONT_WEIGHTS } from "@/lib/storyFonts";
import { buildPostLinkPath, PostLinkKind } from "@/lib/postLinkCaption";
import { prefetchVideoUrl } from "@/lib/videoPreload";
const LINK_KIND_ICONS: Record<PostLinkKind, string> = {
pkg: "teacher",
@@ -175,6 +176,25 @@ export default function StoryViewer({
setMediaAspect(16 / 9);
}, [currentStory?._id]);
// پیش‌بارگذاریِ استوریِ بعدی (همون کاربر یا اولین استوریِ کاربرِ بعدی) — عینِ
// prefetchPostVideos توی ریلز که ویدیوی نزدیک رو زودتر گرم می‌کنه؛ اینجا معادلش برای
// سواپِ بعدیِ استوری، چون قبلاً هیچ پیش‌بارگذاری‌ای برای استوری وجود نداشت
useEffect(() => {
const nextInSameUser = currentUser?.stories[storyIndex + 1];
const nextUser = feed[userIndex + 1];
const nextStory = nextInSameUser ?? nextUser?.stories[0];
if (!nextStory) return;
const nextSrc = nextStory.media_path.startsWith("http")
? nextStory.media_path
: buildStorageUrl(nextStory.media_path);
if (nextStory.media_type === "video") {
prefetchVideoUrl(nextSrc, "low");
} else if (typeof window !== "undefined") {
const img = new window.Image();
img.src = nextSrc;
}
}, [currentUser, storyIndex, userIndex, feed]);
// بوم واقعیِ نمایش‌داده‌شده: contain-fit نسبتِ واقعیِ عکس داخلِ فضای در دسترس — دقیقاً
// همون منطقِ StoryOverlayEditor.tsx (که این‌بار نسبت‌ها ازش میان) و mediaBox توی
// اندروید (StoryViewerModal.tsx)
@@ -447,8 +467,8 @@ export default function StoryViewer({
}
alt=""
fill
sizes="32px"
className="object-cover"
unoptimized
/>
</div>
</StoryRing>
@@ -514,9 +534,9 @@ export default function StoryViewer({
src={mediaSrc}
alt=""
fill
sizes="(max-width: 1024px) 100vw, 512px"
className="object-contain"
priority
unoptimized
onLoad={(e) => {
const img = e.currentTarget;
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
@@ -853,8 +873,8 @@ export default function StoryViewer({
}
alt=""
fill
sizes="40px"
className="object-cover"
unoptimized
/>
</div>
<div className="min-w-0 flex-1">