来自 react-ideal-image 的设计
图片组件可以说是任何类型的站点都需要实现的。尤其是对于性能以及用户体验有要求的开发者,这样的被频繁使用的组件是需要精心设计的。
就拿我这个 Jekyll 博客来说,在构建时会为每一张图片生成多张不同分辨率的版本,根据用户设备宽度展现对应的版本,避免带宽浪费。 但是看过 react-ideal-image 的介绍文章,会觉得自己目前的做法还是显得弱了太多。
延迟加载
首先最容易想到的一点就是延迟加载不在首屏视口中的图片了。之前写过一篇介绍 IntersectionObserver
的文章。
// 创建观测对象
let observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// 图片和视口有交集了
if (entry.intersectionRatio > 0) {
// 停止继续观测,直接加载
observer.unobserve(entry.target);
loadImage(entry.target);
}
});
}, {threshold: 0.01});
// 观测所有图片
images.forEach(image => {
observer.observe(image);
});
在 React 中也有 Waypoint 这样的组件。
<Waypoint onEnter={() => this.setState({src})}>
<img src={this.state.src} />
</Waypoint>
占位元素
但是仅有延迟加载还不够,如果事先没有占位元素,当图片加载完成时,会出现跳动情况,用户之前的滚动位置也会丢失。正是处于这样的考虑,AMP 里使用图片组件都是需要指定尺寸的。
简单的占位元素使用灰色背景图,实现起来也简单。但是经常上 Medium 会发现,里面会使用 Low-Quality Image Placeholder 快速呈现一张模糊的图片,然后再慢慢加载原图。
为了生成这样的效果,需要在构建时。这里用到了之前在Babel 插件开发一文中介绍过的 babel-plugin-macros,直接写到源码里在编译时运行,但是不用担心会出现在最终打包结果中。
const getLqip = file =>
new Promise((resolve, reject) => {
sharp(file)
.resize(20)
.toBuffer((err, data, info) => {
if (err) return reject(err)
const {format} = info
return resolve(`data:image/${format};base64,${data.toString('base64')}`)
})
})
const lqip = await getLqip('cute-dog.jpg')
值得一提的是,这里处理图片用到了 sharp 修改原图的尺寸,之后转成 base64 格式内联到代码中。后面还会用到这个工具。
响应式
和最开始提到的做法一样,构建时生成多个不同分辨率的版本。
<IdealImage
width={100}
height={100}
{...props}
srcSet={[
{width: 100, src: 'cute-dog-100.jpg'},
{width: 200, src: 'cute-dog-200.jpg'},
]}
/>
适应网络环境
文章中最后这方面的设计是我觉得最棒的,也是之前很少考虑过的。如果网络环境不佳,是不是应该加载更小尺寸的版本甚至停止自动下载图片呢?
和之前提供不同分辨率版本一样,我们可以提供一个 WebP 版本。当然需要检测兼容性:
const detectWebpSupport = () => {
if (ssr) return false
const elem = document.createElement('canvas')
if (elem.getContext && elem.getContext('2d')) {
// was able or not to get WebP representation
return elem.toDataURL('image/webp').indexOf('data:image/webp') === 0
} else {
// very old browser like IE 8, canvas not supported
return false
}
}
我们需要检测当前的网络环境,使用 navigator.connection.effectiveType 可以做到。但是支持度比较有限,目前只有 Chrome 实现。附上标准文档。
当发现网络情况不佳时,可以效仿很多需要点击才播放视频的做法,提供点击下载功能,同时展现图片大小,让用户心中有数。
取消下载
如果在这样的做法下,超过一定时间还没有下载完成,需要取消当前下载,让用户稍后重试。 除了 Safari,在其他浏览器中,只需要置空图片地址就行了:
const img = new Image()
//...
img.src = ''
效果对比
参考资料
内容绝大部分来自 react-ideal-image