[译] 网速敏感的视频延迟加载方案

一个大视频的背景,若是作的好,会是一个绝佳的体验!可是,在首页添加一个视频并不只仅是随便找我的,而后加个 25mb 的视频,那会让你的全部的性能优化都付之一炬。javascript

Lazy pandas love lazy loading. (Photo by Elena Loshina)css

我参加过一些团队,他们但愿给首页加上相似的全屏视频背景。我一般不肯意那么作,由于这种作法一般会致使性能上的噩梦。老实说,我曾给一个页面加上一个 40mb 大的视频。 😬html

上次有人让我这么作的时候,我很好奇应如何将背景视频的加载做为渐进加强(Progressive Enhancement),来提高网络链接情况比较好的用户的体验。除了和个人同事们强调视频体积小和压缩视频的重要性之外,也但愿在代码上有一些奇迹发生。前端

下面是最终的解决方案:java

  1. 尝试使用 JavaScript 加载 <source>
  2. 监听 canplaythrough 事件
  3. 若是 canplaythrough 事件没有在 2 秒内触发,那么使用 Promise.race() 将视频加载超时
  4. 若是没有监听到 canplaythrough 事件,那么移除 <source>,而且取消视频加载
  5. 若是监测到 canplaythrough 事件,那么使用淡入效果显示这个视频

标记

这里要注意的问题是,即便我正在 <video> 标签中使用 <source>,但我还没为这些 <source> 设置 src 属性。若是设置了 src 属性,那么浏览器会自动地找到它能够播放的第一个 <source>,并当即开始下载它。android

由于在这个例子中,视频是做为渐进加强的对象,默认状况下咱们不用真的加载视频。事实上惟一须要加载的,是咱们为这个页面设置的预览图片。ios

<video class="js-video-loader" poster="<?= $poster; ?>" muted="true" loop="true">
    <source data-src="path/to/video.webm" type="video/webm">
    <source data-src="path/to/video.mp4" type="video/mp4">
  </video>

JavaScript

我编写了一个简单的 JavaScript 类,用于查找带有 .js-video-loader 这个 class 的 video 元素,让咱们之后能够在其余视频中复用这个逻辑。完整的源码能够从 Github 上看到git

构造函数是这样的:github

constructor () {
    this.videos = Array.from(document.querySelectorAll('video.js-video-loader'));
    // 将在下面状况下返回
    // - 浏览器不支持 Promise
    // - 没有 video 元素
    // - 若是用户设置了减小动态偏好(prefers reduced motion) 
    // - 在移动设备上
    if (typeof Promise === 'undefined'
      || !this.videos
      || window.matchMedia('(prefers-reduced-motion)').matches
      || window.innerWidth < 992
    ) {
      return;
    }
    this.videos.forEach(this.loadVideo.bind(this));
  }

这里咱们所作的就是找到这个页面上全部咱们但愿延迟加载的视频。若是没有,咱们能够返回。当用户开启了减小动态偏好(preference for reduced motion)设置时,咱们一样不会加载这样的视频。为了避免让某些低网速或低图形处理能力的手机用户担忧,在小屏幕手机上也会直接返回。(我在考虑是否能够经过 <source> 元素的媒体查询来作这些,但也不肯定。)web

而后给每一个视频运行这个视频加载逻辑。

loadVideo

loadVideo() 是一个调用其余函数的简单的函数:

loadVideo(video) {
    this.setSource(video);
    // 加上了视频连接后从新加载视频
    video.load();
    this.checkLoadTime(video);
  }

setSource

setSource() 中,咱们找到那些做为数据属性(Data Attributes)插入的视频连接,而且将它们设置为真正的 src 属性。

/**
    * 找 video 子元素中是 <source> 的,
    * 基于 data-src 属性,
    * 给每一个 <source> 设置 src 属性
    *
    * @param {DOM Object} video
    */
    setSource (video) {
      let children = Array.from(video.children);
      children.forEach(child => {
        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {
          child.setAttribute('src', child.dataset.src);
        }
      });
    }

基本上,我所作的就是遍历每个 <video> 元素的子元素,找一个定义了 data-src 属性(child.dataset.src)的 <source> 子元素。若是找到了,那就用 setAttribute 将它的 src 属性设置为视频连接。

如今视频连接已经被设置给 <video> 元素了,下面须要让浏览器再次加载视频。咱们经过在 loadVideo() 中的 video.load() 来完成这个工做。load() 方法是 HTMLMediaElement API 的一部分,它能够重置媒体元素而且重启加载过程。

checkLoadTime

接下来是见证奇迹的时刻。在 checkLoadTime() 方法中咱们建立了两个 Promise。第一个 Promise 将在 <video> 元素的 canplaythrough 事件触发时被 resolve。这个 canplaythrough 事件是浏览器认为这个视频能够在不停下来缓冲的状况下持续播放的时候被触发。咱们在这个 Promise 中添加一个这个事件的监听回调,当这个事件触发的时候执行 resolve()

// 建立一个 Promise,将在
  // video.canplaythrough 事件发生时被 resolve
  let videoLoad = new Promise((resolve) => {
    video.addEventListener('canplaythrough', () => {
      resolve('can play');
    });
  });

咱们同时建立另外一个 Promise 做为计时器。在这个 Promise 中,当通过一个设定好的时间后,咱们使用 setTimeout 来将这个 Promise 给 resolve 掉,我这设置了一个 2 秒的时延(2000毫秒)。

// 建立一个 Promise 将在
  // 特定时间(2s)后被 resolve
  let videoTimeout = new Promise((resolve) => {
    setTimeout(() => {
      resolve('The video timed out.');
    }, 2000);
  });

如今咱们有了两个 Promise,咱们能够经过 Promise.race() 看他们谁先完成。

// 将 promises 进行 Race 看看哪一个先被 resolves
  Promise.race([videoLoad, videoTimeout]).  then(data => {
    if (data === 'can play') {
      video.play();
      setTimeout(() => {
        video.classList.add('video-loaded');
      }, 3000);
    } else {
      this.cancelLoad(video);
    }
  });

在这个 .then() 的回调中咱们等着拿到最早被 resolve 的那个 Promise 传回来的信息。若是这个视频能够播放,那么我就会拿到以前传的 can play,而后试一下是否能够播放这个视频。video.play() 是使用 HTMLMediaElement 提供的 play() 方法来触发视频播放。

3 秒后,setTimeout() 将会给这个标签加上 .video-loaded 类,这将有助于视频文件更巧妙的淡入自动循环播放。

若是咱们没接收到 can play 字符串,那么咱们将取消这个视频的加载。

cancelLoad

cancelLoad() 方法作的基本上跟 loadVideo() 方法相反。它从每一个 source 标签移除 src 属性,而且触发 video.load() 来重置视频元素。

若是咱们不这么作,这个视频元素将会在后台保持加载状态,即便咱们都没将它显示出来。

/**
    * 经过移除全部的 <source> 来取消视频加载
    * 而后触发 video.load().
    *
    * @param {DOM object} video
    */
    cancelLoad (video) {
      let children = Array.from(video.children);
      children.forEach(child => {
        if (child.tagName === 'SOURCE' && typeof child.dataset.src !== 'undefined') {
          child.parentNode.removeChild(child);
        }
      });
      // 从新加载没有 <source> 标签的 video
      // 这样它会中止下载
      video.load();
    }

总结

这个方法的缺点是,咱们仍然试图经过一个不必定靠谱的连接来下载一个可能比较大的文件,可是经过提供一个超时时间,咱们但愿可以给某些网速慢的用户节约一些流量而且得到更好的性能。根据我在 Chrome Dev Tools 里将网速节流到慢 3G 条件下的测试,这个方法将在超时以前加载了 512kb 的视频。即便是一个 3-5mb 的视频,对于一些网速慢的用户来讲,这也带来了显著的流量节省。

你以为怎么样?若是有改进的建议,欢迎在评论里分享!


Originally published at benrobertson.io.

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。

掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

PS:欢迎你们关注个人公众号【前端下午茶】,一块儿加油吧~

另外能够加入「前端下午茶交流群」微信群,长按识别下面二维码便可加我好友,备注加群,我拉你入群~

相关文章
相关标签/搜索