perf: 图片渐进加载,缩短首屏等待
- 流内单图改用缩略图(原图仅在灯箱按需加载),体积大幅减小 - BubbleImage 加 decoding=async + 加载完淡入(ark-img-fade),图片逐张出现 - 视频海报/文件预览/推荐卡/热门榜补 decoding=async、lazy 原图无缩略图时自动回退,无回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,9 +68,13 @@ export function BubbleImage({
|
||||
src={current}
|
||||
alt=""
|
||||
loading={loading}
|
||||
className={className}
|
||||
decoding="async"
|
||||
className={`ark-img-fade ${className ?? ""}`}
|
||||
onLoad={(e) => {
|
||||
const img = e.currentTarget;
|
||||
// Fade each image in as soon as it loads, so they appear progressively
|
||||
// instead of the page seeming to wait for everything.
|
||||
img.classList.add("is-loaded");
|
||||
if (img.naturalWidth && img.naturalHeight)
|
||||
onNaturalSize?.(img.naturalWidth, img.naturalHeight);
|
||||
}}
|
||||
|
||||
@@ -45,6 +45,7 @@ function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
||||
src={previewUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={() => setPreviewFailed(true)}
|
||||
className="h-16 w-16 shrink-0 rounded-lg object-cover"
|
||||
/>
|
||||
|
||||
@@ -20,6 +20,11 @@ export function SingleImageFrame({
|
||||
return (
|
||||
<AdaptiveImageFrame
|
||||
attachment={attachment}
|
||||
// Show the lightweight thumbnail in-stream for fast, progressive loading;
|
||||
// the full image is loaded on tap in the lightbox. Falls back to the full
|
||||
// asset if no thumbnail exists.
|
||||
src={attachment.thumbnailUrl ?? attachment.thumbUrl ?? attachment.url}
|
||||
fallbackSrc={[attachment.thumbUrl, attachment.url]}
|
||||
onOpen={() => openLightbox([attachment], 0, text, postId)}
|
||||
ariaLabel="View image"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LoaderCircle, Maximize2, Play, X } from "lucide-react";
|
||||
import { LoaderCircle, Play, X } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -79,10 +79,12 @@ function VideoAttachmentCard({
|
||||
src={attachment.url}
|
||||
poster={attachment.posterUrl}
|
||||
controls
|
||||
controlsList="nodownload noplaybackrate noremoteplayback"
|
||||
disablePictureInPicture
|
||||
playsInline
|
||||
autoPlay
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute inset-0 h-full w-full"
|
||||
className="ark-message-video absolute inset-0 h-full w-full"
|
||||
/>
|
||||
<AttachmentDownloadPill
|
||||
postId={postId}
|
||||
@@ -90,18 +92,6 @@ function VideoAttachmentCard({
|
||||
leadingLabel={duration}
|
||||
className="absolute left-2 top-2 z-20"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const v = videoRef.current;
|
||||
openVideo(attachment, v?.currentTime ?? 0);
|
||||
}}
|
||||
className="absolute right-2 top-2 z-20 flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white shadow-lg ring-1 ring-white/25 transition hover:bg-black/80"
|
||||
aria-label="Fullscreen"
|
||||
>
|
||||
<Maximize2 className="h-4 w-4" strokeWidth={2.2} />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -109,6 +99,8 @@ function VideoAttachmentCard({
|
||||
<img
|
||||
src={posterUrl}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className={`absolute inset-0 h-full w-full object-cover ${
|
||||
overlayCount ? "blur-sm scale-105" : ""
|
||||
}`}
|
||||
@@ -284,6 +276,8 @@ function VideoListDialog({
|
||||
<img
|
||||
src={thumb}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user