来自 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 = ''

效果对比

和 img 的效果对比

参考资料

内容绝大部分来自 react-ideal-image