前端性能优化: 网页延迟加载资源

如今前端的性能优化越来越受重视了,良好的用户体验更加更吸引和留住用户。前端性能优化手段也逐渐增多,web性能优化包括资源延迟加载,优化代码等前端方面的配置。延迟加载包括有延迟加载图片,css中的图像懒加载,延迟加载视屏等可视部分,也还有一些脚本的延迟加载和执行。

延迟加载

什么是延迟加载?

延迟加载是一种在加载页面时,延迟加载非关键资源的一种方法,而这些非关键资源则在需要时才进行加载,就图像而言,'非关键’通常指的是‘屏幕外’。

  • 延迟加载图像和视频时,可以减少初始页面加载时间、初始页面负载以及系统资源使用量,所有这一切都会对性能产生积极影响。
  • 通常来说加载网页时,浏览器会请求所有的图像,而不管它们实在视窗还是在页面的更深处并且不可见。延迟加载允许我们加载可见的图像,并且在用户滚动时按需异步加载其他的图片。这减少了负载请求的数据,并可以大大加快该过程。

延迟加载图像

HTML中的延迟加载内联图像

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

  2. 使用 Intersection Observer。 之前写过的延迟加载代码,有的是使用scroll或resize等时间处理程序来完成任务。这种方式的有点就是各浏览器之间的兼容性最好。但现在浏览器支持通过Intersection Observer API 来检查元素的可见性。这中方式的性能和效率更好。当然并非所有的浏览器都支持Intersection Observer

与依赖于各种事件处理程序的代码相比,Intersection Observer更容易使用和阅读。只需要注册一个Observer即可监听视元素,就不需要编写冗余的元素可见性检测代码。只需要决定元素可见时需要做什么即可。

<img src alt="I'm an image!">
<img src alt="I'm an image!">

需要关注的三部分:

  1. class,javascript中选择元素时需要使用的类选择器。
  2. src,引用页面最初加载时显示的占位符图片。
  3. data-src和data-srcset,属于占位属性,其中包含元素进入视口后要加载的图片的网址。

接下来使用Intersection Observer实现延迟加载图片,就是我们常说的图片懒加载。

document.addEventListener('DOMContentLoaded',function(){
      const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

        if ("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype) {
            let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
                entries.forEach(function(entry) {
                    if (entry.isIntersecting) {
                        let lazyImage = entry.target;
                        lazyImage.src = lazyImage.dataset.src;
                        lazyImage.classList.remove("lazy");
                        lazyImageObserver.unobserve(lazyImage);
                    }
                });
            });

            lazyImages.forEach(function(lazyImage) {
                lazyImageObserver.observe(lazyImage);
            });
        }else{
        //  对不支持Intersection Observer 其他处理方式
    }
})
document.addEventListener('DOMContentLoaded',function(){
      const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

        if ("IntersectionObserver" in window && "IntersectionObserverEntry" in window && "intersectionRatio" in window.IntersectionObserverEntry.prototype) {
            let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
                entries.forEach(function(entry) {
                    if (entry.isIntersecting) {
                        let lazyImage = entry.target;
                        lazyImage.src = lazyImage.dataset.src;
                        lazyImage.classList.remove("lazy");
                        lazyImageObserver.unobserve(lazyImage);
                    }
                });
            });

            lazyImages.forEach(function(lazyImage) {
                lazyImageObserver.observe(lazyImage);
            });
        }else{
        //  对不支持Intersection Observer 其他处理方式
    }
})

DOMContentLoaded事件中,脚本会查询DOM来获取类属性为lazy的所有<img>元素。

实现效果

Intersection Observer来做延迟加载,但是对兼容性要求严格,可以使用polyfill来做兼容处理。但是也可以使用scrollresize的代码,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);
});
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 示例中,查看代码的实际运行情况。

css中的图像懒加载

将通过设置css的background-color属性了调用图片,预加载时不考虑可见性的<img>不同,css中的图片加载行为是建立在更多推测之上。构建文档和CSSOM以及渲染树后,浏览器会先检查CSS以何种方式适应于文档,再请求外部资源。吐过浏览器确定设计某外部资源成的CSS规则不适用于当前构建的文档,则浏览器将不会请求该资源。

这种推测性行为来延迟CSS中的图片加载,方法是使用Javascript来确定元素在视口内,然后将一个类应用于该元素,已应用背景图像的样式。

<div>
  <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>
  <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-color属性。当元素进入视口时再对其添加这个类。

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

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

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

通过元素是否在视口内(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);
    });
  }
});
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);
    });
  }
});

延迟加载视频

在正常情况下加载视屏时,我们使用的是<video>元素。

视频不需要自动播放

使用度量none的preload属性来阻止浏览器预加载任何视频数据。poster属性提供占位符。

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

视频代替GIF

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

gif图片加载时会自动播放,并且会循环播放而且没有声音。

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

<video autoplay loop>
  <source src="one-does-not-simply.webm" type="video/webm">
  <source src="one-does-not-simply.mp4" type="video/mp4">
</video>
<video autoplay loop>
  <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 autoplay loop width="610" height="254">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>
<video autoplay loop width="610" height="254">
  <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> 延迟加载示例一样,我们将视频网址存放在每个 元素的 data-src 属性中。 然后将使用与上文基于 Intersection Observer 的图像延迟加载示例类似。

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);
    });
  }
});
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 属性开始自动播放。

延迟加载库

参考链接