天天看點

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

來源:Alibaba F2E公衆号

作者:甄子

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

遇到性能優化的問題,大抵都會從兩個方向入手:行業标準優化手段、實際性能瓶頸問題。既能夠從先進的模式、方法、套路吸收借鑒,又能夠結合實際性能情況設計應對方案,已然是高品質工作的路徑了,但是,路上還充斥着各種判斷和選擇,稍有不慎還是會泥足深陷,走上“要你命 3000 ”的道路。

在這條“要你命 3000”的道路上,大體可以分為三個部分:感覺、愈合、防腐。所謂“感覺”,是指對于性能的問題是否能夠發現和了解?所謂“愈合”,是指對于發現和了解的性能問題是否能解決?所謂“防腐”,是指性能問題解決之後是否會在未來劣化?這三個部分已經有非常豐富的時間經驗、文章和理論,這裡不再贅述,我想嘗試從全局和系統化的角度,去分享一下我對“渲染性能優化的本質”之愚見,并嘗試提出一條從底層原理出發的路徑,在渲染性能優化方向上,面對紛繁複雜的問題時,有更加精準和明确的依據和更有價值的方案。

硬體視角

廣義上看性能的本質是:在體驗、處理能力和功耗三個方向上找到平衡點。我這樣定義的靈感是來自于硬體晶片設計,晶片設計從硬體工程視角對晶片要求:面積、性能、功耗三個方向上找到平衡點。在使用 FPGA、CPLD 進行晶片設計和驗證的時候,邏輯門數量受到晶片生産工藝和面積的限制而産生一個總體的面積限制,這時候,要使用一些邏輯門組合成專用電路(所謂 IP)進而提升性能并降低功耗(專用電路功耗小于軟體加通用電路),否則,隻能設計成通用電路如:寄存器操作、通用處理指令……等。是以,可以看出,在條件允許的情況下,專用電路也就是所謂“專業化”能夠提供更好的的面積、性能、功耗比。

有同學會問:那這些專業化的電路使用率小于通用化電路的時候怎麼辦?确實,如此一來性能就會很差,這就是為何 M1 可以幫 Apple 做到行業第一的原因,因為 Apple 從軟體生态到作業系統、從底層系統 API 到硬體驅動、從驅動到硬體電子電路設計時全局規劃的,這種全局規劃保證了軟體調用最頻繁的系統程式硬體化,在提高性能功耗比的同時也保證了專業化電路的使用率,這就是我所謂的:硬體視角。

又有同學問:你這個是 Apple 的體系 Android 又不能用。确實,Android 作為一個開源生态系統不如 Apple 的封閉系統那麼精練、簡潔、一緻,但是,隻要有心,還是可以從 Android 的開源生态系統裡找到蛛絲馬迹,把這些開源生态系統的能力按照:軟體生态到作業系統内、從底層系統 API 到硬體驅動、從驅動到硬體提供的專業電子電路的路徑梳理出來,在根據這個路徑對應到軟體工程上,從硬體視角全局性的看待性能優化問題,充分利用底層硬體的能力。

當初在負責國際浏覽器的時候,因為兩印和東南亞的基礎設施建設落後,移動網絡條件非常差帶寬感人,多媒體時代的移動網際網路充斥着圖檔和視訊,我和團隊便一起研究超分辨率(一種用機器學習算法模型預測圖像的技術實作傳統插值無法達到的 240p 到 720p 超分辨率轉換),希望能夠給兩印和東南亞的 UC 浏覽器使用者帶來更好的體驗。

模型和算法在團隊的努力下很快就有了突破,我們已經解決了大部分模型預測帶來的圖像顯示錯誤問題,但整個模型所要求的算力是兩印和東南亞移動端裝置所無法支撐的,即便我們用盡了降低精度、模型壓縮剪枝、知識蒸餾等手段,在紅米這種中低端機型(低端機型定義為 1G 記憶體的 ARMV7 架構處理器)上仍然隻有幾幀每秒的速度,根本不可用。

于是,我們把目光投向了 ARM NEON 指令優化:一種并行浮點計算和向量運算加速指令集。由于使用的是開源的 XNN 架構,我們有能力對 OP 進行 NEON 的定向優化,進而提升算法模型的前向運算速度,降低對記憶體和 CPU 的壓力,曆時近一年的努力,終于做到了每秒 24 幀的 240p 到 720p 超分辨率能力,部署在 UC Browser 上服務兩印和東南亞網友。

雖然,之前在軟體工程裡也經常接觸彙編指令優化,但這次從軟體(算法模型)到系統 API、從驅動到硬體電子電路的全局優化經曆,讓我真正感受到硬體視角的重要性。

那麼,你可能會問:這和前端有什麼關系?我嘗試舉個例子來诠釋他們之間的相似性。首先,前端包含了渲染和計算兩個部分。渲染部分由 HTML 和 CSS 共同定義,之後,交由浏覽器進行渲染,是以,浏覽器确實屏蔽了大部分連接配接到底層能力的部分,造成前端缺乏抓手。但是,随着 WebGL、WebGPU 等全新 API 暴露,前端還是有一些抓手的。計算部分由 JavaScript 定義,之後,交由腳本引擎執行,是以,腳本引擎屏蔽了大部分連接配接到底層能力的部分,造成前端缺乏抓手,同時,腳本引擎基本都用虛拟機屏蔽底層指令集和硬體差異,是以,又多了一層屏蔽。但是,随着 Node.js 和 WASM 等技術讓部分程式 Local 化本地執行,随着 V8 引擎用一些特殊政策讓部分 JavaScript 被 Sparkplug 編譯成 Local 代碼執行,這種情況也會有所改變。(參考:

https://v8.dev/blog/sparkplug

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

摘自 V8 團隊部落格 Sparkplug 介紹

是以,前端已經在很多場景中穿透 浏覽器/webview 更直接的和底層硬體打交道,硬體視角将成為:在體驗、處理能力和功耗三個方向上找到平衡點的關鍵,接下來,将針對渲染、計算這兩個渲染性能優化的核心場景分别進行介紹。

渲染視角

其實,在上文中 HTML 和 CSS 定義了渲染的說法過于粗糙,精确的表述應該是:HTML 和 CSS 定義了渲染的内容,而 JavaScript 可以幹預内容和渲染過程。沒有 JavaScript 的幹預,HTML 和 CSS 定義的渲染就是靜态 HTML 頁面的渲染(由于動畫和視訊等比較特殊,不在本文讨論),在 DHTML 和 XSLT 銷聲匿迹後,動态渲染更多是由 JavaScript 完成的,這展現了解耦後各司其職的優勢,同時,把動态渲染能力從簡單的 API 調用提升到程式設計語言的複雜邏輯控制,釋放無限的可能性。綜上所述,我把渲染視角拆分為:渲染内容、渲染過程、JavaScript 幹預三個部分,分别闡釋一下我的了解。

渲染内容

首先,WWW 的出現将人類從資訊孤島推向了互聯互通的網際網路時代,而資訊的載體就是 Web 上的 HTML 俗稱:超文本标記語言,彼時,将 HTML 渲染出來的核心是排版引擎,Web 标準也是圍繞着排版展開的。随着技術發展,人們對于排版出來的靜态内容逐漸疲勞,DHTML 和 XHTML(XML + XSLT)等技術和進階的 API 如:HTTPWebRequest ……帶來了動态能力,Flash、Silverlight、JavaAplate ……等技術帶來了富用戶端能力,随着 Web 2.0 去中心化打破門戶的壟斷,整個網際網路産業帶來了空前的繁榮。

随着行業的發展和技術的進步,渲染内容從最初的“文檔排版”承載簡單資訊,到“富媒體”承載多媒體資訊,再到今天的增強現實技術 WebXR 承載複雜的數字和現實混合資訊,渲染内容對 渲染引擎、顯示能力、硬體加速能力都提出了不同的要求。最簡單的說,任何一個引擎都會把 Animation 相關 API 單獨拎出來,進而區分這種高負載的渲染工作,在架構和底層引擎上進行特殊優化。

此外,在顯示能力上渲染内容的不同也會産生差異,最常見是分辨率、HDR……等内容對顯示能力有特殊的要求。硬體加速則更容易了解,針對不同負載的渲染工作,首先降低 CPU 、記憶體、磁盤 I/O 的壓力,其次是更多用 GPU 、DSP 等專業電子電路進行替代,進而達到更高的性能/功耗比。

了解這些是為了區分軟體工程裡建構内容,這些内容的選擇對渲染性能有決定性的影響,這種差異又受限于底層的硬體如:CPU、GPU、DSP、專業加速電路……等,是以,他們是從内容解析開始,到底層硬體加速能力,渾然一體且密切相關的。即便在浏覽器引擎的屏蔽下缺少直接控制的能力,對于選擇何種内容的表達方式(端應用裡 UI 控件和 Draw API 選擇、Web 裡 HTML 标記和 CSS 樣式選擇對 Paint 的影響……等)都需要有穿透到底層的視角和了解力,才能在“要你命 3000 ”的基礎上描繪一條更接近問題本質的優化路徑。

拿端應用裡 UI 控件和 Draw API 選擇來說,内容對 API 選擇有很大的限制。2016年剛帶浏覽器團隊的時候,我研讀了 Servo 的源碼,這是一個 Mozilla 和三星共同開發的下一代浏覽器引擎(題外話:因為 Servo 雙方程式員發明了偉大的 Rust 程式設計語言,我非常喜歡并學了很長時間,推薦大家了解一下),Servo 的開源項目裡提供的 Demo 是使用 Android 的 SurfaceView 來保證浏覽器渲染性能。不使用 View 的原因是因為 View 通過系統發出 VSYNC 信号進行重繪,重新整理時間間隔是 16ms ,如果不能再 16ms 完成繪制就會造成界面的卡頓,是以,Servo 選擇了 SurfaceView 來解決這一問題。更深層次去看,其實,本質上是因為 HTML 複雜和動态的内容決定了 View 不合适,可以想見,用 View 不斷局部重新整理将給頁面帶來閃爍,而 SurfaceView 的雙緩沖技術可以把要處理的圖檔在記憶體裡處理好後,在将其顯示在螢幕上,這樣就解決了 Servo 顯示 HTML 内容的問題。同樣的,很多遊戲選擇雙緩沖技術也是因為顯示的内容是“遊戲”。

OpenGL 裡 GPU 有兩種渲染方式:目前螢幕渲染和離屏渲染,光栅化、遮罩、圓角、陰影會觸發離屏渲染,而離屏渲染需要建立獨立的緩沖區、多次切換上下文環境(On-Screen到Off-Screen轉換),最後再把上下文從離屏切換到目前螢幕以顯示離屏緩沖區的渲染結果。所有這些隻是 API 能力,而内容的選擇決定了光栅化、遮罩、圓角、陰影的觸發和性能的耗散,這背後就展現了渲染内容對底層和硬體的影響。

前端和端應用的原理是一樣的,不同的地方在于前端的路徑更長,視角穿透到底層和硬體的難度更大,因為,前端的宿主環境:浏覽器/浏覽器核心/渲染引擎 是在系統的 UI 引擎和渲染引擎之上又包裹了一層。同時,這一層包裹還涉及不同浏覽器廠商在不同平台上的不同實作。但是,随着 WebGL 和 WebGPU……等技術在前端領域的應用,穿透到底層和硬體的難度被這種平行技術能力所簡化和降低了,前端甚至在視角穿透到底層和硬體的同時,對底層和硬體具備了一定的幹預能力,尤其是内容渲染的硬體加速能力,讓渲染内容的設計和實作具備了更寬松的環境。

具備了渲染内容的把控力,實作上就不必贅述了,很簡單的一遠一近兩步即可。所謂“一遠”是指根據業務需求和産品設計把視角穿透到底層和硬體審查技術能力可以帶來什麼新的、有意思的東西。所謂“一近”就是收回視角選擇合适的 UI 控件和 Draw API、HTML 标記和 CSS 去建構要渲染的内容,剩下的就是中間的渲染過程。

渲染過程

從成像原理上說,渲染過程包括:CPU 計算(UI 引擎或浏覽器引擎、渲染引擎的工作)、圖形渲染 API (OpenGL/Metal)、GPU 驅動、GPU渲染、VSYNC 信号發射,HSYNC 信号發射的過程。常見的渲染問題有:卡頓、撕裂、掉幀……,卡頓、撕裂、掉幀通常都是渲染時間過長造成,渲染時間問題大多數情況下是耗費在 CPU 計算上,部分情況下是耗費在圖形渲染上,怎麼了解呢?其實很簡單,把一個渲染性能很差的複雜頁面用高端機流暢渲染過程錄屏,再拿個低端機播放錄屏的視訊并在同一個手機上打開頁面讓浏覽器進行渲染,圖像複雜度沒有差異渲染性能上視訊播放卻比頁面渲染快很多,這就是在展示 CPU、GPU 計算和圖形渲染的耗時,因為視訊的解碼和渲染相較浏覽器引擎更加簡單且渲染過程更短。(特殊編解碼格式和高碼率的視訊除外)

是以,從渲染過程來看,性能優化的本質在首先于降低 CPU、GPU 計算負載。其次,如果有條件(實作差異對業務有影響需要說服業務方)通過不同的渲染内容建構方法去影響渲染過程的話,優先選擇有 CPU、GPU 優化指令和專用電子電路加速的底層 API 來建構渲染内容。例如在 H.264 硬體加速普及的今天,是否應該用 X.265 / H.265 就值得商榷。

在探讨渲染過程的時候,流暢性名額是首先需要關注的,根據 60Hz 重新整理率下 16.6ms 的幀渲染速度,可以從時間角度定義 16.6ms x 2 (雙緩沖)、16.6ms x 3(三緩沖)的 CPU 和 GPU 處理時間,壓縮渲染過程來保證流暢度。

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

OOPD (Out of Process Display Compositor,程序外 Display 合成器),它的主要目的是将 Display Compositor 從 Browser 程序遷移到 Viz 程序(也就是原來的 GPU 程序),Browser 則變成了 Viz 的一個 Client,Renderer 跟 Viz 建立連結(CompositorFrameSink)雖然還是要通過 Browser,但是建立連結後送出 CompositorFrame 就是直接送出給 Viz 了。Browser 同樣也是送出 CompositorFrame 給 Viz,最後在 Viz 生成最終的 CompositorFrame,通過 Display 交由 Renderer 合成輸出。

OOPR (Out of Process Rasterization,程序外光栅化)OOPR 跟目前的 GPU 光栅化機制的主要差別是:

  1. 在目前的 GPU 光栅化機制中,Worker 線程在執行光栅化任務時,會調用 Skia 将 2D 繪圖指令轉換成 GPU 指令,Skia 發出的 GPU 指令通過 Command Buffer 傳送到 Viz 程序的 GPU 線程中執行;
  2. 在 OOPR 中,Worker 線程在執行光栅化任務時,它直接将 2D 繪圖指令(DisplayItemList)序列化到 Command Buffer 傳送到 GPU 線程,運作在 GPU 線程的部分再調用 Skia 去生成對應的 GPU 指令,并交由 GPU 直接執行;

簡而言之,就是将 Skia 光栅化的部分從 Renderer 程序轉移到了 Viz 程序。當 OOPD,OOPR 和 SkiaRenderer 都開啟後:

  1. 光栅化和合成都遷到了 Viz 程序;
  2. 光栅化和合成都使用 Skia 做 2D 繪制,實際上 Chromium 所有的 2D 繪制最終都交由 Skia 來做,由 Skia 生成對應的 GPU 指令;
  3. 光栅化和合成時,Skia 最終輸出 GPU 指令都在 GPU 線程,并且使用同一個 Skia GrContext(Skia 内部的 GPU 繪圖上下文);
渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

這意味着,當 Skia 對 Vulkan,Metal,DX12 等其它 3D API 的支援完善後,Chromium 就可以根據不同的平台和裝置,來決定 Skia 使用哪個 GPU API 來做光栅化和合成。而 Vulkan,Metal,DX12 這些更 Low Level 的 API 對比 GL API,可以帶來更低的 CPU 開銷和更好的性能。

縱觀渲染過程,不同的 Low Level API 受到光栅化過程影響,光栅化過程受到合成器工作過程影響,合成器工作過程受到 Blink 對渲染内容處理的影響:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

參考 Chromium 團隊文檔

對渲染過程感興趣可以看一下這個文檔:Life of a Pixel,對于渲染過程的學習和了解,可以了解渲染内容的不同選擇對于性能的影響過程,對于性能影響過程的分析可以精确定位性能問題,同時,了解渲染過程會産生很多優化手段去對抗白屏、掉坑、閃爍、卡頓……等性能和使用者體驗問題。

上述相對較多的介紹靜态渲染内容,但是,今天的軟體工程面對着複雜的、動态的場景,比如:資料動态加載和動态渲染、條件渲染、動效動畫……等,是以,JavaScript 的幹預也會導緻渲染内容的變化,進而影響渲染的過程,下面,介紹一下 JavaScript 幹預的相關問題。

JavaScript 幹預

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

從原理上說 Blink 暴露 DOM 的 API 給 JavaScript 調用(其實,這裡還有一部分 CSSOM API 的暴露,用于幹預 CSS ,這裡就不贅述了,因為現代前端開發架構大多會直接将這部分幹預結果内聯到 HTML 裡,借此降低浏覽器引擎的計算量),如上圖 createElement 一個 HTML 标記 append 到 document.body.firstChild 的 childNodes[1] 之上,也就是左圖 P2 ,DOM Tree 會發生變化導緻整個渲染過程發生變化:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

這也就是 Virtual-Tree 技術能夠提高浏覽器渲染性能的原理:合并 DOM Tree 的變更進行批量 bindings 進而降低重入渲染過程的幾率和頻次。

簡單說,從 Blink 的視角看 V8 其實是一個局外人,浏覽器引擎良好的解耦了 V8 對 DOM 的幹預,讓這種幹預局限在針對 HTML 标記本身上,但是,由于 JavaScript 的幹預會導緻 DOM 的變化,是以,同樣會導緻後續的渲染過程産生變化,是以,有時候合并 DOM Tree 的變更可能帶來渲染結果的錯誤,在不了解渲染過程的前提下,使用 Virtual-Tree 出現一些渲染的問題可能更加難以定位和解決。

其次,在一些條件渲染或 SPA 應用的一些路由邏輯上,也會因為渲染内容的選擇和改變對渲染過程産生負面的影響,這種影響可能會超出 16.7ms 的限制而造成卡頓等問題,條件和判斷邏輯的優化很可能能夠緩解部分渲染性能問題(由于這屬于 JavaScript 的範疇就不在此展開了),一句話概括:JavaScript 盡快執行和傳回。下文将從 Passer 、Layout 、Compositor 的計算視角去分析:計算複雜度對渲染性能的影響。

計算視角

簡單來說,計算視角就是看:DOM、style、layout、comp.assign、paint(including prepaint)這個渲染過程中計算的部分,因為,這些計算的耗時會直接影響渲染性能。關于這個過程行業裡有一個 CRP 的概念,下面就從 CRP 開始,看看計算視角下渲染性能優化的問題和手段。

CRP 概覽

通過網絡 I/O 或磁盤 I/O (緩存)加載 HTML CSS 之後的鍊路:解碼 HTML、CSS 檔案(GZip 文本傳輸前壓縮等)、處理過程(HTML、CSS Passing)、DOM Tree 建構、Style 内聯、布局、合成器、繪制,這裡涉及浏覽器引擎進行大量的解析和計算等處理過程,為此,需要引入一個概念:關鍵渲染路徑(Critical Rendering Path),簡稱:CRP (以下部分内容摘自大漠老師和我一起梳理的部分内容)

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結
  • 首先,一旦浏覽器得到響應,它就開始解析它。當它遇到一個依賴關系時,它就會嘗試下載下傳它
  • 如果它是一個樣式檔案(CSS檔案),浏覽器就必須在渲染頁面之前完全解析它(這就是為什麼說CSS具有渲染阻礙性)
  • 如果它是一個腳本檔案(JavaScript檔案),浏覽器必須: 停止解析,下載下傳腳本,并運作它。隻有在這之後,它才能繼續解析,因為 JavaScript 腳本可以改變頁面内容(特别是HTML)。(這就是為什麼說JavaScript阻塞解析)
  • 一旦所有的解析工作完成,浏覽器就建立了 DOM 樹和CSSOM樹。将它們結合在一起就得到了渲染樹。
  • 倒數第二步是将渲染樹轉換為布局。這個階段也被稱為 重排
  • 最後一步是繪制。它涉及到根據浏覽器在前幾個階段計算出來的資料對像素進行字面上的着色

把這幾步放到渲染引擎渲染頁面的過程中來,就能更清晰的認識到,CRP 會經過下面幾個過程:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

簡單地說,CRP 的步驟:

  • 處理 HTML 标記并建構 DOM 樹
  • 處理 CSS 标記并建構 CSSOM 樹
  • 将 DOM 樹和 CSSOM 樹合并大一個渲染樹
  • 根據渲染樹來布局
  • 将各個節點繪制到螢幕上

注意:當 DOM 或者 CSSOM 發生變化的時候(JavaScript可以通過 DOM API 和 CSSOM API 對它們進行操作,進而改變頁面視覺效果或内容)浏覽器就需要再次執行上面的步驟,上文介紹的 Virtual-Tree 渲染優化就源自這裡。

優化頁面的關鍵渲染路徑(Critical Rendering Path)三件事:

  • 減少關鍵資源請求數: 減小使用阻塞的資源(CSS 和 JS),注意,并非所有資源是關鍵資源,尤其是 CSS 和 JS(比如使用媒體查詢的 CSS,使用異步的 JS 就不關鍵了)
  • 減少關鍵資源大小:使用各種手段,比如減少、壓縮和緩存關鍵資源,資料量越小,引擎計算複雜度越小
  • 縮短關鍵渲染路徑長度

在具體優化 CRP 時可以按下面的正常步驟進行:

  • 對 CRP 進行分析和特性描述,記錄 關鍵資源數量、關鍵資源大小 和 CRP 長度
  • 最大限度減少關鍵資源的數量:删除它們,延遲它們的下載下傳,将它們标記為異步等
  • 優化關鍵資源位元組數以縮短下載下傳時間(往返次數),減少 CRP 長度
  • 優化其餘關鍵資源的加載順序,需要盡早下載下傳所有關鍵資源,以縮短 CRP 長度

Lighthouse 或 Navigation Timing API 檢測關鍵請求鍊

我們需要一些工具幫助我們檢測 CRP 中一些重要名額,如:關鍵資源數量、關鍵資源大小 、CRP 長度等。在Chrome中使用 Lighthouse插件,Node 版本的 Lighthouse:

// 安裝lighthouse
» npm i -g lighthouse 

» lighthouse https://jhs.m.taobao.com/ --locale=zh-CN --preset=desktop --disable-network-throttling=true --disable-storage-reset=true --view           

可以得到一份詳細報告,在這份報告中可以看到 ”關鍵請求“ 相關的資訊:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

更詳細的可以查閱 Lighthouse 官網文檔(

https://github.com/GoogleChrome/lighthouse)

除了使用 Lightouse 檢測工具之外,還可以使用 Navigation Timing API 來捕獲并記錄任何頁面的真實 CRP 性能。

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

我們也可以使用性能監控規範中相關 API 來對頁面真實使用者場景的性能監控:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

通過相應工具或技術手段獲得 CRP 分析結果之後,就可以針對性的進行優化。

CRP 優化政策

完整内容請等待大漠老師的文章發表,我這邊隻列舉計算視角下相關的部分。

計算視角下的 HTML

  • 編寫有效的可讀的 DOM:
  1. 用小寫字母書寫,每個标簽都應該是小寫的,是以請不要在HTML标簽中使用任何大寫字母
  2. 關閉自我封閉的标簽
  3. 避免過度使用注釋(建議使用相應工具清除注釋)
  4. 組織好DOM,盡量隻建立絕對必要的元素
  • 減少 DOM 元素的數量(在 Blink 核心的 HTMLDocumentParser 進行中 Token 的個數和 DOM 元素數量息息相關,減少 Token 數量可以加快 DOM Tree 的建構,進而加快排版渲染首幀的速度),因為頁面上有過多的 DOM 節點會減慢最初的頁面加載時間、減慢渲染性能,也可能導緻大量的記憶體使用。是以請監控頁面上存在的DOM元素的數量,確定你的頁面沒有:
  1. 沒有超過 1500 個 DOM 節點
  2. DOM節點的嵌套沒有超過 32級
  3. 父節點沒有 60 個以上的子節點

參考:

https://zhuanlan.zhihu.com/p/48524320

《浏覽器核心原理--Chromium Blink Html解析》by 不穿格子衫的程式猿

計算視角下的 CSS

  • CSS類(class)的長度:類名的長度會對 HTML 和 CSS 檔案産生輕微的影響 (在某些場景下存有争議,詳細參閱 CSS 選擇器性能)
  • 關鍵CSS(Critical):将頁面 CSS 分為關鍵 CSS (Critical CSS)和 非關鍵CSS(No-Critical CSS),關鍵CSS 通過 <

    style

    > 方式内聯到頁面中(盡可能壓縮後引用),可以使用 critical 工具來完成(詳細參閱 關鍵CSS)
  • 使用媒體查詢:符合媒體查詢的樣式才會阻塞頁面的渲染,當然所有樣式的下載下傳不會被阻塞,隻是優先級會調低。
  • 避免使用 @import 引入CSS:被

    @import

    引入的 CSS 需要依賴包含的 CSS 被下載下傳與解析完畢才能被發現,增加了關鍵路徑中的往返次數,意味着浏覽器引擎的計算負載加重。
  • 分析樣式表的複雜性:分析樣式表有助于發現有問題的,備援 和重複的 CSS 選擇器。分析出 CSS 中備援和重複 CSS選擇器,可以删除這些代碼,加速 CSS 檔案讀取和加載。可以使用 TestMyCSS、analyze-css、Project Wallace 和 CSS Stats來幫助你分析和更正CSS代碼
//如果結點有id屬性if (element.hasID()) 
  collectMatchingRulesForList(
      matchRequest.ruleSet->idRules(element.idForStyleResolution()),
      cascadeOrder, matchRequest);
//如果結點有class屬性if (element.isStyledElement() && element.hasClass()) { 
  for (size_t i = 0; i < element.classNames().size(); ++i)
    collectMatchingRulesForList(
        matchRequest.ruleSet->classRules(element.classNames()[i]),
        cascadeOrder, matchRequest);
}
//僞類的處理...
//标簽選擇器處理collectMatchingRulesForList(
    matchRequest.ruleSet->tagRules(element.localNameForSelectorMatching()),
    cascadeOrder, matchRequest);
//最後是通配符...           

可以通過代碼,直覺的感受不同 CSS 選擇器帶來的計算開銷差異,進而對優化計算性能提供指導。參考:

https://nextfe.com/how-chrome-compute-css/

《從Chrome源碼看浏覽器如何計算CSS》by 李銀城

優化計算本身

浏覽器引擎本身也是軟體,在了解了渲染過程後,其實就了解了渲染的軟體細節。那麼,軟體工程角度優化計算的方法其實是比較豐富的,如果把程式設計對象了解為:算法 + 資料結構 這個理論相信大家都熟悉,我想說的是:算法 + 資料結構 從性能優化視角下可以看做:時間 + 空間。由此,可以引入一個比較常用的性能優化政策:時間換空間 or 空間換時間。當空間壓力大的時候(也就是存儲壓力大),可以用時間來換空間,典型的案例是:檔案壓縮。當時間壓力比較大的時候(也就是計算壓力大),可以用空間來換時間,典型案例是上文說到的:緩沖區(緩存),利用把長程序運算密集型任務分割成一系列可并發的子任務,并行計算後存儲起來(緩沖區/緩存),再由 GPU 輸出到 Display 上。

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

以下,通過 Layout 和 Compositing 的執行個體,分析一下計算視角下渲染性能的問題。

Layout 執行個體

除了 CRP 前文所述的 HTML 、CSS 的部分優化思路外(解決下圖 DOM、style 部分),渲染流水線包括:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

圖中紅框部分 CPU 和 GPU 渲染流水線耗時部分,就是渲染流水線優化方向。

核心思路:減少渲染流水線計算負載

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

不同的 HTML 标記和 CSS 樣式選擇,以及我們在其中使用的布局方法,會在不經意間,對布局引擎産生計算負載。避免這種負載的産生,就是讓布局引擎盡可能少的計算,可以用前文所述空間換時間的方法,用确定性的數值去避免布局引擎的估計或計算。參考:

https://www.chromium.org/developers/design-documents/multi-column-layout

注意,這裡提供的不是具體的渲染性能優化方法,而是思路。是以,在具體項目裡使用這個思路,還需要額外的工作,包括:調試能力、統計分析能力……,最常用的就是對 Chromium 核心進行調試,找到計算負載的根源。以打開一個頁面為例,我們把斷點打在blink中,源碼為 third_party/blink/renderer/core/dom/

http://document_init.cc

中的 DocumentInit::Type DocumentInit::ComputeDocumentType

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結
https://zhuanlan.zhihu.com/p/260645423

《Chromium源碼學習(1)—— 建構,調試》by mark-0xg

「 The performance goal of the Blink project is to be able to run web content at 60fps on a mobile phone, which means we have 16ms per frame to handle input, execute scripts, and execute the rendering pipeline for changes done by scripts through style recalculation, render tree building, layout, compositing, painting, and pushing changes to the graphics hardware. So style recalculation can only use a fraction of those 16ms. In order to reach that goal, the brute force solution is not good enough.

At the other end of the scale, you can minimize the number of elements having their style recalculated by storing the set of selectors that will change evaluation for each possible attribute and state change and recalculate computed style for each element that matches at least one of those selectors against the set of the descendants and the sibling forest.

At the time of writing, roughly 50% of the time used to calculate the computed style for an element is used to match selectors, and the other half of the time is used for constructing the RenderStyle (computed style representation) from the matched rules. Matching selectors to figure out exactly which elements need to have the style recalculated and then do a full match is probably too expensive too.

空間換時間部分:

We landed on using what we call descendant invalidation sets to store meta-data about selectors and use those sets in a process called style invalidation to decide which elements need to have their computed styles recalculated. 」

https://docs.google.com/document/d/1vEW86DaeVs4uQzNFI5R-_xS9TcS1Cs_EUsHRSgCHGu8/edit?usp=sharing

《Style Invalidation in Blink》by [email protected]

Compositing 執行個體

核心思路:找到渲染引擎設計者為之努力優化的問題并避免之。

簡單來說,在閱讀 Chromium 等浏覽器核心設計文檔或部落格的時候,經常看到一些設計方案在闡述 Blink 核心團隊遇到的問題和他們解決問題的思路與設想,那麼,如果換個思路來看,避免了解引發這些渲染性能問題的原因,并在自己的項目中避免這些情況的發生,就能夠良好的進行優化。

文章:

https://www.chromium.org/developers/design-documents/impl-side-painting

《Multithreaded Rasterization》

問題:

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

大漠老師提供的示例:

background: url(//www.textures4photoshop.com/tex/thumbs/watercolor-paint-background-free-thumb32.jpg) repeat center center;  background-size: 20%;           
渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

此外,就是研讀一些:

https://www.chromium.org/developers/design-documents/compositor-thread-architecture

《Compositor Thread Architecture》之類的文檔,了解在合成過程中 CPU、GPU、記憶體(空間大小、換入換出、記憶體對齊……等阻滞計算過程的記憶體問題也會産生計算負載,因為這會拉長計算時間)都在發生什麼?什麼情況或條件會産生線程間切換?什麼問題會引發資源競争?從這些問題入手反思:我們交給 Compositor 的是不是最合适的?用這種逆向思維的方式發現計算瓶頸并針對性的優化。同時,這些思想的根源都是很樸素的軟體工程能力和程式設計能力,在無法覺察和了解這些問題的時候,不妨把這些基礎軟體工程能力和程式設計能力補一補,例如看看:《UNIX 網絡程式設計》這樣的著作。(

https://book.douban.com/subject/1500149/和https

://book.douban.com/subject/4886882/)

總結

高品質完成渲染性能優化,用“要你命 3000 ”的方法會有明顯的瓶頸:你無法做的比别人好。隻有層層深入,從:解碼 HTML、CSS 檔案(GZip 文本傳輸前壓縮等)、處理過程(HTML、CSS Passing)、DOM Tree 建構、Style 内聯、布局、合成器、繪制開始,再到 WebGL、Vulkan、Skia 等底層程式設計接口,最後到硬體視角下自己的頁面是如何被渲染出來的。了解的越多、越深越能夠發現更深層次、更有價值的問題,結合自己不斷磨練的程式設計能力和軟體工程能力,就能提出一些屬于自己的解決方案,進而:做的比别人好!

渲染性能優化的全局視角,教你做的比别人好硬體視角渲染視角計算視角總結

繼續閱讀