Avoiding Unnecessary Paints

HTML5 Rocks

引言

將一個網站或是網頁應用程式的元件畫出來可以是相當耗費資源的,甚至有可能在程式執行時對效能產生負面的連鎖效應。這篇文章很快的說明一下什麼會讓瀏覽器產生畫圖(painting)的行為,並且讓你知道如何預防不必要的畫圖行為。

畫圖行為: 讓我們快速的介紹一下

瀏覽器主要的工作之一,就是將你的DOM和CSS轉換成螢幕上的像素(pixel),而達成這項工作需要經過相當複雜的處理。 一開始是這樣的,瀏覽器讀取標記語言(markup),然後建立DOM樹。 接下來建立CSSOM,做的事情和建立DOM樹差不多。 接著將DOM和CSSOM組合在一起,最後,我們就得到一個可以來畫像素的結構。

如果你想要更深入一點知道瀏覽器是怎麼運作的,這裡有篇文章 in-depth article here on HTML5 Rocks 你應該看一看!

畫圖行為的處理本身是很有趣的。 在Chrome中,組合好的DOM和CSS樹,會使用一個軟體叫做Skia來點陣化。 如果你曾經玩過canvas元件,Skia的API會讓你感到非常熟悉,這個API有很多moveTo-和lineTo-形式和一堆更加進階的函式。實際上,所有需要被畫出來的元件會被提取到Skia可以執行的的集合當中,然後產出一堆點陣(bitmaps)。這些點陣會被上傳到GPU,GPU再合成這些點陣成為螢幕上顯示的圖。

要注意的是,Skia的工作負載跟你怎麼使用你的元件有直接相關。如果你使用了繁重的演算法,Skia會需要做更多的工作。 Colt McAnlis寫了篇文章article on how CSS affects page render weight,你應該看看。

就像先前說的,畫圖行為需要時間,如果我們不想辦法減少處理時間,而讓它執行超過我們的幀預算,約是16毫秒,使用者就會發現畫圖會掉幀,這會讓這個APP的使用者經驗非常的差,而且覺得這個東西很沒用。我們絕對不想要這樣,所以我們要來看看什麼事情所造成的畫圖行為是不必要的,然後看看我們能夠怎麼辦。

卷軸(Scrolling)

每次你將卷軸捲上捲下的時候,瀏覽器就需要重畫它即將要顯示在螢幕上的內容。如果重畫的區域很小當然很好,但即使在這樣的情況下,元件也有可能是以很複雜的方式被提交給瀏覽器的。所以需要畫圖的區域很小不代表它可以被畫得很快。

你可以用Chrome的DevTools裡面的"Show Paint Rectangles"功能(按一下右下角的齒輪圖案)來看看那些區域被重畫了。當DevTools開啟的狀態下,簡單的跟網頁做些互動,你就會看見閃亮的矩形出現在頁面被重畫的地方。

在Chrome DevTools中的Show Paint Rectangles功能

卷軸的效能跟你網站的成功與否很有關係,使用者的的確確會發現你的網站或是網頁應用程式捲頁捲得不好,而且他們很不喜歡這樣。 所以我們有興趣的是讓捲動卷軸時,讓畫圖行為保持輕量,讓使用者不會看到奇怪的地方。

我之前寫了一篇文章,article on scrolling performance,如果你想知道關於卷軸效能的更多細節你可以去拿來看看。

互動(Interactions)

互動是另一個會導致畫圖行為的原因,比如: hover行為、點按行為、拖拉行為。當使用者執行以上其中一個行為,比如說hover行為好了,Chrome就必須去重畫收到影響的元件。而且就像卷軸事件,如果要執行大量又複雜的畫圖行為,也會導致掉幀。

每個人都想要很棒的、平滑的、互動的動畫效果,所以再一次的我們需要看看這些動畫而產生的改變會不會花掉太多的時間。

一個不好的組合範例

一個昂貴的畫圖行為範例

如果我同時捲動卷軸又滑動滑鼠會發生什麼事?對我來說非常有可能不經意的跟某個我捲過它的元件"互動"了,然後觸發了一次昂貴的畫圖行為。反過來說,這樣會導致我用掉幀預算約16.7毫秒(需要每秒要畫60幀用掉的時間)。 我做了一個範例,來讓你知道我到底是什麼意思。 希望你在捲動卷軸跟滑動滑鼠的時候會看到hover的影響,但先讓我們來看看這在Chrome DevTools中所表現的樣子:

Chrome's DevTools擷取出來的那些耗費昂貴資源的幀

你可以看上面這張圖,當我hover過某一個區塊時,DevTools暫存了畫圖的工作。 我故意讓它執行了一些超級繁重的處理,好強調我想表達的,所以有些時候超過了幀預算。最後我想要的是讓畫圖行為不必要的發生,特別是執行捲動卷軸的時候!

那麼我們到底要怎麼停止這樣昂貴的畫圖行為呢?其實很簡單實作,技巧在於連接一個scroll的處理函式,會關閉hover的影響,然後設定一個計時器,時間到了再把hover打開。意思是我們保證當你在捲動卷軸時不用去執行昂貴的互動事件的重畫。當你停止動作一段足夠的時間後,我們再將互動事件啟動。

實作這個技巧是會影響到你的應用程式的使用者經驗的,所以要靈活運用。只有你和你的團隊有權力決定要停止多久再重新啟動hover事件的影響。

這邊是程式碼:

// 用來追蹤hover行為的啟動情形
var enableTimer = 0;

/*
 * 監聽卷軸並移除可能的hover
 */
window.addEventListener('scroll', function() {
  clearTimeout(enableTimer);
  removeHoverClass();

  // 在一秒後重新啟動hover,這裡你自己決定你要的等待多久!
  enableTimer = setTimeout(addHoverClass, 1000);
}, false);

/**
 * 從body移除hover的class
 */
function removeHoverClass() {
  document.body.classList.remove('hover');
}

/**
 * 把hover行為需要的class加回去
 */
function addHoverClass() {
  document.body.classList.add('hover');
}

你可以看到我們放了一個class在body上,去追蹤hover現在是否是被允許的。相關的style如下:

/* 在執行任何hover事件之前要把這個class加上去 */
.hover .block:hover {
 …
}

這就是全部了!

結論

瀏覽器的Rendering效能對於使用者使用你的應用程式是很關鍵的,你應該要一直注意並確保你的畫圖行為的工作量小於16ms。 而且你應該在開發階段使用DevTools來幫助你確認並修復你的程式的瓶頸。

一些不經意的互動行為,特別是跟那些會產生昂貴畫圖行為的元件互動時,可能會非常的耗費Redering效能。 但也如你所見,我們可以用一小段程式碼來修復這樣的情形。

再多看看你的網站和應用程式吧,看看是不是能為它的畫圖行為再多做一點保護。

Comments

0