“👨🏻💻和代码有一个能🏃🏻♀️就行”,看似一句玩笑话但可能已经成为了事实。图片优化作为前端应该必须掌握的一项技能,但是你做三年开发也并不会真正的优化一次。
这几天在掘金看到了我将 2K stars 的 《丑丑头像》,用 next.js 重写了 这篇文章,在评论区有几个的人在讨论说遇到了滚动时卡顿的问题,其实整个页面仅展示 10 张随机生成的头像图片,这看起来不是个好的现象,正好可以尝试做一点优化看看效果怎么样。
因为不清楚测量哪些指标可以直指卡顿的原因,所以我还是先对页面进行一次分析:
通过网络请求这块可以看到,造成这次卡顿的主要原因可能有两个:
目前的页面加载的图片数量为 10,单从数量来看是很少的,所以我选择将图片数量提升到 1000 以上。在图片依次加载完毕后 DOM 中将有大量的不可释放的节点,再次造成卡顿。
解决这个问题的方案我选择虚拟列表,保证 DOM 中不会有大量不可释放的节点。
需要编写一个懒加载组件和一个瀑布流布局组件,以及在 Service 端对预览图片动态转换为渐进式 JPEG 格式。
实现图片懒加载组件的核心是应用 IntersectionObserver API,此提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法。
在组件实际编写中我选择直接 react-intersection-observer 代替原生 API,此模块提供了适用于 Reacrt 中用来监控组件状态的钩子 useInView
Hoook API,配置可见区域的比例为1/4,当 next/image
组件进去视图1/4后 inView
会切换为 true。
import { useState } from 'react';
import { useInView } from 'react-intersection-observer';
import Image from "next/image";
import { placeholder } from './placeholder';
export function LazyImage({ src = '' }) {
const [loaded, setLoaded] = useState(false);
const { ref, inView } = useInView({
triggerOnce: true,
threshold: 0.25,
});
return (
<div ref={ref} style={{ width, height }}>
{inView && <>
<Image src={src} />
</>}
</div>
);
}
MasonryLayout
组件由 MasonryLayout
容器和 CardCell
内容项两部分组成:
MasonryLayout 容器: 利用 ResizeObserver API 监听容器尺寸的变化,根据内容项预设的尺寸计算 columnCount
和 rowCount
两个属性,其中容器由 react-window 模块中的 VariableSizeGrid
提供,这个模块的主要特点就是用于高效渲染大量列表和表格数据。
const columnWidth = 342;
const rowHeight = 400;
const MasonryLayout: React.FC<MasonryLayoutProps> = ({ images }: MasonryLayoutProps) => {
const [containerWidth, setContainerWidth] = useState<number>(1200);
const [containerHeight, setContainerHeight] = useState<number>(800);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleResize = () => {
if (containerRef.current) {
setContainerWidth(containerRef.current.offsetWidth);
setContainerHeight(window.innerHeight - 154);
}
};
const resizeObserver = new ResizeObserver(handleResize);
const currentContainer = containerRef.current;
if (currentContainer) {
resizeObserver.observe(currentContainer);
}
handleResize();
return () => {
if (currentContainer) {
resizeObserver.unobserve(currentContainer);
}
};
}, []);
const getColumnCount = useCallback(() => {
return Math.floor(containerWidth / columnWidth);
}, [containerWidth]);
const getRowHeight = useCallback((index: number) => rowHeight, []);
const columnCount = getColumnCount();
const rowCount = Math.ceil(images.length / columnCount);
return (
<div ref={containerRef} style={{ width: '100%' }}>
<Grid
columnCount={columnCount}
columnWidth={() => columnWidth}
height={containerHeight}
rowCount={rowCount}
rowHeight={getRowHeight}
width={containerWidth}
itemData={{ images, columnCount, columnWidth }}
>
{CardCell}
</Grid>
</div>
);
};
CardCell 内容项: 这个 Card 组件就是源代码中主要的显示区域,直接当做 CardCell 会发现丢失了每行和没列之间的间距,通过网页审查元素可以看到使用 react-window 模块后,每个 Call 区域都是通过定位的方式实现排列,所以我通过判断 CardCell 的位置为每一个 CardCell 添加了合适的 left
和 top
属性,实现了每项之间的间隔。
const CardCell: React.FC<CellProps> = ({ columnIndex, rowIndex, style, data }) => {
const { images, columnCount } = data;
const imageIndex = rowIndex * columnCount + columnIndex;
const image = images[imageIndex];
if (!image) return null;
const rowInIndex = imageIndex % columnCount;
return (
<Card className="w-[342px] h-[400px]" style={{
...style,
boxSizing: 'border-box',
left: `${(columnWidth + gap) * rowInIndex}px`,
top: `${(rowHeight + gap) * rowIndex}px`
}} key={image.url}>
<CardContent className={'p-5'}>
<LazyImage
src={image.url}
alt={'index'}
width={300}
height={300}
/>
</CardContent>
<CardFooter className={'flex justify-around items-center'}>
<Button onClick={() => onDownload(image.url)} variant="outline"> 下载 </Button>
</CardFooter>
</Card>
);
};
渐进式JPEG(Progressive JPEG)一种渐进式 JPEG 压缩格式在呈现图像的方式上类似于 GIF(图形互换格式)。在网页浏览器中呈现时,图像会逐层下载,逐渐显现。直到完全呈现,图像逐渐变得清晰。
支持渐进式 JPEG 需要 Service 端支持,sharp 是用于在 Nodejs 中对图片高效加工的模块,仅通过一个选项就可以支持返回渐进式 JPEG 格式。
// 提供渐进式 JPEG 预览, 并降低质量
const jpegBuffer = await sharp(Buffer.from(result))
.jpeg({ progressive: true, quality: 75 })
.toBuffer();
return new Response(jpegBuffer, {
status: 200,
headers: {
'Content-Type': `image/jpeg`,
}
});
每当新的内容项 CardCell 进入视图1/4 时就会发起图片资源的请求,但是由于图片资源加载时间长,你将内容项继续向上滚动移出了视图,新的内容项继续进入视图,继续发起图片资源请求,这样就造成了无法及时加载当前视图中的图片,因为它排到的请求的队尾,我考虑了两种参考方案:
目前这个遗留问题在原项目中不存在,因为原项目要求仅展示 10 张图片。
通过上述优化措施,不仅解决了原有页面的卡顿问题,还提高了页面在大量图片展示情况下的性能。此外,这些技术方案也为其他类似项目提供了有价值的参考。对于前端开发者而言,了解并掌握这些优化技巧是非常重要的,特别是在现代Web应用中,高性能的图片展示已经成为用户良好体验的关键因素之一。