頁面不僅要快速加載,而且要順暢地運行;滾動應與手指的滑動一樣快,並且動畫和交互應如絲綢般順滑。
目前大多數裝置的螢幕更新率為60次/秒。因此,如果在頁面中有一個動畫或漸變效果,或者用戶正在滾動頁面,那麼瀏覽器渲染動畫或頁面的每一幀的速率也需要跟設備螢幕的刷新率保持一致。
其中每個影格的預算時間僅比 16 毫秒多一點 (1 秒/ 60 = 16.66 毫秒)。但實際上,瀏覽器有整理工作要做,因此所有工作需要在10毫秒內完成。如果無法符合此預算,幀率將下降,並且內容會在螢幕上抖動(judders)。此現象通常稱為卡頓(jank),會對使用者體驗產生負面影響。
像素管道(The pixel pipeline)
在工作時需要了解並注意五個主要區域,這些是擁有最大控制權的部分,也是像素至螢幕管道中的關鍵點:
JavaScript。一般來說,會使用 JavaScript 來實現一些視覺變化的效果。例如用 jQuery 的 animate 函數做一個動畫、對一個資料集進行排序或是往頁面裡加入一些 DOM 元素等。除了 JavaScript,還有其他一些常用方法也可以實現視覺變化效果,例如:CSS Animations、Transitions 和 Web Animation API。
樣式計算Sytle calculations。 This is the process of figuring out which CSS rules apply to which elements based on matching selectors, for example, .headline or .nav > .nav__item. From there, once rules are known, they are are calculated.
佈局。在知道對一個元素應用哪些規則之後,瀏覽器即可開始計算它要佔據的空間大小及其在螢幕的位置。網頁的佈局模式意味著一個元素可能影響其他元素,例如元素的寬度一般會影響其子元素的寬度以及樹中各處的節點,因此對於瀏覽器來說,佈局過程是經常發生的。
繪製。繪製是填充像素的過程。它涉及繪出文字、顏色、圖像、邊框和陰影,包括元素的每個視覺部分。繪製一般是在多個表面(通常稱為層layers)上完成的。
合成。由於頁面的各部分可能被繪製到多層,因此它們需要按正確順序繪製到螢幕上,以便正確渲染頁面。對於與另一個元素重疊的元素來說,這一點特別重要,因為一個錯誤可能會使一個元素錯誤地出現在另一個元素的上層。
管道的每個部分都有機會產生卡頓,因此請務必準確了解程式碼觸發管道的哪些部分。
不一定每幀都總是會經過管道每個部分的處理。實際上,無論是使用JavaScript、CSS 或網頁動畫,在實現視覺變化時,管道針對指定幀的運行通常有三種方式:
1. JS / CSS > 樣式> 佈局> 繪製> 合成
如果修改元素的「layout」屬性,即改變了元素的幾何屬性(例如寬度、高度等),那麼瀏覽器將必須檢查所有其他元素,然後「自動重排」頁面(reflow the page)。任何受影響的部分都需要重新繪製,而且最終繪製的元素需進行合成。
2. JS / CSS > 樣式> 繪製> 合成
#
如果修改「paint only」屬性(例如背景圖片、文字顏色或陰影等),即不會影響頁面佈局的屬性,則瀏覽器會跳過佈局,但仍將執行繪製。
3. JS / CSS > 樣式> 合成
如果更改一個既不用重新佈局也不要重新繪製的屬性,則瀏覽器將只執行合成。這個最後的方式開銷最小,最適合應用生命週期中的高壓力點,例如動畫或滾動。
性能是一種避免執行的藝術,並且使執行的任何操作盡可能高效。 許多情況下,這需要與瀏覽器配合,而不是跟著它對著幹。 值得謹記的是,上面列出的各項管道工作在計算開銷上有所不同,有些任務比其他任務的開銷要大!
JavaScript often triggers visual changes. Sometimes that's directly through style manipulations, and sometimes it's calculations that result in visual changes, like searching or ting 運行時機.的JavaScript 可能是導致效能問題的常見原因,應設法盡可能減少其影響。
JavaScript 效能分析可以說是一門藝術,因為編寫的 JavaScript 程式碼與實際執行的程式碼完全不像。現代瀏覽器使用 JIT 編譯器和各種各樣的最佳化和技巧來實現盡可能快的執行,這極大地改變了程式碼的動態性。
一些幫助應用很好地執行 JavaScript的事情:
對於動畫效果的實現,避免使用 setTimeout 或 setInterval,使用 requestAnimationFrame。
將長時間運行的 JavaScript 從主執行緒移到 Web Worker。
使用小任務來執行對多個訊框的 DOM 變更。
使用 Chrome DevTools 的 Timeline 和 JavaScript 分析器來評估 JavaScript 的影響。
當螢幕正在發生視覺變化時,最好在幀的開頭執行操作。保證 JavaScript 在訊框開始時運作的唯一方式是使用 requestAnimationFrame。
/** * If run as a requestAnimationFrame callback, this * will be run at the start of the frame. */function updateScreen(time) { // Make visual updates here.} requestAnimationFrame(updateScreen);
框架或範例可能使用 setTimeout 或 setInterval 來執行動畫之類的視覺變化,但這種做法的問題是,回呼函數在影格中的某個時點運行,可能剛好在幀的末尾,而這經常會使我們丟失幀,導致卡頓。 (composite等js的運作需要時間,會阻塞UI更新)。
事實上,jQuery 目前的預設 animate 行為是使用 setTimeout!強烈建議打上修補程式使用 requestAnimationFrame。
JavaScript 在瀏覽器的主執行緒上運行,恰好與樣式計算、佈局以及許多情況下的繪製一起運行。如果 JavaScript 運行時間過長,就會阻塞這些其他工作,可能會導致幀丟失。
因此,要妥善處理 JavaScript 何時運行以及運行多久。例如,如果在滾動之類的動畫中,最好是想辦法使 JavaScript 保持在3-4毫秒的範圍內。超過此範圍,就可能要佔用太多時間。如果在空閒期間,則不必那麼斤斤計較所佔的時間。
在許多情況下,可以將純計算工作移到 Web Worker,例如,不需要DOM 存取權限,資料操作或遍歷(例如排序或搜尋),往往很適合這種模型,載入和模型生成也是如此。
var dataSortWorker = new Worker("sort-worker.js?1.1.11"); dataSortWorker.postMesssage(dataToSort);// The main thread is now free to continue working on other things...dataSortWorker.addEventListener('message', function(evt) { var sortedData = evt.data; // Update data on screen...});
並非所有工作都適合此模型:Web Worker 沒有 DOM 存取權。如果操作必須在主執行緒上執行,可以考慮一種批次方法,將大型任務分割為小任務,每個小任務所佔時間不超過幾毫秒,並且在每個影格的 requestAnimationFrame 處理程序內執行。
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList);function processTaskList(taskStartTime) { var taskFinishTime; do {// Assume the next task is pushed onto a stack.var nextTask = taskList.pop();// Process nextTask. processTask(nextTask);// Go again if there’s enough time to do the next task.taskFinishTime = window.performance.now(); } while (taskFinishTime - taskStartTime < 3); if (taskList.length > 0) requestAnimationFrame(processTaskList); }
此方法會產生 UX 和 UI 後果,您將需要使用進度或活動指示器來確保使用者知道任務正在處理中。在任何情況下,此方法都不會佔用應用程式的主線程,這有助於主線程始終對使用者互動作出快速回應。
在評估一個框架、函式庫或自己的程式碼時,請務必逐幀評估執行 JavaScript 程式碼的開銷。當執行性能關鍵的動畫工作(例如變換或滾動)時,這一點尤其重要。
測量 JavaScript 開銷和效能情況的最佳方法是使用 Chrome DevTools。通常,您將獲得以下的簡單記錄:
The Main section provides a flame chart of JavaScript calls so you can analyze exactly which functions were called and how long each took.
如果發現有長時間運行的JavaScript,則可以在DevTools 使用者介面的頂部啟用JavaScript 分析器:
##以這種方式分析JavaScript 會產生開銷,因此一定只在想要更深入了解JavaScript 執行時間特性時才啟用它。啟用此複選框後,現在可以執行相同的操作,您將獲得有關JavaScript 中調用了哪些函數的更多資訊:
##有了這些資訊之後,就可以評估JavaScript 對應用程式效能的影響,並開始找出並修正函數運行時間過長的熱點(hotspots)。如前所述,應設法移除長時間運行的 JavaScript,或者若無法移除,則將其移到 Web Worker 中,騰出主執行緒繼續執行其他任務。
如果您開發的是遊戲或計算開銷很大的應用,則可能屬於本指南的例外情況,因為您一般會將大量計算放入單個幀,在這種情況下各種方法都很有用。
簡而言之,慎用微優化,因為它們通常不會對應到您正在建立的應用程式類型。 2/8法則,先從瓶頸著手優化。
縮小樣式計算的範圍並降低其複雜性計算樣式的第一部分是建立一組匹配選擇器,這實質上是瀏覽器計算出給指定元素應用哪些classes, pseudo-selectors and IDs。
第二部分涉及從符合選擇器中取得所有樣式規則,並計算出此元素的最終樣式。在 Blink(Chrome 和 Opera 的渲染引擎)中,這些過程的開銷至少在目前是大致相同的:
Roughly 50% of the time used to calculate the computed style for an element is used to match selectors,而另一半時間用於從匹配的規則中構建 RenderStyle(computed style representation)。
在最簡單的情況下,在CSS 中只有一個類別的元素:
.title {
/* styles */
}
但是,隨著專案的成長,將可能產生更複雜的CSS,最終的選擇器可能變成這樣:
.box:nth-last-child(-n+1) .title {
/* styles */
}
為了知道是否需要應用樣式,瀏覽器實際上必須詢問「這是否為有title 類別的元素,其父元素恰好是負第N 個子元素加上1 個有box 類別的元素?特定的選擇器可以更改為一個類別:
.final-box-title {
/* styles */
}
開發者可能對該類別的名稱有疑問,但此工作對於瀏覽器而言要簡單得多。在上一版本中,為了知道該元素是否為其類型的最後一個,瀏覽器首先必須知道關於其他元素的所有情況,以及其後面是否有任何元素會是第N 個最後子元素,這比簡單地將類別選擇器與元素相符的開銷要大得多。
產生render tree時,對於每個DOM元素,必須在所有Style Rules中找到符合的 selector 並將對應的樣式規則合併。
css選擇器的解析是從右往左的,這樣公共樣式就在CSSOM樹的父節點上,更具體的樣式(選擇器更具體)會在子節點上,節點分支和遍歷次數都會變少。如果採用left-to-right 的方式讀取css規則,那麼大多數規則讀到最後才會發現是不匹配,做了很多無用功;而採取right-to-left 的方式,只要發現最右邊選擇器不匹配,就直接捨棄,避免很多無效匹配。
另一個效能考慮,在元素變更時需要計算的工作量對於許多樣式更新而言是更重要的因素。
In general terms, the worst case cost of calculating the computed style of elements is the number of elements multiplied by the selector count, because each element needs to be at least checked once against every style rule to see if it matches.
註:以前曾經是這樣:如果改變了(例如)body 元素上的一個類,則該頁的所有子元素將需要重新計算其計算樣式。現在有點不一樣:對於更改時會導致重新計算樣式的元素,某些瀏覽器會維護一小組每個這種元素獨有的規則。這意味著,根據元素在樹中的位置以及所改變的特定屬性,元素不一定需要重新計算。
樣式計算可能經常是直接針對少量目標元素,而不是聲明整個頁面無效。在現代瀏覽器中,這往往不再是個問題,因為瀏覽器不一定需要檢查一項更改可能影響的所有元素。另一方面,較早的瀏覽器不一定針對此類任務進行了最佳化。應盡可能減少宣告為無效的元素的數量。
附註:如果您熱衷於網頁元件,有一點值得注意,樣式計算在這方面稍有不同,因為預設樣式不會跨越Shadow DOM 的邊界,並且範圍限於單一元件,而不是整個樹。但是,整體來看,同樣的概念仍然適用:規則簡單的小樹比規則複雜的大樹會更有效率地處理。
測量樣式重新計算的開銷
測量樣式重新計算的最簡單、最好的方法是使用 Chrome DevTools 的 Timeline 模式。首先,開啟 DevTools,前往 Timeline 選項卡,選取記錄並與您的網站互動。停止記錄後,將看到下圖所示情況。
頂部的條表示每秒幀數,如果看到柱形超過較低的線,即 60fps 線,則存在長時間運行的幀。
如果一些滾動之類的互動或其他互動時出現長時間運行的幀,則應進一步檢視。
如果出現較大的紫色區塊,如上例所示,請點選記錄以了解更多細節。
在這次抓取中,有一個長時間運行的重新計算樣式事件,其時間剛好超過18 毫秒,並且恰好發生在在滾動期間,導致使用者體驗到明顯的抖動。
如果點擊事件本身,將看到一個呼叫堆疊,精確指出了您的 JavaScript 中導致觸發樣式變更的位置。此外,也獲得樣式受變更影響的元素數量(本例中剛好超過 400 個元素),以及執行樣式計算所花費的時間。您可以使用此資訊來開始嘗試在程式碼中尋找修正點。
BEM的編碼方法實際上納入了上述選擇器匹配的性能優勢,因為它建議所有元素都有單個類,並且在需要層次結構時也納入了類的名稱:
.list { }
.list__list-item { }
如果需要一些修飾符,像在上面我們想為最後一個子元素做一些特別的東西,就可以如下新增:
.list__list-item--last-child {}
如果您在尋找一種好方法來組織您的 CSS,則 BEM 真的是個很好的起點,不僅從結構的角度如此,還因為樣式查找得到了簡化。
佈局是瀏覽器計算各元素幾何資訊的過程:元素的大小以及在頁面中的位置。 根據所使用的 CSS、元素的內容或父級元素,每個元素都會有明確或隱含的大小資訊。此過程在 Chrome、Opera、Safari 和 Internet Explorer 中稱為佈局 (Layout)。 在 Firefox 中稱為自動重排 (Reflow),但實際上其過程是一樣的。
與樣式計算相似,佈局開銷的直接考慮因素如下:
#需要佈局的元素數量。
這些佈局的複雜度。
佈局的作用範圍一般為整個文件。
DOM 元素的數量將影響效能,應盡可能避免觸發佈局。
評估版面模型的效能;新版 Flexbox比舊版 Flexbox 或基於浮動的版面模型更快。
Avoid forced synchronous layouts and layout thrashing; read style values then make style changes.
當變更樣式時,瀏覽器會檢查變更是否需要計算佈局,以及是否需要更新渲染樹。對「幾何屬性」#(如寬度、高度、左側或頂部)的變更都需要佈局計算。
.box {
width: 20px;
height: 20px;
}
/**改變 寬度和高度觸發佈局。 */
.box--expanded {
width: 200px;
height: 350px;
}
佈局幾乎總是作用到整個文件。如果有大量元素,將需要很長時間來計算所有元素的位置和尺寸。
如果無法避免佈局,關鍵還是要使用 Chrome DevTools 來查看佈局要花多長時間,並確定佈局是否是造成瓶頸的原因。首先,開啟 DevTools,選擇「Timeline」標籤,點擊「record」按鈕,然後與您的網站互動。當您停止記錄時,您將看到網站表現情況的詳細分析:
#在仔細研究上例中的框架時,我們看到超過20 毫秒用在佈局上,當我們在動畫中設定16 毫秒來取得螢幕上的畫面時,此佈局時間太長。您也可以看到,DevTools 將說明樹的大小(本例中為 1618 個元素)以及需要佈局的節點數。
網頁有各種佈局模型,有些模式比其他模式受到更廣泛的支援。最早的 CSS 佈局模型使我們能夠在螢幕上對元素進行相對、絕對定位或透過浮動元素定位。
下面的螢幕截圖顯示了在 1,300 個框上使用浮動的佈局開銷。當然,這是一個人為的例子,因為大多數應用將使用各種手段來定位元素。
如果我們更新此範例以使用Flexbox(Web 平台的新模型),則出現不同的情況:
現在,對於相同數量的元素和相同的視覺外觀,佈局的時間要少得多(本例中為分別3.5 毫秒和14 毫秒)。務必記住,對於某些情況,可能無法選擇Flexbox,因為它沒有浮動那麼受支持,但是在可能的情況下,至少應研究佈局模型對網站性能的影響,並且採用最大程度減少網頁執行開銷的模型。
在任何情況下,無論是否選擇 Flexbox,都應當在應用的高壓力點期間嘗試完全避免觸發佈局!
将一帧送到屏幕会采用如下顺序:
首先 JavaScript 运行,然后计算样式,然后布局。但是,JavaScript 在更改元素样式后,获取其几何属性的值,此时会强制浏览器应用新样式提前执行布局,值后才能获取几何属性值。这被称为强制同步布局(forced synchronous layout)。
要记住的第一件事是,在 JavaScript 运行时,来自上一帧的所有旧布局值是已知的,并且可供您查询。因此,如果(例如)您要在帧的开头写出一个元素(让我们称其为“框”)的高度,可能编写一些如下代码:
// Schedule our function to run at the start of the frame.requestAnimationFrame(logBoxHeight);function logBoxHeight() { // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); }
如果在请求此框的高度之前,已更改其样式,就会出现问题:
function logBoxHeight() { box.classList.add('super-big'); //样式更改后,浏览器必须先应用新的样式(重绘)之后才能获取当前的值,有时是多做无用功 // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); }
现在,为了获得框的高度,浏览器必须先应用样式更改(由于增加了 super-big 类),然后运行布局,这时才能返回正确的高度。这是不必要的,并且可能开销很大。
因此,始终应先批量读取样式并执行(浏览器可以使用上一帧的布局值),然后执行任何赋值操作。
以上函数应为:
function logBoxHeight() { // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); box.classList.add('super-big'); }
大部分情况下,并不需要先应用新样式然后查询值,使用上一帧的值就足够了。与浏览器同步(或比其提前)运行样式计算和布局可能成为瓶颈。
有一种方式会使强制同步布局更糟:连续执行大量这种强制布局。如下:
function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; } }
此代码循环处理一组段落,并设置每个段落的宽度以匹配一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),所以它必须应用更改的样式,然后运行布局。每次迭代都将出现此问题!
此示例的修正方法还是先读取值,然后写入值:
// Read.var width = box.offsetWidth;function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) {// Now write.paragraphs[i].style.width = width + 'px'; } }
如果要保证安全,应当查看 FastDOM,它会自动批处理读取和写入,应当能防止意外触发强制同步布局或布局抖动。
绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是管道中运行时间最长的任务,应尽可能避免此任务。
除 transform 或 opacity 属性之外,更改任何属性始终都会触发绘制。
绘制通常是像素管道中开销最大的部分,应尽可能避免绘制。
通过layer promotion和动画的编排来减少绘制区域。
使用 Chrome DevTools paint profile来评估绘制的复杂性和开销;应尽可能降低复杂性并减少开销。
如果触发布局,则总是会触发绘制,因为更改任何元素的几何属性意味着其像素需要修正!
如果更改非几何属性,例如背景、文本或阴影,也可能触发绘制。在这些情况下,不需要布局,并且管道将如下所示:
您可以使用 Chrome DevTools 來快速確定正在繪製的區域。開啟 DevTools,按下鍵盤上的 Esc 鍵。在出現的面板中,前往“rendering”標籤,然後選取“Show paint rectangles”。
開啟此選項後,每次發生繪製時,Chrome 將讓螢幕閃爍綠色。如果看到整個螢幕閃爍綠色,或看到不應繪製的螢幕區域,則應進一步研究。
Chrome DevTools Timeline 中有一個選項提供更多資訊:繪製分析器。若要啟用此選項,請前往 Timeline,然後選取頂部的「Paint」方塊。需要注意的是,請務必僅在嘗試分析繪製問題時才開啟此選項,因為它會產生開銷,並且會影響效能分析結果。最好是在想要更深入了解具體繪製內容時使用。
完成了上述設定之後,現在可以執行 Timeline 錄製,並且繪製記錄將包含更多的細節。透過點擊一幀的繪製記錄,您將進入該幀的繪製分析器:
#點擊繪製分析器將調出視圖,您可以查看所繪製的元素、所花的時間,以及所需的各個繪製調用:
此分析器顯示區域和複雜性(實際上就是繪製所花的時間),如果不能選擇避免繪製,這兩個都是可以設法修正的面向。
繪製並非總是繪製到記憶體中的單一影像。事實上,在必要時瀏覽器可以繪製到多個圖像或合成層(compositor layers)。
此方法的優點是,定期重繪的或透過變形在螢幕上移動的元素,可以在不影響其他元素的情況下進行處理。 Sketch、GIMP 或 Photoshop 之類的藝術檔案也是如此,各個層可以在彼此的上面處理並合成,以創建最終圖像。
建立新圖層的最佳方式是使用 will-change CSS 屬性。此方法在Chrome、Opera 和Firefox 上有效,並且透過 transform 的值將建立一個新的合成器層:
.moving-element {
will-change: transform;
}
對於不支援 will-change 但受益於圖層建立的瀏覽器,例如Safari 和Mobile Safari,需要使用3D 變形來強制建立一個新圖層:
.moving-element {
transform: translateZ(0);
}
但要注意的是:不要建立太多層,因為每層都需要記憶體和管理開銷。
如果已將一個元素提升到一個新層,可使用 DevTools 確認這樣做已帶來效能優勢。請勿在不分析的情況下提升元素。
然而,雖然提升元素,卻仍需要繪製工作。繪製問題的一個大挑戰是,瀏覽器將兩個需要繪製的區域聯合在一起,而這可能導致整個螢幕重繪。因此,如果頁面頂層有一個固定標頭,而在螢幕底部還有正在繪製的元素,則整個螢幕可能最終要重繪。
減少繪製區域往往是編排動畫和變換,使其不過多重疊,或設法避免對頁面的某些部分設定動畫。
在談到繪製時,有些繪製比其他繪製的開銷更大。例如,繪製任何涉及模糊(例如陰影)的元素所花的時間將比(例如)繪製一個紅框的時間要長。但是,對 CSS 而言,這一點並不總是很明顯:background: red; 和 box-shadow: 0, 4px, 4px, rgba(0,0,0,0.5); 看起來不一定有截然不同的性能特性,但確實很不相同。
利用上述繪圖分析器,您可以確定是否需要尋求其他方式來實現效果。問問自己,是否可能使用一組開銷較小的樣式或替代方式來實現最終結果。
您要盡可能的避免繪製的發生,特別是在動畫效果中。因為每幀10毫秒的時間預算一般來說是不足以完成繪製工作的,尤其是在行動裝置上。
以上是瀏覽器渲染效能優化的詳細內容。更多資訊請關注PHP中文網其他相關文章!