天天看點

Chrome 圖檔解碼與 Image.decode API

在這篇文章裡面,我會對 Chrome 是何時對圖檔進行解碼進行說明,并結合 Chrome 的渲染流水線說明為什麼圖檔解碼可能會造成動畫卡頓,而新的 Image.decode API 是如何讓我們控制圖檔解碼的時機,通過預先解碼來避免動畫卡頓。

為了讓讀者更好地了解本文的内容,請閱讀我之前的文章 ——

浏覽器渲染流水線解析與網頁動畫性能優化

圖檔解碼

圖檔解碼與動畫

預設的情況下,圖檔的解碼會發生在這個圖檔所屬的 image 元素被光栅化的過程中。而當一個元素處于可見區域,或者非常接近可見區域,浏覽器的合成器就會安排元素所在圖層的光栅化。合成器會檢查該圖層是否包含圖檔,如果有的話會建立對應的圖檔解碼任務交由光栅化線程去執行,在這個過程中,合成器所在的合成線程是前台線程,光栅化線程是背景線程。

Chrome 圖檔解碼與 Image.decode API
光栅化線程的圖檔解碼任務

我們從

了解到網頁動畫可以分為合成器動畫(也可以稱為圖層動畫)和非合成器動畫(也可以稱為 DOM 動畫),對于合成器動畫來說,因為可見區域的繪制允許出現空白,是以合成器在動畫每一幀的繪制過程中不需要等待該幀可見區域的光栅化任務(包含圖檔解碼任務)的完成,動畫的運作和光栅化是異步的。而對于非合成器動畫來說,因為不允許可見區域的繪制出現空白,是以動畫的每一幀都需要等待可見區域的光栅化任務的完成才允許送出繪制請求,這相當于動畫的運作和光栅化是強制同步的。

我在支付寶的技術分享上也解釋過為什麼頁端通過 rAF/timer 來連續改變元素的 transform,模拟慣性滾動的動畫,始終在流暢度上很難達到由浏覽器合成器驅動的慣性滾動動畫的水準,容易出現卡頓掉幀的情況(舊版的手淘頁面就是采用這種方式模拟慣性滾動)。其中一個重要的原因就是圖檔解碼,圖檔解碼是光栅化過程中非常耗時的一個步驟,通常需要花費幾十毫秒或者上百毫秒,有的超大圖檔甚至可能達到幾百毫秒,如果動畫會被圖檔解碼所阻塞,那麼當圖檔即将出現在可見區域時,掉幀也是必然會發生的事情。

之前也有幫螞蟻莊園分析過一個彈出面闆卡頓的問題,這個彈出面闆動畫也是一個 rAF/timer 驅動的非合成器動畫,面闆上包含了一個超大圖檔的顯示,動畫過程中當圖檔即将出現在可見區域時,觸發圖檔解碼就造成了幾百毫秒的動畫卡頓。

圖檔解碼緩存

合成器會通過一個圖檔解碼緩存

ImageDecodeCache

來緩存解碼後的圖檔像素資料和生成的紋理,如果一個圖檔已經解碼并在緩存中時,它再次被繪制時就不需要重複解碼。但是解碼緩存的大小是有限制的,隻能容納有限的圖檔(在移動裝置的上限通常是 128 兆或者更低),緩存采用 MRU(最近使用)的淘汰政策,如果一個圖檔已經被移出可見區域并且一段時間沒有參與繪制,就有可能會被緩存淘汰,當它重新進入可見區域時,就會觸發重解碼。

Image.decode API

因為圖檔解碼可能會造成非合成器動畫的卡頓,那麼最直覺的優化想法就是,我能不能先解碼圖檔,解碼完成後再把圖檔加入到 DOM 樹裡面參與繪制。這種方式在 UI 程式設計裡面也十分常見,應用自己管理一個解碼圖檔的緩存池,如果一個圖檔需要顯示時先請求解碼,解碼完成後才真正加入到 UI 界面中參與繪制。浏覽器新增的

就是讓 Web 也具備相似的能力。

Image.decode 可以讓 Web 端請求對這個圖檔進行提前解碼,這個請求被 Blink 發送到合成器,合成器就會生成一個圖檔解碼任務交由光栅化線程去運作。Image.decode 會傳回一個 Promise,當光栅化線程完成解碼任務後會通知合成器并将結果儲存在解碼緩存裡面,合成器再通知 Blink resolve 這個 Promise,這時 Web 端就能接收到圖檔解碼完成的通知。

ScriptPromise HTMLImageElement::decode(ScriptState* script_state,
                                       ExceptionState& exception_state) {
  return GetImageLoader().Decode(script_state, exception_state);
}

void ImageLoader::DispatchDecodeRequestsIfComplete() {
  ...

  LocalFrame* frame = GetElement()->GetDocument().GetFrame();
  for (auto& request : decode_requests_) {
    ...
    Image* image = GetContent()->GetImage();
    frame->GetChromeClient().RequestDecode(
        frame, image->PaintImageForCurrentFrame(),
        WTF::Bind(&ImageLoader::DecodeRequestFinished,
                  WrapCrossThreadWeakPersistent(this), request->request_id()));
    request->NotifyDecodeDispatched();
  }
}           
Image.decode API 在 Chrome 中的部分實作,Blink 向合成器發起解碼請求

這個

示範視訊

顯示了如何使用 Image.decode API 來避免一個 rAF 動畫的突然卡頓,這裡是

Demo 的代碼
function prepareImage() {
  var img = new Image();
  img.src = "nebula.jpg";
  img.decode().then(function() { document.body.appendChild(img); });
}           

上面的示例代碼中,prepareImage 中請求對 nebula.jpg 進行解碼,在解碼完成後再把它加入到 DOM 樹,這樣就規避了 rAF 驅動的時鐘指針旋轉動畫因為圖檔解碼造成的卡頓。

如前所述,合成器的圖檔解碼緩存大小是有限的,而且不必要的記憶體占用也是不好的行為,是以對所有已經加載的圖檔都發起解碼請求并不是一個良好的使用方式。一個可能的更好做法是:

  1. 當圖檔加載完成後,需要在可見區域顯示或者即将需要顯示,這時先發起解碼請求,等待解碼完成後再加入到 DOM 樹中(可以跟圖檔延遲加載的機制相結合);
  2. 如果一個圖檔已經不在可見區域很長一段時間,可以考慮從 DOM 樹移除,隻保留一個大小相同的占位符,等下次再進入可見區域時再重複 1 的操作;
如果讀者覺得這篇文章有所幫助,請繼續關注專欄和為本文點贊,這樣可以幫助我繼續創作更多更有價值的文章。

繼續閱讀