整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

前端开发,原生 JS 实现最简单的图片懒加载

前端开发,原生 JS 实现最简单的图片懒加载


加载


什么是懒加载

懒加载其实就是延迟加载,是一种对网页性能优化可方式,比如当访问一个页面的时候,优先显示可视区域的图片而不一次性加载所有图片,当需要显示的时候再发送图片请求,避免打开网页时加载过多资源。

什么时候用懒加载

当页面中需要一次性载入很多图片的时候,往往都是需要用懒加载的。

懒加载原理

我们都知道HTML中的 <img>标签是代表文档中的一个图像。。说了个废话。。

<img>标签有一个属性是 src,用来表示图像的URL,当这个属性的值不为空时,浏览器就会根据这个值发送请求。如果没有 src属性,就不会发送请求。

嗯?貌似这点可以利用一下?

我先不设置 src,需要的时候再设置?

nice,就是这样。

我们先不给 <img>设置 src,把图片真正的URL放在另一个属性 data-src中,在需要的时候也就是图片进入可视区域的之前,将URL取出放到 src中。

实现


HTML结构

<div class="container">
 <div class="img-area">
   <img class="my-photo" alt="loading" data-src="./img/img1.png">
 </div>
 <div class="img-area">
   <img class="my-photo" alt="loading" data-src="./img/img2.png">
 </div>
 <div class="img-area">
   <img class="my-photo" alt="loading" data-src="./img/img3.png">
 </div>
 <div class="img-area">
   <img class="my-photo" alt="loading" data-src="./img/img4.png">
 </div>
 <div class="img-area">
   <img class="my-photo" alt="loading" data-src="./img/img5.png">
 </div>
</div>

仔细观察一下, <img>标签此时是没有 src属性的,只有 alt和 data-src属性。

alt 属性是一个必需的属性,它规定在图像无法显示时的替代文本。 data-* 全局属性:构成一类名称为自定义数据属性的属性,可以通过 HTMLElement.dataset来访问。

如何判断元素是否在可视区域

方法一

网上看到好多这种方法,稍微记录一下。

  1. 通过 document.documentElement.clientHeight获取屏幕可视窗口高度
  2. 通过 document.documentElement.scrollTop获取浏览器窗口顶部与文档顶部之间的距离,也就是滚动条滚动的距离
  3. 通过 element.offsetTop获取元素相对于文档顶部的距离

然后判断②-③<①是否成立,如果成立,元素就在可视区域内。

方法二(推荐)

通过 getBoundingClientRect()方法来获取元素的大小以及位置,MDN上是这样描述的:

The Element.getBoundingClientRect() method returns the size of an element and its position relative to the viewport.

这个方法返回一个名为 ClientRect的 DOMRect对象,包含了 top、 right、 botton、 left、 width、 height这些值。

MDN上有这样一张图:

可以看出返回的元素位置是相对于左上角而言的,而不是边距。

我们思考一下,什么情况下图片进入可视区域。

假设 constbound=el.getBoundingClientRect();来表示图片到可视区域顶部距离; 并设 constclientHeight=window.innerHeight;来表示可视区域的高度。

随着滚动条的向下滚动, bound.top会越来越小,也就是图片到可视区域顶部的距离越来越小,当 bound.top===clientHeight时,图片的上沿应该是位于可视区域下沿的位置的临界点,再滚动一点点,图片就会进入可视区域。

也就是说,在 bound.top<=clientHeight时,图片是在可视区域内的。

我们这样判断:

function isInSight(el) {
 const bound=el.getBoundingClientRect();
 const clientHeight=window.innerHeight;
 //如果只考虑向下滚动加载
 //const clientWidth=window.innerWeight;
 return bound.top <=clientHeight + 100;
}

这里有个+100是为了提前加载。

加载图片

页面打开时需要对所有图片进行检查,是否在可视区域内,如果是就加载。

function checkImgs() {
 const imgs=document.querySelectorAll('.my-photo');
 Array.from(imgs).forEach(el=> {
   if (isInSight(el)) {
     loadImg(el);
   }
 })
}

function loadImg(el) {
 if (!el.src) {
   const source=el.dataset.src;
   el.src=source;
 }
}

这里应该是有一个优化的地方,设一个标识符标识已经加载图片的index,当滚动条滚动时就不需要遍历所有的图片,只需要遍历未加载的图片即可。

函数节流

在类似于滚动条滚动等频繁的DOM操作时,总会提到“函数节流、函数去抖”。

所谓的函数节流,也就是让一个函数不要执行的太频繁,减少一些过快的调用来节流。

基本步骤:

  1. 获取第一次触发事件的时间戳
  2. 获取第二次触发事件的时间戳
  3. 时间差如果大于某个阈值就执行事件,然后重置第一个时间
function throttle(fn, mustRun=500) {
 const timer=null;
 let previous=null;
 return function() {
   const now=new Date();
   const context=this;
   const args=arguments;
   if (!previous){
     previous=now;
   }
   const remaining=now - previous;
   if (mustRun && remaining >=mustRun) {
     fn.apply(context, args);
     previous=now;
   }
 }
}

这里的 mustRun就是调用函数的时间间隔,无论多么频繁的调用 fn,只有 remaining>=mustRun时 fn才能被执行。

实验


页面打开时

可以看出此时仅仅是加载了img1和img2,其它的img都没发送请求,看看此时的浏览器

第一张图片是完整的呈现了,第二张图片刚进入可视区域,后面的就看不到了~

页面滚动时

当我向下滚动,此时浏览器是这样

此时第二张图片完全显示了,而第三张图片显示了一点点,这时候我们看看请求情况

img3的请求发出来,而后面的请求还是没发出~

全部载入时

当滚动条滚到最底下时,全部请求都应该是发出的,如图

更新


方法三 IntersectionObserver

经大佬提醒,发现了这个方法

先附上链接:

jjc大大:

https://github.com/justjavac/the-front-end-knowledge-you-may-dont-know/issues/10

阮一峰大大:

http://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html

API Sketch for Intersection Observers:

https://github.com/WICG/IntersectionObserver

IntersectionObserver可以自动观察元素是否在视口内。

var io=new IntersectionObserver(callback, option);
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();

callback的参数是一个数组,每个数组都是一个 IntersectionObserverEntry对象,包括以下属性:

属性描述time可见性发生变化的时间,单位为毫秒rootBounds与getBoundingClientRect()方法的返回值一样boundingClientRect目标元素的矩形区域的信息intersectionRect目标元素与视口(或根元素)的交叉区域的信息intersectionRatio目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0target被观察的目标元素,是一个 DOM 节点对象

我们需要用到 intersectionRatio来判断是否在可视区域内,当 intersectionRatio>0&&intersectionRatio<=1即在可视区域内。

代码

function checkImgs() {
 const imgs=Array.from(document.querySelectorAll(".my-photo"));
 imgs.forEach(item=> io.observe(item));
}

function loadImg(el) {
 if (!el.src) {
   const source=el.dataset.src;
   el.src=source;
 }
}

const io=new IntersectionObserver(ioes=> {
 ioes.forEach(ioe=> {
   const el=ioe.target;
   const intersectionRatio=ioe.intersectionRatio;
   if (intersectionRatio > 0 && intersectionRatio <=1) {
     loadImg(el);
   }
   el.onload=el.onerror=()=> io.unobserve(el);
 });
});

源自:segmentfault

声明:文章著作权归作者所有,如有侵权,请联系小编删除。

于一些拥有大量图片的网站来说,如果一个页面有超过 50 张图片,就会造成网站页面加载太慢以及移动端耗费流量太大。就像前几天给理想做的作品页面,页面上至少200张图,为了解决这样的问题,可以使用LazyLoad按需加载,又称懒加载。

LazyLoad按需加载

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

什么是LazyLoad按需加载

LazyLoad按需加载采用图片按需加载技术,打开页面时只会加载首屏图片。访客往下滚动时才会陆续加载需要展现的图片,这样非常高效,体验舒适。

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

LazyLoad按需加载实现方法

我们在自己做网站时,也可以实现LazyLoad按需加载,增强网站的用户体验。下面学做网站论坛就来介绍一下LazyLoad按需加载实现方法。(以下会使用到HTML代码,如果对代码不熟悉,可以学习一下html视频教程)

引入LazyLoad按需加载必须的二个文件:jquery.js和jquery.lazyload.js。引入方法很简单,只需将下面的代码放到</head>标签上方即可;

<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script><script src="https://raw.githubusercontent.com/tuupola/jquery_lazyload/master/jquery.lazyload.min.js"></script>

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

网站上所有图片都使用以下的格式书写:

<img class="lazy" src="" data-original="图片地址" width="100" height="100" alt="">

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

在网站的</body>标签上面,放上以下的JS代码,来实现LazyLoad按需加载(懒加载);

<script type="text/javascript">$(function() { $("img.lazy").lazyload({ threshold : 200, // 设置阀值 effect : "fadeIn" // 设置图片渐入特效 });});</script>

做个笔记,图片如何实现懒加载(LazyLoad按需加载)

这样,我们自己在建网站时,也可以轻松实现LazyLoad按需加载(懒加载)了。快去自己的网站上试试吧!

网站典型负载中,图像和视频是非常重要的一部分内容。 不过遗憾的是,项目干系人并不愿意从其现有应用中删除任何媒体资源。 这种僵局不免令人沮丧,尤其是当所有相关方都希望改善网站性能,但却无法就具体方法达成一致时。 幸运的是,延迟加载解决方案可以减少初始页面负载_和_加载时间,但不会删减任何内容。

什么是延迟加载?

延迟加载是一种在加载页面时,延迟加载非关键资源的方法, 而这些非关键资源则在需要时才进行加载。 就图像而言,“非关键”通常是指“屏幕外”。 如果您曾使用过 Lighthouse 并检验过其提供的改进机会,就有可能从 屏幕外图像审核中看到一些这方面的指导:

您可能已经见过延迟加载的实际应用,其过程大致如下:

  • 您访问一个页面,并开始滚动阅读内容。
  • 在某个时刻,您将占位符图像滚动到视口中。
  • 该占位符图像瞬间替换为最终图像。

您可以在热门发布平台 Medium 上找到图像延迟加载的示例。该平台在加载页面时会先加载轻量级的占位符图像,并在其滚动到视口时,将之替换为延迟加载的图像。

图像延迟加载实际应用示例。 占位符图像在页面加载时加载(左侧),当滚动到视口时,最终图像随即加载(即在需要时加载)。

为何要延迟加载图像或视频,而不是直接_加载_?

因为直接加载可能会加载用户永远不会查看的内容, 进而导致一些问题:

  • 浪费数据流量。 如果使用无限流量网络,这可能还不是最坏的情况(不过,这些宝贵的带宽原本可以用来下载用户确实会查看的其他资源)。 但如果流量有限,加载用户永远不会查看的内容实际上是在浪费用户的金钱。
  • 浪费处理时间、电池电量和其他系统资源。 下载媒体资源后,浏览器必须将其解码,并在视口中渲染其内容。
  • 延迟加载图像和视频时,可以减少初始页面加载时间、初始页面负载以及系统资源使用量,所有这一切都会对性能产生积极影响。 在本指南中,我们将针对延迟加载图像和视频提供一些技巧及指导,并列举几个常用的库。

延迟加载图像

内联图像

<img> 元素中使用的图像是最常见的延迟加载对象。 延迟加载 <img> 元素时,我们使用 JavaScript 来检查其是否在视口中。 如果元素在视口中,则其 src(有时是 srcset)属性中就会填充所需图像内容的网址。

使用 Intersection Observer

如果您曾经编写过延迟加载代码,您可能是使用 scroll 或 resize 等事件处理程序来完成任务。 虽然这种方法在各浏览器之间的兼容性最好,但是现代浏览器支持通过 Intersection Observer API 来检查元素的可见性,这种方式的性能和效率更好。

注:并非所有浏览器都支持 Intersection Observer。 如果浏览器之间的兼容性至关重要,请务必阅读下一节,其中会说明如何使用性能稍差(但兼容性更好!)的 scroll 和 resize 事件处理程序来延迟加载图像。

与依赖于各种事件处理程序的代码相比,Intersection Observer 更容易使用和阅读。这是因为开发者只需要注册一个 Observer 即可监视元素,而无需编写冗长的元素可见性检测代码。 然后,开发者只需要决定元素可见时需要做什么即可。 假设我们的延迟加载

元素采用以下基本标记模式:

<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load-1x.jpg" data-srcset="image-to-lazy-load-2x.jpg 2x, image-to-lazy-load-1x.jpg 1x" alt="I'm an image!">

在此标记中,我们应关注三个相关部分:

  • class 属性,这是我们在 JavaScript 中选择元素时要使用的类选择器。
  • src 属性,引用页面最初加载时显示的占位符图像。
  • data-src 和 data-srcset 属性,属于占位符属性,其中包含元素进入视口后要加载的图像的网址。

现在,我们来看看如何在 JavaScript 中使用 Intersection Observer,并通过以下标记模式延迟加载图像:

document.addEventListener("DOMContentLoaded", function() {
 var lazyImages=[].slice.call(document.querySelectorAll("img.lazy"));
 if ("IntersectionObserver" in window) {
 let lazyImageObserver=new IntersectionObserver(function(entries, observer) {
 entries.forEach(function(entry) {
 if (entry.isIntersecting) {
 let lazyImage=entry.target;
 lazyImage.src=lazyImage.dataset.src;
 lazyImage.srcset=lazyImage.dataset.srcset;
 lazyImage.classList.remove("lazy");
 lazyImageObserver.unobserve(lazyImage);
 }
 });
 });
 lazyImages.forEach(function(lazyImage) {
 lazyImageObserver.observe(lazyImage);
 });
 } else {
 // Possibly fall back to a more compatible method here
 }
});

在文档的 DOMContentLoaded 事件中,此脚本会查询 DOM,以获取类属性为 lazy 的所有 <img> 元素。 如果 Intersection Observer 可用,我们会创建一个新的 Observer,以在 img.lazy 元素进入视口时运行回调。 请参阅此 CodePen 示例,查看代码的实际运行情况.

注:此代码使用名为 isIntersecting 的 Intersection Observer 方法,该方法在 Edge 15 的 Intersection Observer 实现中不可用。 因此,以上延迟加载代码(以及其他类似的代码片段)将会失败。 请查阅此 GitHub 问题,以获取有关更完整的功能检测条件的指导。

不过,虽然Intersection Observer在浏览器之间获得良好的支持,但并非所有浏览器皆提供支持。 对于不支持 Intersection Observer 的浏览器,您可以使用 polyfill,或者如以上代码所述,检测 Intersection Observer 是否可用,并在其不可用时回退到兼容性更好的旧方法。

使用事件处理程序(兼容性最好的方法)

虽然您_应该_使用 Intersection Observer 来执行延迟加载,但您的应用可能对浏览器的兼容性要求比较严格。 您_可以_使用 polyfil 为不支持 Intersection Observer 的浏览器提供支持(这种方法最简单),但也可以回退到使用 scroll 和 resize的代码,甚至回退到与 getBoundingClientRect 配合使用的 orientationchange 事件处理程序,以确定元素是否在视口中。

document.addEventListener("DOMContentLoaded", function() {
 let lazyImages=[].slice.call(document.querySelectorAll("img.lazy"));
 let active=false;
 const lazyLoad=function() {
 if (active===false) {
 active=true;
 setTimeout(function() {
 lazyImages.forEach(function(lazyImage) {
 if ((lazyImage.getBoundingClientRect().top <=window.innerHeight && lazyImage.getBoundingClientRect().bottom >=0) && getComputedStyle(lazyImage).display !=="none") {
 lazyImage.src=lazyImage.dataset.src;
 lazyImage.srcset=lazyImage.dataset.srcset;
 lazyImage.classList.remove("lazy");
 lazyImages=lazyImages.filter(function(image) {
 return image !==lazyImage;
 });
 if (lazyImages.length===0) {
 document.removeEventListener("scroll", lazyLoad);
 window.removeEventListener("resize", lazyLoad);
 window.removeEventListener("orientationchange", lazyLoad);
 }
 }
 });
 active=false;
 }, 200);
 }
 };
 document.addEventListener("scroll", lazyLoad);
 window.addEventListener("resize", lazyLoad);
 window.addEventListener("orientationchange", lazyLoad);
});

此代码在 scroll 事件处理程序中使用 getBoundingClientRect 来检查是否有任何 img.lazy 元素处于视口中。 使用 setTimeout 调用来延迟处理,active 变量则包含处理状态,用于限制函数调用。 延迟加载图像时,这些元素随即从元素数组中移除。 当元素数组的 length 达到 0 时,滚动事件处理程序代码随即移除。 您可在此 CodePen 示例中,查看代码的实际运行情况。

虽然此代码几乎可在任何浏览器中正常运行,但却存在潜在的性能问题,即重复的 setTimeout 调用可能纯属浪费,即使其中的代码受限制,它们仍会运行。 在此示例中,当文档滚动或窗口调整大小时,不管视口中是否有图像,每 200 毫秒都会运行一次检查。 此外,跟踪尚未延迟加载的元素数量,以及取消绑定滚动事件处理程序的繁琐工作将由开发者来完成。

简而言之:请尽可能使用 Intersection Observer,如果应用有严格的兼容性要求,则回退到事件处理程序。

CSS 中的图像

虽然 <img> 标记是在网页上使用图像的最常见方式,但也可以通过 CSS background-image 属性(以及其他属性)来调用图像。 与加载时不考虑可见性的 <img> 元素不同,CSS 中的图像加载行为是建立在更多的推测之上。 构建文档和 CSS 对象模型以及渲染 树后,浏览器会先检查 CSS 以何种方式应用于文档,再请求外部资源。 如果浏览器确定涉及某外部资源的 CSS 规则不适用于当前构建中的文档,则浏览器不会请求该资源。

这种推测性行为可用来延迟 CSS 中图像的加载,方法是使用 JavaScript 来确定元素在视口内,然后将一个类应用于该元素,以应用调用背景图像的样式。 如此即可在需要时而非初始加载时下载图像。 例如,假定一个元素中包含大型主角背景图片:

<div class="lazy-background">
 <h1>Here's a hero heading to get your attention!</h1>
 <p>Here's hero copy to convince you to buy a thing!</p>
 <a href="/buy-a-thing">Buy a thing!</a>
</div>

div.lazy-background 元素通常包含由某些 CSS 调用的大型主角背景图片。 但是,在此延迟加载示例中,我们可以通过 visible 类来隔离 div.lazy-background 元素的 background-image 属性,而且我们会在元素进入视口时对其添加这个类:

.lazy-background {
 background-image: url("hero-placeholder.jpg"); /* Placeholder image */
}
.lazy-background.visible {
 background-image: url("hero.jpg"); /* The final image */
}

我们将从这里使用 JavaScript 来检查该元素是否在视口内(通过 Intersection Observer 进行检查!),如果在视口内,则对 div.lazy-background 元素添加 visible 类以加载该图像:

document.addEventListener("DOMContentLoaded", function() {
 var lazyBackgrounds=[].slice.call(document.querySelectorAll(".lazy-background"));
 if ("IntersectionObserver" in window) {
 let lazyBackgroundObserver=new IntersectionObserver(function(entries, observer) {
 entries.forEach(function(entry) {
 if (entry.isIntersecting) {
 entry.target.classList.add("visible");
 lazyBackgroundObserver.unobserve(entry.target);
 }
 });
 });
 lazyBackgrounds.forEach(function(lazyBackground) {
 lazyBackgroundObserver.observe(lazyBackground);
 });
 }
});

如上文所述,由于并非所有浏览器都支持 Intersection Observer,因此您需要确保提供回退方案或 polyfill。 请参阅此 CodePen 演示,查看代码的实际运行情况。

延迟加载视频

与图像元素一样,视频也可以延迟加载。 在正常情况下加载视频时,我们使用的是 <video> 元素(尽管也可以改为使用 <img>,不过实现方式受限)。 但是,延迟加载 <video> 的_方式_取决于用例。 下文探讨的几种情况所需的解决方案均不相同。

视频不自动播放

对于需要由用户启动播放的视频(即_不_自动播放的视频),最好指定 <video> 元素的 preload 属性:

<video controls preload="none" poster="one-does-not-simply-placeholder.jpg">
 <source src="one-does-not-simply.webm" type="video/webm">
 <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

这里,我们使用值为 none 的 preload 属性来阻止浏览器预加载_任何_视频数据。 为占用空间,我们使用 poster 属性为 <video> 元素提供占位符。 这是因为默认的视频加载行为可能会因浏览器不同而有所不同:

  • 在 Chrome 中,之前的 preload 默认值为 auto,但从 Chrome 64 开始,默认值变为 metadata。 虽然如此,在 Chrome 桌面版中,可能会使用 Content-Range 标头预加载视频的部分内容。 Firefox、Edge 和 Internet Explorer 11 的行为与此相似。
  • 与 Chrome 桌面版相同,Safari 11.0 桌面版会预加载视频的部分内容, 而 11.2 版(目前为 Safari 的 Tech Preview 版)仅预加载视频元数据。 iOS 版 Safari 不会 预加载视频。
  • 启用流量节省程序模式后,preload 默认为 none。

由于浏览器在 preload 方面的默认行为并非一成不变,因此您最好明确指定该行为。 在由用户启动播放的情况下,使用 preload="none" 是在所有平台上延迟加载视频的最简单方法。 但 preload 属性并非延迟加载视频内容的唯一方法。 利用视频 预加载快速播放或许能提供一些想法和见解,助您了解如何通过 JavaScript 播放视频。

但遗憾的是,当我们想使用视频代替动画 GIF 时,事实证明以上方法无效。我们将在下文介绍相关内容。

用视频代替动画 GIF

虽然动画 GIF 应用广泛,但其在很多方面的表现均不如视频,尤其是在输出文件大小方面。 动画 GIF 的数据大小可达数兆字节, 而视觉效果相当的视频往往小得多。

使用 <video> 元素代替动画 GIF 并不像使用 <img> 元素那么简单。 动画 GIF 具有以下三种固有行为:

  • 加载时自动播放。
  • 连续循环播放(但并非始终如此)。
  • 没有音轨。

使用 <video> 元素进行替代类似于:

<video autoplay muted loop playsinline>
 <source src="one-does-not-simply.webm" type="video/webm">
 <source src="one-does-not-simply.mp4" type="video/mp4">
</video>

autoplay、muted 和 loop 属性的含义不言而喻,而 playsinline 是在 iOS 中进行自动播放所必需。 现在,我们有了可以跨平台使用的“视频即 GIF”替代方式。 但是,如何进行延迟加载?Chrome 会自动延迟加载视频,但并不是所有浏览器都会提供这种优化行为。 根据您的受众和应用要求,您可能需要自己手动完成这项操作。 首先,请相应地修改 <video> 标记:

<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
 <source data-src="one-does-not-simply.webm" type="video/webm">
 <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

您会发现添加了 poster 属性,您可以使用该属性指定占位符以占用 <video> 元素的空间,直到延迟加载视频为止。 与上文中的 <img>延迟加载示例一样,我们将视频网址存放在每个 <source> 元素的 data-src 属性中。 然后,我们将使用与上文基于 Intersection Observer 的图像延迟加载示例类似的 JavaScript:

document.addEventListener("DOMContentLoaded", function() {
 var lazyVideos=[].slice.call(document.querySelectorAll("video.lazy"));
 if ("IntersectionObserver" in window) {
 var lazyVideoObserver=new IntersectionObserver(function(entries, observer) {
 entries.forEach(function(video) {
 if (video.isIntersecting) {
 for (var source in video.target.children) {
 var videoSource=video.target.children[source];
 if (typeof videoSource.tagName==="string" && videoSource.tagName==="SOURCE") {
 videoSource.src=videoSource.dataset.src;
 }
 }
 video.target.load();
 video.target.classList.remove("lazy");
 lazyVideoObserver.unobserve(video.target);
 }
 });
 });
 lazyVideos.forEach(function(lazyVideo) {
 lazyVideoObserver.observe(lazyVideo);
 });
 }
});

延迟加载 <video> 元素时,我们需要对所有的 <source> 子元素进行迭代,并将其 data-src 属性更改为 src 属性。 完成该操作后,必须通过调用该元素的 load 方法触发视频加载,然后该媒体就会根据 autoplay 属性开始自动播放。

利用这种方法,我们即可提供模拟动画 GIF 行为的视频解决方案。这种方案的流量消耗量低于动画 GIF,而且能延迟加载内容。

延迟加载库

如果您并不关心延迟加载的_实现_细节,只想直接选择使用现有的库(无需感到羞愧!),您有很多选项可以选择。 许多库使用与本指南所示标记模式相似的标记模式。 以下提供一些实用的延迟加载库:

  • lazysizes 是功能全面的延迟加载库,可以延迟加载图像和 iframe。 其使用的模式与本文所示的代码示例非常相似,会自动与 <img> 元素上的 lazyload 类绑定,并要求您在 data-src 和/或 data-srcset 属性中指定图像网址,这两个属性的内容将分别交换到 src 和/或 srcset 属性中。 该库使用 Intersection Observer(您可以使用 polyfill),并可以通过许多插件进行扩展,以执行延迟加载视频等操作。
  • lozad.js 是超轻量级且只使用 Intersection Observer 的库。 因此,它的性能极佳,但如果要在旧浏览器上使用,则需要 polyfill。
  • blazy 是另一个轻量级的延迟加载器(大小为 1.4 KB)。 与 lazysizes 相同,blazy 不需要任何第三方实用程序即可进行加载,并且适用于 IE7+。 但其缺点是不使用 Intersection Observer。
  • yall.js 是我编写的库,该库使用 Intersection Observer,可回退到事件处理程序, 而且与 IE11 和主流浏览器兼容。
  • 如果您正在寻找 React 特定的延迟加载库,您可考虑使用 react-lazyload。 虽然该库不使用 Intersection Observer,但_的确_为习惯于使用 React 开发应用的开发者提供熟悉的图像延迟加载方法。

可能出错的地方

虽然延迟加载图像和视频会对性能产生重要的积极影响,但这项任务并不轻松。 如果出错,可能会产生意想不到的后果。 因此,务必要牢记以下几点:

注意首屏

使用 JavaScript 对页面上的所有媒体资源进行延迟加载很诱人,但您必须抵挡住这种诱惑。 首屏上的任何内容皆不可进行延迟加载, 而应将此类资源视为关键资产,进行正常加载。

以正常而非延迟加载方式加载关键媒体资源的主要理据是,延迟加载会将这些资源的加载延迟到 DOM 可交互之后,在脚本完成加载并开始执行时进行。 对于首屏线以下的图像,可以采用延迟加载,但对于首屏上的关键资源,使用标准的 <img> 元素来加载速度会快得多。

当然,如今用来查看网站的屏幕多种多样,且大小各有不同,因此首屏线的具体位置并不明确。 笔记本电脑上位于首屏的内容在移动设备上可能位于首屏线_以下_。 目前并没有完全可靠的建议,无法在每种情况下完美解决这个问题。 您需要清点页面的关键资产,并以典型方式加载这些图像。

此外,您可能也不想严格限定首屏线作为触发延迟加载的阈值。 对您来说,更理想的做法是在首屏线以下的某个位置建立缓冲区,以便在用户将图像滚动到视口之前,即开始加载图像。 例如,Intersection Observer API 允许您在创建新的 IntersectionObserver 实例时,在 options 对象中指定 rootMargin 属性。 如此即可为元素提供缓冲区,以便在元素进入视口之前触发延迟加载行为:

let lazyImageObserver=new IntersectionObserver(function(entries, observer) {
 // Lazy loading image code goes here
}, {
 rootMargin:"0px 0px 256px 0px"
});

如果 rootMargin 的值与您为 CSS margin 属性指定的值相似,这是因为该值就是如此!在本例中,我们将观察元素(默认情况下为浏览器视口,但可以使用 root 属性更改为特定的元素)的下边距加宽 256 个像素。 这意味着,当图像元素距离视口不超过 256 个像素时,回调函数将会执行,即图像会在用户实际看到它之前开始加载。

要使用滚动事件处理代码实现这种效果,只需调整 getBoundingClientRect 检查以包括缓冲区,如此一来,您即可在不支持 Intersection Observer 的浏览器上获得相同效果。

布局移位与占位符

若不使用占位符,延迟加载媒体可能会导致布局移位。 这种变化不仅会让用户产生疑惑,还会触发成本高昂的 DOM 布局操作,进而耗用系统资源,造成卡顿。 您至少应考虑使用纯色占位符来占用尺寸与目标图像相同的空间,或者采用 LQIP 或 SQIP 等方法,在媒体项目加载前提供有关其内容的提示。

对于 <img> 标记,src 最初应指向一个占位符,直到该属性更新为最终图像的网址为止。 请使用 <video> 元素中的 poster 属性来指向占位符图像。 此外,请在 <img> 和 <video> 标记上使用 width 和 height 属性。 如此可以确保从占位符转换为最终图像时,不会在媒体加载期间改变该元素的渲染大小。

图像解码延迟

在 JavaScript 中加载大型图像并将其放入 DOM 可能会占用主线程,进而导致解码期间用户界面出现短时间无响应的情况。 您可以先使用 decode 方法异步解码图像,再将其插入到 DOM 中,以减少此类卡顿现象,但请注意: 这种方法尚不能通用,而且会增加延迟加载逻辑的复杂性。 如果要采用这种方法,请务必进行检查。 以下示例显示如何通过回退来使用 Image.decode()

var newImage=new Image();
newImage.src="my-awesome-image.jpg";
if ("decode" in newImage) {
 // Fancy decoding logic
 newImage.decode().then(function() {
 imageContainer.appendChild(newImage);
 });
} else {
 // Regular image load
 imageContainer.appendChild(newImage);
}

请参阅此 CodePen 链接,查看与此示例相似的代码的实际运行情况。 如果您大部分的图像都相当小,则这种方法的帮助不大,但肯定有助于减少延迟加载大型图像并将其插入 DOM 时的卡顿现象。

内容不加载

有时,媒体资源会因为某种原因而加载失败,进而导致发生错误。 何时会发生这种情况?何时发生视情况而定,以下是一种假设情况: 您有一个短时间(例如,5 分钟)的 HTML 缓存策略,而用户访问网站,_或_保持打开旧选项卡并长时间离开(例如,数个小时),然后返回继续阅读内容。 在此过程中的某个时刻,发生重新部署。 在此部署期间,图像资源的名称因为基于哈希的版本控制而更改,或者完全移除。 当用户延迟加载图像时,该资源已不可用,因此导致加载失败。

虽然出现这种情况的机会比较小,但您也有必要制定后备计划,以防延迟加载失败。 对于图像,可采取如下解决方案:

var newImage=new Image();
newImage.src="my-awesome-image.jpg";
newImage.onerror=function(){
 // Decide what to do on error
};
newImage.onload=function(){
 // Load the image
};

发生错误时采取何种措施取决于应用。 例如,可以将图像占位符区域替换为按钮,以允许用户尝试重新加载该图像,或者直接在图像占位符区域显示错误消息。

此外,也可能会发生其他情况。 无论采取何种方法,在发生错误时通知用户,并提供可能的解决方案总不是坏事。

JavaScript 可用性

不应假定 JavaScript 始终可用。 如果要延迟加载图像,请考虑提供 <noscript> 标记,以便在 JavaScript 不可用时显示图像。 例如,最简单的回退方法是使用 <noscript> 元素在 JavaScript 处于关闭状态时提供图像:

<!-- An image that eventually gets lazy loaded by JavaScript -->
<img class="lazy" src="placeholder-image.jpg" data-src="image-to-lazy-load.jpg" alt="I'm an image!">
<!-- An image that is shown if JavaScript is turned off -->
<noscript>
 <img src="image-to-lazy-load.jpg" alt="I'm an image!">
</noscript>

如果 JavaScript 已关闭,用户会_同时_看到占位符图像以及 <noscript> 元素中包含的图像。 要解决此问题,我们可以在 <html> 标记上放置 no-js 类,如下所示:

<html class="no-js">

然后,在通过 <link> 标记请求任何样式表之前,于 <head> 中放置一行内联脚本,用于在 JavaScript 处于打开状态时从 <html> 元素中移除 no-js 类:

<script>document.documentElement.classList.remove("no-js");</script>

最后,我们可以使用一些 CSS,在 JavaScript 不可用时隐藏类为 lazy 的元素,如下所示:

.no-js .lazy {
 display: none;
}

这并不会阻止占位符图像加载,但是结果却更令人满意。 关闭 JavaScript 的用户不只是能看到占位符图像,这要比只能看到占位符和没有意义的图像内容更好。


结论

务必谨慎使用延迟加载图像和视频方法,该方法可以显著减少网站上的初始加载时间和页面负载。 用户不查看的媒体资源不会为其带来不必要的网络活动和处理成本,但用户可以根据需要查看这些资源。

就性能改进方法而言,延迟加载无可争议。 如果您的网站上存在大量内联图像,这是减少非必要下载的好方法。 您的网站用户和项目干系人都会因该方法而受益匪浅!