性能優化之關於像素管道及優化(二)

像素管道,這個和咱們寫代碼息息相關的東西,我估計不少人都不太清楚它是個什麼,網上也有幾篇文章關於它的內容,可是不是那麼盡如人意,那麼我就詳細說說這個東西,以及如何優化它。css

關於動畫加載與人們的反應

一個流暢的動畫關乎用戶體驗(留存)html

延遲 用戶反應
0 - 16 毫秒 大部智能設備的刷新率都是 60HZ,也就是每幀 16 毫秒<br />(包括瀏覽器將新幀繪製到屏幕上所需的時間),<br />留給應用大約 10 毫秒的時間來生成一幀。
0 - 100 毫秒 在此時間窗口內響應用戶操做,他們會以爲能夠當即得到結果。<br />時間再長,操做與反應之間的鏈接就會中斷。
100 - 300 毫秒 用戶會遇到輕微可覺察的延遲。
300 - 1000 毫秒 在此窗口內,延遲感受像是任務天然和持續發展的一部分。<br />對於網絡上的大多數用戶,加載頁面或更改視圖表明着一個任務。
1000+ 毫秒 超過 1 秒,用戶的注意力將離開他們正在執行的任務。
10,000+ 毫秒 拜拜
  • 對於一個動做的響應,我建議通常在 100 毫秒內解決,這適用於大多數輸入,無論他們是在點擊按鈕、切換表單控件仍是啓動動畫。
  • 對於須要超過 500 毫秒才能完成的操做,請始終提供反饋,例如 Loading

關於像素管道

從純粹的數學角度而言,每幀的預算約爲 16 毫秒(1000 毫秒 / 60 幀 = 16.66 毫秒/幀)。 但由於瀏覽器須要花費時間將新幀繪製到屏幕上,只有 10 毫秒來執行代碼瀏覽器

若是沒法符合此預算,幀率將降低,而且內容會在屏幕上抖動。 此現象一般稱爲卡頓,會對用戶體驗產生負面影響。sass

而瀏覽器花費時間進行繪製的過程就是執行像素管道的過程。網絡

什麼是像素管道

一個經典的圖:異步

像素管道

s上圖就是一個像素管道,這就是像素繪製到屏幕上的關鍵點。函數

  • JavaScript(代碼變更)。通常來講,咱們會使用 JavaScript 來實現一些視覺變化的效果。好比用 jQuery 的 animate 函數作一個動畫、對一個數據集進行排序或者往頁面裏添加一些 DOM 元素等。固然,除了 JavaScript,還有其餘一些經常使用方法也能夠實現視覺變化效果,好比:CSS Animations、Transitions 和 Web Animation API。
  • Style(樣式計算)。此過程就是利用 CSS 匹配器計算出元素的變化,再進行計算每一個元素的最終樣式。
  • Layout(佈局計算)。當 Style 規則應用後,瀏覽器會開始計算其在屏幕上顯示的位置和佔據的空間大小,然而一個元素的變更可能會影響到另一個元素,從而引發重排,因此佈局變更是很頻繁的,這一過程常常發生。
  • Paint(繪製)。繪製就是簡單的像素填充,會將排列好的樣式進行填充。其包括文本、顏色、圖片、邊框、陰影等任何可視部分。由於網頁樣式是個層級結構,因此繪製操做會在每一層進行。
  • Composite(合成)。由於層級緣由,當層級繪製完成,爲了確保層級結構的正確,合成操做會按照正確的層級順序繪製到屏幕上,以便保證渲染的正確性,由於一個小小的層級順序錯誤,就有可能形成樣式紊亂。

管道的每一個部分都有可能會產生卡頓,因此咱們務必要知道哪一部分出現了問題,對症下藥。佈局

因爲如今瀏覽器的更新,許多瀏覽器已經可以將繪製樣式變更和頁面繪製分開線程進行渲染,這已經不是咱們可以控制的了,可是不管怎麼變更,管道始終要進行,不必定每幀都老是會通過管道每一個部分的處理。實際上,無論是使用 JavaScriptCSS 仍是網絡動畫,在實現視覺變化時,管道針對指定幀的運行一般有三種方式。post

管道運行方式

JS/CSS —> Style —> Layout —> Paint —> Composite

像素管道

此過程就是咱們常說的瀏覽器重排,也就是改變了元素的幾何屬性(例如寬度、高度、左側或頂部位置等),那麼瀏覽器將必須檢查全部其餘元素,而後「自動重排」頁面。任何受影響的部分都須要從新繪製,並且最終繪製的元素需進行合成,重排進行了管道的每一步,性能受到較大影響。性能

JS/CSS —> Style —> Paint —> Composite

這既是咱們常說的重繪,也就是修改「paint only」屬性(例如背景圖片、文字顏色或陰影等),即不會影響頁面佈局的屬性,則瀏覽器會跳過佈局,但仍將執行繪製。

JS/CSS —> Style —> Composite

此過程不會重排重繪,僅僅是進行合成,也就是修改 transformopacity 屬性更改來實現動畫,性能獲得較大提高,最適合於應用生命週期中的高壓力點,例如動畫或滾動。

若是你想知道更改任何指定 CSS 屬性將觸發上述三個版本中的哪個,請點擊這裏

上面列出的各項管道工做在計算開銷上有所不一樣,一些任務比其餘任務的開銷要大,因此接下來,讓咱們深刻了解此管道的各個不一樣部分。

我會以一些常見問題爲例,闡述如何診斷和修正它們。

如何優化

JS/CSS(代碼變更)

咱們使用 JS 來改變樣式是最爲常見的,對於經過 JS 來改變更畫,有如下幾點須要注意:

  • 動畫效果儘可能使用 requestAnimationFrame 而不是使用 setTimeout 或者 setInterval
  • 因爲 JS 是單線程運行,請將須要耗費大量時間運行的任務放到 Web Worker 進行執行。
  • 請使用微任務來進行 DOM 更改,若是你不瞭解什麼是微任務,請點擊這裏
  • 使用 Chrome DevToolsTimelineJavaScript 分析器來評估 JavaScript 的影響

使用requestAnimationFrame

大多時候,我想大部分人執行一個動畫效果都會用到 setTimeout 或者 setInterval 這兩個函數,可是這兩個函數和requestAnimationFrame 有什麼區別呢,彷佛用下來感受差很少?

這要從屏幕刷新率提及。

屏幕刷新頻率

屏幕刷新頻率,即圖像在屏幕上更新的速度,也即屏幕上的圖像每秒鐘出現的次數,它的單位是赫茲(Hz)。 對於通常筆記本電腦,這個頻率大概是60Hz。

所以,當你對着電腦屏幕什麼也不作的狀況下,顯示器也會以每秒60次的頻率正在不斷的更新屏幕上的圖像。爲何你感受不到這個變化? 那是由於人的眼睛有視覺停留效應,即前一副畫面留在大腦的印象還沒消失,緊接着後一副畫面就跟上來了,這中間只間隔了16.7ms(1000/60≈16.7), 因此會讓你誤覺得屏幕上的圖像是靜止不動的。而屏幕給你的這種感受是對的,試想一下,若是刷新頻率變成1次/秒,屏幕上的圖像就會出現嚴重的閃爍,這樣就很容易引發眼睛疲勞、痠痛和頭暈目眩等症狀。

動畫原理

根據上面的原理咱們知道,你眼前所看到圖像正在以每秒60次的頻率刷新,因爲刷新頻率很高,所以你感受不到它在刷新。而動畫本質就是要讓人眼看到圖像被刷新而引發變化的視覺效果,這個變化要以連貫的、平滑的方式進行過渡。 那怎麼樣才能作到這種效果呢?

刷新頻率爲60Hz的屏幕每16.7ms刷新一次,咱們在屏幕每次刷新前,將圖像的位置向左移動一個像素,即1px。這樣一來,屏幕每次刷出來的圖像位置都比前一個要差1px,所以你會看到圖像在移動;因爲咱們人眼的視覺停留效應,當前位置的圖像停留在大腦的印象還沒消失,緊接着圖像又被移到了下一個位置,所以你纔會看到圖像在流暢的移動,這就是視覺效果上造成的動畫。

setTimeoutsetInterval

理解了上面的概念之後,咱們不難發現,setTimeout 其實就是經過設置一個間隔時間來不斷的改變圖像的位置,從而達到動畫效果的。但咱們會發現,利用seTimeout實現的動畫在某些配置較低的機器上會出現卡頓、抖動的現象。 這種現象的產生有兩個緣由:

  • setTimeout的執行時間並非肯定的。在Javascript中, setTimeout 任務被放進了異步隊列中,只有當主線程上的任務執行完之後,纔會去檢查該隊列裏的任務是否須要開始執行,所以 setTimeout 的實際執行時間通常要比其設定的時間晚一些。
  • 刷新頻率受屏幕分辨率和屏幕尺寸的影響,所以不一樣設備的屏幕刷新頻率可能會不一樣,而 setTimeout 只能設置一個固定的時間間隔,這個時間不必定和屏幕的刷新時間相同。

以上兩種狀況都會致使setTimeout的執行步調和屏幕的刷新步調不一致,從而引發丟幀現象。 那爲何步調不一致就會引發丟幀呢?

  • 第0ms: 屏幕未刷新,等待中,setTimeout也未執行,等待中;

  • 第10ms: 屏幕未刷新,等待中,setTimeout開始執行並設置圖像屬性left=1px;

  • 第16.7ms: 屏幕開始刷新,屏幕上的圖像向左移動了1px, setTimeout 未執行,繼續等待中;

  • 第20ms: 屏幕未刷新,等待中,setTimeout開始執行並設置left=2px;

  • 第30ms: 屏幕未刷新,等待中,setTimeout開始執行並設置left=3px;

  • 第33.4ms:屏幕開始刷新,屏幕上的圖像向左移動了3px, setTimeout未執行,繼續等待中;

從上面的繪製過程當中能夠看出,屏幕沒有更新left=2px的那一幀畫面,圖像直接從1px的位置跳到了3px的的位置,這就是丟幀現象,這種現象就會引發動畫卡頓。

requestAnimationFrame

setTimeout相比,requestAnimationFrame最大的優點是由系統來決定回調函數的執行時機。具體一點講,若是屏幕刷新率是60Hz,那麼回調函數就每16.7ms被執行一次,若是刷新率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms,換句話說就是,requestAnimationFrame的步伐跟着系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。

除此以外,requestAnimationFrame還有如下兩個優點:

  • CPU節能:使用setTimeout實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後臺執行動畫任務,因爲此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,徹底是浪費CPU資源。而requestAnimationFrame則徹底不一樣,當頁面處理未激活的狀態下,該頁面的屏幕刷新任務也會被系統暫停,所以跟着系統步伐走的requestAnimationFrame也會中止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了CPU開銷。

  • 函數節流:在高頻率事件(resize,scroll等)中,爲了防止在一個刷新間隔內發生屢次函數執行,使用requestAnimationFrame可保證每一個刷新間隔內,函數只被執行一次,這樣既能保證流暢性,也能更好的節省函數執行的開銷。一個刷新間隔內函數執行屢次時沒有意義的,由於顯示器每16.7ms刷新一次,屢次繪製並不會在屏幕上體現出來。

Web Worker

因爲 JavaScript 是單線程的,遇到大量計算問題會使整個頁面卡住,形成頁面十分卡頓的感受,在許多狀況下,能夠將純計算工做移到 Web Worker,例如,若是它不須要 DOM 訪問權限。數據操做或遍歷(例如排序或搜索)每每很適合這種模型,加載和模型生成也是如此。

可是,因爲 Web Worker 不能訪問 DOM,若是您的工做必須在主線程上執行,請考慮一種批量方法,將大型任務分割爲微任務,每一個微任務所佔時間不超過幾毫秒,而且在每幀的 requestAnimationFrame 處理程序內運行,而且,您將須要使用進度或活動指示器來確保用戶知道任務正在被處理,從而有助於主線程始終對用戶交互做出快速響應。

避免微優化 JavaScript

我知道許多人對優化有着極致的追求,可能一個函數比另一個函數快上 10 倍,好比請求元素的 offsetTop 比計算 getBoundingClientRect() 要快,可是,每幀調用這類函數的次數幾乎老是不多,通常只能節省零點幾毫秒的時間。

固然我並非說這樣作很差,可是這花費的精力和得到的提高相比起來很不值得,也就是說,花費了大力氣修改,可能界面毫無變化,還會破壞代碼的結構性,我建議,代碼結構性和穩定性的重要性遠遠大於微優化。

Style(樣式計算)

你們都清楚重排和重繪這兩個詞,改變 DOM 結構就會致使瀏覽器從新計算元素樣式,在不少狀況下還會對整個頁面或頁面的一部分進行佈局(即自動重排)。這就是所謂的樣式的計算。

計算樣式實際上分爲兩個步驟:

  1. 建立一組匹配選擇器(瀏覽器計算出給指定元素應用哪些類、僞選擇器和 ID)
  2. 從匹配選擇器中獲取全部樣式規則,並計算出此元素的最終樣式

用於計算某元素計算樣式的時間中大約有 50% 用來匹配選擇器,而另外一半時間用於從匹配的規則中構建

這一節其實沒什麼好寫的,其實就兩點須要注意一下:

  • 下降選擇器的複雜性
  • 減小必須計算其樣式的元素數量

下降選擇器的複雜性

例如:

.box:nth-last-child(-n+1) .title {
  /* styles */
}

這個 class ,瀏覽器會查找這是否爲有 title 類的元素,其父元素剛好是負第 N 個子元素加上 1 個帶 box 類的元素? 計算此結果可能須要大量時間,具體取決於所用的選擇器和相應的瀏覽器。 改成這樣可能會更好:

.final-box-title {
  /* styles */
}

固然,有些樣式必不可免會使用到第一種寫法,可是我建議,儘可能少用這種寫法。

舉個具體的栗子:

這是頁面上的元素:

<div class="box"></div>
<div class="box"></div>
<div class="box b-3"></div>

這是寫的 css 選擇器

.box:nth-child(3)
.box .b-3

查找的元素越多,查找的花費時間越多。 若是 .box:nth-child(3) 花費時間是 2ms, .box .b-3 花費時間是 1ms,若是有 100 個元素,.box:nth-child(3) 花費 200ms,.box .b-3 花費 100ms,時間差距就出來了。

整體來講,計算元素的計算樣式的最糟糕的開銷狀況是元素數量乘以選擇器數量,由於須要對照每一個樣式對每一個元素都檢查至少一次,看它是否匹配。

因此請儘可能減小無效的 class,可能寫在頁面上不會形成任何影響,可是這會給瀏覽器形成負擔,因此我建議使用 BEM 命名規範。

建議使用 BEM

BEM(塊、元素、修飾符)之類的編碼方法實際上歸入了上述選擇器匹配的性能優點,由於它建議全部元素都有單個類,而且在須要層次結構時也歸入了類的名稱:

.list { }
.list__list-item { }

若是須要一些修飾符,像在上面咱們想爲最後一個子元素作一些特別的東西,就能夠按以下方式添加:

.list__list-item--last-child {}

sass 則能夠更好的組織 BEM

.list {
  &__list-item {
    &--last-child {}
  }
}

Layout(佈局)

佈局的過程是上就是重排的過程,重排幾乎將整個頁面從新計算佈局,開銷之大顯而易見。 DOM 的數量以及複雜性將影響到性能。

如下幾點建議可讓咱們優化佈局:

  • 儘量避免佈局操做
  • 使用 flexbox 而不是浮動佈局
  • 避免強制同步佈局
  • 避免佈局抖動

避免強制同步佈局

強制同步佈局(Forced Synchronous Layout),發生的緣由在於在 JavaScript 代碼階段觸發了 Layout 部分的 CSS 屬性。 例如:讀取某個元素的 offsetWidth 值,就會強迫瀏覽器在此幀就必須更新,瀏覽器會當即計算樣式和佈局,而後更新視圖,此刻,瀏覽器會進入讀取數據/寫入數據的循環中。 用一張圖表示:

Forced Synchronous Layout

因爲比較抽象,來舉個具體的栗子:

栗子 1:

divs.forEach(function(elem, index, arr) {
  if (window.scrollY < 200) {
    element.style.opacity = 0.5;
  }
});

讀取 window.scrollY 值會形成 Layout,接着設置透明度,會形成瀏覽器 讀取(讀取 scrollY)/ 寫入(opacity),forEach 致使瀏覽器會一直循環進行這種強制同步操做。 修改以下:

const positionY = window.scrollY;

divs.forEach(function(elem, index, arr) {
  if (positionY < 200) {
    element.style.opacity = 0.5;
  }
});

先預讀取 scrollY 的值,再進行循環寫入操做。

栗子 2:

divs.forEach(function(elem, index, arr) {
  if (elem.offsetHeight < 500) {
    elem.style.maxHeight = '100vh';
  }
});

同上一個問題,讀取 offsetHeight,寫入 maxHeight

修改以下:

if (elem.offsetHeight < 500) { // 先讀取屬性值
  divs.forEach(function(elem, index, arr) { // 再更新樣式
    elem.style.maxHeight = '100vh';
  });
}

栗子 3:

var newWidth = container.offsetWidth;

divs.forEach(function(elem, index, arr) {
  element.style.width = newWidth;
});

這個栗子是正確的,沒問題。

避免佈局抖動

不斷的強制同步會致使佈局抖動。

下面一個栗子,點擊 click 以後將藍色寬度設置爲和綠色相同:

Layout Thrashing Demo

代碼以下:

const paragraphs = document.querySelectorAll('p');
const clickme = document.getElementById('clickme');
const greenBlock = document.getElementById('block');

clickme.onclick = function(){
  greenBlock.style.width = '600px';

  for (let p = 0; p < paragraphs.length; p++) {
    let blockWidth = greenBlock.offsetWidth;
    paragraphs[p].style.width = `${blockWidth}px`;
  }
};

你們看看有什麼問題?

問題就在於,循環讀取了 greenBlock.offsetWidth,致使瀏覽器不斷進行樣式計算和佈局計算,將此刻的值賦予 paragraphs[p].style.width,強迫在此幀得到更新值,並作樣式更新,至關於到 Layout 步驟取值,而後打斷,下一個循環繼續。

Layout Thrashing

Forced Reflow is a likely performance bottleneck.

Details 上能夠看到「Forced reflow is a likely performance bottleneck.」

解決方法很簡單:

clickme.onclick = function(){
  greenBlock.style.width = '600px';
  const blockWidth = greenBlock.offsetWidth;

  for (let p = 0; p < paragraphs.length; p++) {
    paragraphs[p].style.width = `${blockWidth}px`;
  }
};

提早取出 greenBlock.offsetWidth,而後再批量寫入。

Paint(繪製)

繪製是填充像素的過程,像素最終合成到用戶的屏幕上。它每每是管道中運行時間最長的任務。

這過程當中其實什麼好說的,就如下幾點須要注意一下:

  • transformopacity 屬性以外,更改任何屬性始終都會觸發繪製。
  • 繪製一般是像素管道中開銷最大的部分
  • 經過層(z-index)的提高和動畫的編排來減小繪製區域

Composite(合成)

合成是像素管道的最後一環,合成是將頁面的已繪製部分放在一塊兒以在屏幕上顯示的過程。

此方面有兩個關鍵因素影響頁面的性能:須要管理的合成器層數量,以及您用於動畫的屬性。

  • z-index 層數過多會佔用更多的內存,請合理分配
  • 堅持使用 transformopacity 屬性更改來實現動畫,這不會觸發重排和重繪。
  • 使用 will-changetranslateZ 提高移動的元素。

總結

從整篇文章看,Paint(繪製) 和 Composite(合成)是說的最少的內容,由於這一部分僅僅是須要注意的點。

總有人問,從 JavaScriptCSS 哪一個入手,性能會更好一點?

其實從像素管道的角度看,改變 Layout 的成本是比較高的,不管你是使用 JavaScript 仍是 CSS

在編寫 JavaScript 代碼時,不經意間可能會形成強制同步和佈局抖動。

其實完成一個項目的優化不是刻意進行的,而是在一點一滴編碼過程當中積累進行的,使優化成爲你的習慣,寫出的代碼天然就有了優化的內容。

原文出處:https://www.cnblogs.com/liangyin/p/11270371.html

相關文章
相關標籤/搜索