[譯] 網速敏感的視頻延遲加載方案

一個大視頻的背景,若是作的好,會是一個絕佳的體驗!可是,在首頁添加一個視頻並不只僅是隨便找我的,而後加個 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:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索