减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

图片懒加载是一个很重要的前端性能优化手段。这篇文章将从懒加载的最简单场景开始介绍,逐步增加复杂度,希望能讲清楚常见的图片懒加载场景及在该场景下对应的解决办法。

 

为什么要做图片的懒加载

假设在用户访问某个页面时就加载这个页面全部的图片(即使这些图片并不处在用户的当前的视窗中),在弱网环境或者网速较慢的环境下,这些“冗余”图片的下载会占用用户本来就非常有限的带宽,伤害用户体验(比如影响其他资源的下载)。所以对于网站的图片,理想的做法是懒加载(按需加载)。

 

图片懒加载的原理

在浏览器内部对于各种资源有着一套自己的优先级定义,浏览器会优先加载优先级高的资源

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

如果我们不去进行图片的懒加载,默认情况下,资源的priority如下。

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

这些优先级标记为high的图片会占用其他资源的下载带宽,可能会造成某些比较关键的资源(比如xhr call)加载缓慢,拖慢页面速度。

 

图片懒加载的简单实现

图片懒加载的思路一般时当页面加载时加载一个尺寸很小的占位图片(1kb以下),然后再通过js选择性的去加载真正的图片。

 

一个最简单的的实现如下

<!-- index.html -->
<img src="placeholder.jpg" data-src="real_image.jpt" />
// index.css

  img[data-src] {
      filter: blur(0.2em);
  }

  img {
      filter: blur(0em);
      transition: filter 0.5s;
  }
// index.js
(function lazyLoad(){
    const imageToLazy = document.querySelectorAll('img[data-src]');
    const loadImage = function (image) {
        image.setAttribute('src', image.getAttribute('data-src'));
        image.addEventListener('load', function() {
            image.removeAttribute("data-src");
        })
    }

    imageToLazy.forEach(function(image){
        loadImage(image);
    })
})()

通过懒加载之后,资源优先级如下。

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

 

图片懒加载的进阶实现–滚动加载

上面的方案并不完美,对于用户来说,不在视窗中的图片可能根本不是用户当前关心的图片,所以我们可以让这些图片出现在用户视窗中再进行加载。

运用Intersection Observer 我们可以做到当图片滚动到视窗后再加载该图片。

(function lazyLoad(){
    const imageToLazy = document.querySelectorAll('img[data-src]');
    const loadImage = function (image) {
        image.setAttribute('src', image.getAttribute('data-src'));
        image.addEventListener('load', function() {
            image.removeAttribute("data-src");
        })
    }


    const intersectionObserver = new IntersectionObserver(function(items, observer) {
        items.forEach(function(item) {
            if(item.isIntersecting) {
                loadImage(item.target);
                observer.unobserve(item.target);
            }
        });
    });

    imageToLazy.forEach(function(image){
        intersectionObserver.observe(image);
    })
})()

上面的这些demo都在https://github.com/hateonion/lazy-load 这个repo里面。

 

如何选择合适的Placeholder图片

在上面的demo中我们使用了placeholder图片,实际上,图片所占的位置是否确定对于我们选择placeholder图片有着很大的影响。

 

图片尺寸已知

图片尺寸已知出现的场景一般是博文的题图或者网站中一些固定尺寸的thumbnail,这些图的尺寸一般固定且一般不会发生改变。对于这种场景,我们可以加载对应尺寸的placeholder图片(如上一节的demo)。我们可以自己裁剪对应尺寸的的placeholder图片或者使用类似http://placeholder.com/ 这样的服务来获取placeholder图片。

 

图片尺寸未知

图片尺寸未知的情况下一般我们需要生成对应的thumbnail然后去加载我们生成的thumbnail去做placeholder。为了生成这些thumbnail你可以调用imagemagick或者调用一些在线的图片分割服务(比如七牛

 

懒加载防止布局抖动

在图片懒加载时,由于图片的尺寸不定,浏览器难以计算需要给图片预留出的位置。所以当图片加载完成后会出现网页布局的抖动。

(image from From http://davidecalignano.it/lazy-loading-with-responsive-images-and-unknown-height/)

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

 

即使我们选择的placeholder很小,可以在毫秒级别完成下载,用户可能意识不到布局的抖动。但是在一些性能比较差的设备上,这种布局的抖动还是会一定程度上影响用户的体验。为了完全避免布局闪动,我们可以采用aspect ratio boxes 的技术来制作一个占位用的元素。

<div class="lazy-load__container feature">
  <img src="placeholder.jpg" data-src="real.jpg" />
</div>
.lazy-load__container{
    position: relative;
    display: block;
    height: 0;
}

.lazy-load__container.feature {
    // feature image 的高宽比设置成42.8%
    // 对于其他图片 比如 post图片,高宽比可能会不同,可以使用其他css class去设置
    padding-bottom: 42.8%;
}

.lazy-load__container img {
    position: absolute;
    top:0;
    left:0;
    height: 100%;
    width: 100%;
}

 

结果

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

 

上面这个实现的原理其实很简单,由于 padding-bottom (或者 padding-top)声明为百分比时是根据元素生成的box的 width 去计算百分比的,所以我们通过padding-bottom去声明一个对应高宽比的container。而这个container的具体尺寸会由尺寸确定的外层元素确定,但是高宽比始终保持一致。

而图片的尺寸设置成100%container的尺寸保证图片始终和container的尺寸保持一致。

需要注意的是上面这个方法并不能适配图片比例不一致的网站(比如本站),不过好在,为了用户体验,现在绝大多数网站的图片比例都有明确的要求,绝大多数情况下我们只适配保证网站常用的的几种图片宽高比例即可。

 

像Medium一样懒加载图片

Medium的懒加载图片的体验相信去 Medium 读过文章的同学都体验过了,可以说是非常的流畅。而其背后的技术其实也就是我们上面讲到的几种技术的组合。

  1. 使用 aspect ratio box 创建占位元素。
  2. 在html解析时只加载一个小尺寸的图片,并且添加blur效果。
  3. 最后使用js选择性的加载真实图片。

Demo 如下 codePen by José M. Pérez

 

总结

  1. 懒加载用户当前视窗中的图片可以提升页面的加载性能。
  2. 懒加载的思路是在html解析时先加载一个placeholder图片,最后再用js选择性的加载真实图片。
  3. 如果需要滚动加载可以使用 Intersection Observer 。
  4. 对于固定尺寸和不定尺寸的图片,我们可以选择不同的服务去或者placeholder图片。
  5. 对于图片尺寸不确定引起的布局抖动问题我们可以使用 aspect ratio box 来解决。

 

其实说到懒加载,这项技术已经不局限于单一的图片懒加载。除了图片外,懒加载也早就应用到了视频、音频等其它媒体资源的优化上,同时随着组件化思维的不断深化,本着懒加载的核心观点 —— 用户暂时用不到的资源又能延迟去加载的,就大可做成懒加载,WebComponent 也已经纳入到懒加载优化当中,现代前端各种构建工具、类库,都有做相关的优化,进行组件资源的按需加载。本文主要把焦点聚集到“图片”的懒加载处理上,做一番探讨。


 

原理

图片懒加载的原理其实非常简单:使用手段让 <img> 元素默认只载入非常小的图片内容,再利用 JavaScript 检查元素是否在视口中。 如果元素在视口中,则往其 src(有时是srcset)属性中填充真正所需图像内容的地址,加载真正的图片,实现延迟加载。

最早的也是最为有名与流行的实现方式看起来大致如下:

<img src="blank.gif" data-src="normal.jpg" alt="IMG" />

或者使用 base64 内联:

<img src="data:image/gif;base64,R0lGODdhAQABAPAAAMPDwwAAACwAAAAAAQABAAACAkQBADs=" data-src="normal.jpg" />

更懒的方式甚至会像下面这样省掉占位用的小图,当然这种方式是不被推崇的,留空的 src 属性会带来多余的、有害的网络请求。你可以完全不写 src 属性,这样虽然不符合规范,但是也能工作,但不要留空 src

<img src="" data-src="normal.jpg" alt="IMG" />

简单直接是这种方式能成为主流的原因。一张宽高仅有 1px 的透明不可见的 blank.gif,作为默认载入的占位图最合适不过,只有非常小的体积,网页初始化后迅速载入,而真正所需图像的地址被放到了约定的 data-src 属性里。当 JavaScript 代码检测到(通过页面载入/ 滚动)图片进入视口时,便将 data-src 中的值赋给 src 属性,替换掉 blank.gif,触发原始所需图片的加载。但这种简单有力的方式,有些弊端。

问题

首先,其中一个非常直观可见的问题是「布局抖动」。在图片懒加载时,由于默认载入的 blank.gif 图片几乎没有大小,真正的图片还没有经过浏览器载入,浏览器就无法确定需要给原始图片预留多大的位置。当真正的图片加载完成后,网页就会出现肉眼可见的布局抖动问题。虽然该问题实际对内容本身没有影响,但影响的是用户体验。

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

解决办法是给图片元素设置已知的宽高(元素属性或者 CSS 样式都行),这样浏览器就可以根据宽高预留位置。

<img src="blank.gif" data-src="normal.jpg" style="width:800px;height:600px;" />

当然固定宽高大小对响应式布局来说不友好,于是就出现了更好的「长宽比盒子」技巧,根据图片宽高计算出图片的长宽比,利用长宽比与 Padding 边距设定占位元素。

<div class="aspect-ratio-box">
  <img src="blank.gif" data-src="normal.jpg" />
</div>
.aspect-ratio-box {
  position: relative;
  /* 根据宽高比计算设置 box 的 padding-bottom */
  padding-bottom: 62.5%;
}

.aspect-ratio-box img {
  position: absolute;
  height: 100%;
  width: 100%;
}

利用长宽比盒子,就能在响应式布局页面中,较好地处理图片懒加载布局抖动的问题。其实更多的长宽比盒子实际用例是用在响应式 video 元素上,关于长宽比盒子技术,可以进一步阅读 CSS-TRICKS 的文章 —— Aspect Ratio Boxes。布局抖动解决后样子如下:

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

第二个问题是,这个图片懒加载的最原始实现,没有考虑到响应式图片Responsive Images)技术的到来。

<img srcset="small.jpg 100w, middle.jpg 200w, large.jpg 300w" data-src="normal.jpg" />

不过好在如今的各种 LazyLoad 类库,都已经针对这个问题做了优化,对响应式图片做了兼容。只需要将原来单一图片的 data-src 换成多张图的 data-srcset 即可。

<img src="blank.gif" data-srcset="small.jpg 100w, middle.jpg 200w, large.jpg 300w" />

在 data-srcset 列出可供懒加载使用的响应式图片,类库就会自动根据终端判断加载哪一张。

第三个问题是一个比较严重的问题,问题的根源在于懒加载中 src 属性的 blank.gif 这张占位图本身。为了压缩体积,降低初始载入时间,选用了一张非常小的图。默认载入的这张图虽然在用户浏览站点自身网页时,没什么区别,因为有懒加载脚本加持。但对于站外来说,这张小图带来的影响却是致命的,因为站外访问内容时,没有图片懒加载。

这就产生了严重的弊端,blank.gif 懒加载的方式,对站外 SEO、社会化分享以及 RSS 等稍后阅读工具非常不利。除了站点自身,几乎所有站外的工具服务,都不会刻意在引用你的内容时,帮你处理懒加载。于是,Google 爬虫爬取你的网页内容时,只会爬到带有 blank.gif 的“空白” <img> 标签,Google 帮你缓存的网页快照,都会是“缺图”的;同时,通过社会化分享功能将内容分享到如 Twitter、Facebook 等站外时,你的内容也会是“无图”的;更惨的是,通过 RSS、Pocket 等稍后阅读工具订阅了你站点内容的读者,他们在阅读工具里无法看到你文章中所有懒加载的图片,阅读工具里有的,只是空空的 blank.gif。

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

于是,问题来了之后就触发了又一次的迭代。尝试修正如下:

<img src="small.jpg" data-src="normal.jpg" />

最直接的更改,选取一张由原图经过压缩后的 small.jpg 作为懒加载占位图,也就是我们常说的 thumbnail。比如原图的分辨率是 2000×1000,体积是 2MB,经过压缩后的小图是 200×100,体积是 20KB。这样,初始加载这张缩略图仍然会比加载原图快速,同时站外 SEO、RSS 阅读器等也能读取到缩略图勉强解决看不到图的问题。这里我们暂且不谈用压缩的小图替换 blank.gif 时带来的额外工作量问题 —— 需要对每一张原图生成缩略图。该方案仍然不是一个完美的方案。


 

改进

先抛出一个自我打击:如果一直从“先人”创造的最早的图片懒加载方法入手,打死我也不会想到下面这个改进的方案。这里我先直接亮出改进方案的实例:

<div class="aspect-ratio-box" style="padding-bottom:56.25%;">
  <img
    src="original.jpg"
    srcset="thumbnail.jpg"
    data-srcset="small.jpg 100w, middle.jpg 200w, large.jpg 300w"
  />
</div>

这个改进的图片懒加载方案有包括但不限于以下一些特性:

  • 支持响应式图片(Responsive Images)、支持 webp 格式
  • 对响应式布局友好、支持移动终端屏幕旋转时进行图片大小重绘
  • 使用了 srcset 的特性,在主流的现代浏览器上有完整的支持,同时覆盖率高[3]
    • 在无法支持 srcset 或者懒加载类库不能生效的旧浏览器上可完美回退(Fallback)
  • 对站外 SEO、社会化分享、RSS 稍后阅读等工具服务友好
  • 占位用的缩略图可配置性极强,支持多种 placeholder 展示形式

 

以下来说说这个改进方案的魔法之处。还记得最早的图片懒加载方法吗?没错,就是为了避免初始加载原图,刻意把 src 属性的图替换成了空占位图/缩略小图,把原图放到约定的 data-src 属性的旧方法:

<img src="blank.gif" data-src="normal.jpg" />

既然直接在 src 属性上动刀会产生那么多问题,那么有没有什么办法能在不改动 src 属性的基础上,通过其它办法避免浏览器初始加载 src 属性中的原图,实现懒加载呢?有!下面说一下改进方案的魔术原理。

  1. 首先,带有 .aspect-ratio-box 类选择器的外层 <div> 标签,是用来保证懒加载图片在未载入原图时的占位大小的。这个其实是前文说的「长宽比盒子」技巧,解决布局抖动问题,这里没什么特别点。

  2. 改进方案中 <img> 标签中的 src 属性携带的仍然是原始大小的图片 original.jpg,确保了站外 SEO、社会化分享、RSS 等不会读不到原图。src 中就是未改动的原图,该读取原图的还是原图。

  3. 改进方案中 <img> 标签,利用 srcset 属性存放了一张缩略图 thumbnail.jpg。正是这张缩略图,有着与 blank.gif 异曲同工的效果 —— 能够阻止 <img> 初始化时 src 原图被加载。这便是改进方案的关键点

  4. 最后也是不可或缺的一步,为保证懒加载能正常完成整个逻辑,同时保证对响应式图片的支持,改进方案中 <img> 标签将原本应该在 srcset 中的响应式图片列表,存放在了约定的 data-srcset 中。当图片进入视口时,懒加载类库就会用 data-srcset 中的响应式图片地址列表,替换掉实际在 srcset 中的缩略图。最后浏览器再根据终端计算判断,选择加载 src 属性与 srcset 属性中的图片。

减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动
srcset 兼容性(2019 年 7 月)

正是通过这么一连串与早期方案截然不同的流程,完美地避开了动刀 src 属性产生各种问题的同时,保证了懒加载功能的正常进行。这一切,全得益于 srcset 这个神奇的 HTML5 属性,以及其与 src 属性以及浏览器三者之间的完好协作。

 
 

后记

这么一个突破固有思维的改进方案,当然不是我一下拍脑子能想得出来的,毕竟我还是太菜了,有些东西得继续学习。能了解到这个技巧,全赖「业务是驱动技术发展的主要力量」—— 自从博客更换了系统后,饱受原始图片懒加载方式带来的,布局抖动与 RSS 阅读器不友好的困扰。一直在寻找更好的方案处理博客文章图片的懒加载,直到最近才从谷歌中搜索到 Ivo Petkov 写的懒加载类库 —— responsively-lazy,并进一步阅读了其相关文章,于是才了解到这么个创新想法。

在目前能找得到的绝大多数 LazyLoad JS 类库中,基本上都是改 src 属性的方案,也就存在改 src 产生的问题。Ivo Petkov 的方案真让我耳目一新,只可惜他写的 responsively-lazy 类库已经很久没有维护了,甚至还有使用老式的 getBoundingClientRect() 办法检测元素的出现,而不是像 lozad 那样使用精简有力的 IntersectionObserver API。但这并不妨碍新方案本身,创新想法上的发光点。

如果你还在犹豫是否使用响应式图片、图片懒加载的话,读到这里,就不要犹豫了。快试试吧,还能同时用上呢。

 

也可以参考:避免大型、复杂的布局和布局抖动

 

本文:减小图片懒加载布局抖动, 图片懒加载从简单到复杂, 防止网页加载布局抖动

Leave a Reply