譯 | 核子可樂、Tina
技術和軟件開發領域存在一種有趣的現象,就是同樣的模式迭起興衰、周而復始。
過去Web非常簡單。URL 指向服務器,服務器將數據混合成 html,然后在瀏覽器上呈現該響應。圍繞這種簡單范式,誕生了各種Javascript框架,以前可能需要數月時間完成的一個應用程序基本功能,現在借助這些框架創建相對復雜的項目卻只需要數小時,我們節省了很多時間,從而可以將更多精力花在業務邏輯和應用程序設計上。
但隨著 Web 不斷地發展,Javascript 失控了。不知何故,我們決定向用戶拋出大量 App,并在使用時發出不斷增加的網絡請求;不知何故,為了生成 html,我們必須使用 JSON,發出數十個網絡請求,丟棄我們在這些請求中獲得的大部分數據,用一個越來越不透明的 JavaScript 框架黑匣子將 JSON 轉換為 html,然后將新的 html 修補到 DOM 中......
難道大家快忘記了我們可以在服務器上渲染 html 嗎?更快、更一致、更接近應用程序的實際狀態,并且不會向用戶設備發送任何不必要的數據?但是如果沒有 Javascript,我們必須在每次操作時重新加載頁面。
現在,有一個新的庫出現了,摒棄了定制化的方法,這就是 htmx。作為 Web 開發未來理念的一種實現,它的原理很簡單:
htmx 出現在 2020 年,創建者 Carson Gross 說 htmx 來源自他于 2013 年研究的一個項目 intercooler.js。2020 年,他重寫了不依賴 jQuery 的 intercooler.js,并將其重命名為 htmx。然后他驚訝的發現 Django 社區迅速并戲劇性地接受了它!
圖片來源:https://lp.jetbrains.com/django-developer-survey-2021-486/
Carson Gross 認為 htmx 設法抓住了開發者對現有 Javascript 框架不滿的浪潮,“這些框架非常復雜,并且經常將 Django 變成一個愚蠢的 JSON 生產者”,而 htmx 與開箱即用的 Django 配合得更好,因為它通過 html 與服務器交互,而 Django 非常擅長生成 html。
對于 htmx 的迅速走紅,Carson Gross 發出了一聲感嘆:這真是“十年窗下無人問,一舉成名天下知(this is another example of a decade-long overnight success)”。
可以肯定的一點是 htmx 絕對能用,單從理論上講,這個方法確實值得稱道。但軟件問題終究要歸結于實踐效果:效果好嗎,能不能給前端開發帶來改善?
在 DjangoCon 2022 上,Contexte 的 David Guillot 演示了他們在真實 SaaS 產品上實現了從 React 到 htmx 的遷移,而且效果非常好,堪稱“一切 htmx 演示之母”(視頻地址:https://www.youtube.com/watch?v=3GObi93tjZI)。
Contexte 的項目開始于 2017 年,其后端相當復雜,前端 UI 也非常豐富,但團隊非常小。所以他們在一開始的時候跟隨潮流選擇了 React 來“構建 API 綁定 SPA、實現客戶端狀態管理、前后端狀態分離”等。但實際應用中,因為 API 設計不當,DOM 樹太深,又需要加載很多信息,導致 UI“非常非常緩慢”。在敏捷開發的要求下,團隊里唯一的 Javascript 專家對項目的復雜性表現得一無所措,因此他們決定試試 htmx。
于是我們決定大膽嘗試,花幾個月時間用簡單的 Django 模板和 htmx 替換掉了 SaaS 產品中已經使用兩年的 React UI。這里我們分享了一些相關經驗,公布各項具體指標,希望能幫同樣關注 htmx 的朋友們找到說服 CTO 的理由!
6. 將 Web 構建時間縮短了 88%(由 40 秒縮短至 5 秒)
7. 首次加載交互時間縮短了 50% 至 60%(由 2 到 6 秒,縮短至 1 到 2 秒)
8. 使用 htmx 時可以配合更大的數據集,超越 React 的處理極限
9. Web 應用程序的內存使用量減少了 46%(由 75 MB 降低至 40 MB)
這些數字令人頗為意外,也反映出 Contexte 應用程序高度契合超媒體的這一客觀結果:這是一款以內容為中心的應用程序,用于顯示大量文本和圖像。很明顯,其他 Web 應用程序在遷移之后恐怕很難有同樣夸張的提升幅度。
但一些開發者仍然相信,大部分應用程序在采用超媒體 /htmx 方法之后,肯定也迎來顯著的改善,至少在部分系統中大受裨益。
可能很多朋友沒有注意,移植本身對團隊結構也有直接影響。在 Contexte 使用 React 的時候,后端與前端之間存在硬性割裂,其中兩位開發者全職管理后端,一位開發者單純管理前端,另有一名開發者負責“全?!薄#ㄟ@里的「全?!?,代表這位開發者能夠輕松接手前端和后端工作,因此能夠在整個「?!股溪毩㈤_發功能。)
而在移植至 htmx 之后,整個團隊全都成了“全?!遍_發人員。于是每位團隊成員都更高效,能夠貢獻出更多價值。這也讓開發變得更有樂趣,因為開發人員自己就能掌握完整功能。最后,轉向 htmx 也讓軟件優化度上了一個臺階,現在開發人員可以在棧內的任意位置進行優化,無需與其他開發者提前協調。
如今,單頁應用(SPA)可謂風靡一時:配合 React、Redux 或 Angular 等庫的 JS 或 TS 密集型前端,已經成為創建 Web 應用程序的主流方式。以一個需要轉譯成 JS 的 SPA 應用為例:
但 htmx 風潮已經襲來,人們開始強調一種“傻瓜客戶端”方法,即由服務器生成 html 本體并發送至客戶端,意味著 UI 事件會被發送至服務器進行處理。
用這個例子進行前后對比,我們就會看到前者涉及的活動部件更多。從客戶端角度出發,后者其實回避了定制化客戶端技術,采取更簡單的方法將原本只作為數據引擎的服務器變成了視圖引擎。
后一種方法被稱為 AJAX(異步 JavaScript 與 XML)。這種簡單思路能夠讓 Web 應用程序獲得更高的響應性體驗,同時消除了糟糕的“回發”(postback,即網頁完全刷新),由此回避了極其低效的“viewstate”等.NET 技術。
htmx 在很多方面都體現出對 AJAX 思路的回歸,最大的區別就是它僅僅作為新的聲明性 html 屬性出現,負責指示觸發條件是什么、要發布到哪個端點等。
另一個得到簡化的元素是物理應用程序的結構與構建管道。因為不再涉及手工編寫 JS,而且整個應用程序都基于服務器,因此不再對 JS 壓縮器、捆綁器和轉譯器做(即時)要求。就連客戶端項目也能解放出來,一切都由 Web 服務器項目負責完成,所有應用程序代碼都在.NET 之上運行。從這個角度來看,這與高度依賴服務器的 Blazor Server 編程模型倒是頗有異曲同工之妙。
技術和軟件開發領域存在一種有趣的現象,就是同樣的模式迭起興衰、周而復始。隨著 SPA 的興起,人們一度以為 AJAX 已經過氣了,但其基本思路如今正卷土重來。這其中當然會有不同的權衡,例如更高的服務器負載和網絡流量(畢竟現在我們發送的是數據視圖,而不只是數據),但能讓開發者多個選擇肯定不是壞事。
雖然不敢確定這種趨勢是否適用于包含豐富用戶體驗的高復雜度應用程序,但毫無疑問,相當一部分 Web 應用程序并不需要完整的 SPA 結構。對于這類用例,簡單的 htmx 應用程序可能就是最好的解決方案。
參考鏈接:
https://news.ycombinator.com/item?id=33218439
https://www.reddit.com/r/django/comments/rxjlc6/htmx_gaining_popularity_rapidly/
https://mekhami.github.io/2021/03/26/htmx-the-future-of-web/
https://www.compositional-it.com/news-blog/more-on-htmx-back-to-the-future/
聲明:本文為InfoQ編譯,未經許可禁止轉載。
多數設備的刷新頻率是60Hz,也就說是瀏覽器對每一幀畫面的渲染工作要在16ms內完成,超出這個時間,頁面的渲染就會出現卡頓現象,影響用戶體驗。前端的用戶體驗給了前端直觀的印象,因此對B/S架構的開發人員來說,熟悉瀏覽器的內部執行原理顯得尤為重要。
瀏覽器大體上由以下幾個組件組成,各個瀏覽器可能有一點不同。
注意:chrome瀏覽器與其他瀏覽器不同,chrome使用多個渲染引擎實例,每個Tab頁一個,即每個Tab都是一個獨立進程。
Chrome瀏覽器使用多個進程來隔離不同的網頁,在Chrome中打開一個網頁相當于起了一個進程,每個tab網頁都有由其獨立的渲染引擎實例。因為如果非多進程的話,如果瀏覽器中的一個tab網頁崩潰,將會導致其他被打開的網頁應用。另外相對于線程,進程之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由于多個線程共享著相同的地址空間和資源,所以會存在線程之間有可能會惡意修改或者獲取非授權數據等復雜的安全問題。
在內核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:
GUI渲染線程負責渲染瀏覽器界面HTML元素,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。在Javascript引擎運行腳本期間,GUI渲染線程都是處于掛起狀態的,也就是說被凍結了.
JS為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JS是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突;如果JS是多線程的話,在多線程的交互下,處于UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那么這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果,當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,JS在最初就選擇了單線程執行。
GUI渲染線程與JS引擎線程互斥的,是由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JavaScript線程和UI線程同時運行),那么渲染線程前后獲得的元素數據就可能不一致。當JavaScript引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。由于GUI渲染線程與JS執行線程是互斥的關系,當瀏覽器在執行JS程序的時候,GUI渲染線程會被保存在一個隊列中,直到JS程序執行完成,才會接著執行。因此如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。
瀏覽器定時計數器并不是由JS引擎計數的, 因為JS引擎是單線程的, 如果處于阻塞線程狀態就會影響記計時的準確, 因此通過單獨線程來計時并觸發定時是更為合理的方案。
當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由于JS的單線程關系所有這些事件都得排隊等待JS引擎處理。
在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到JS引擎的處理隊列中等待處理。
用戶請求的HTML文本(text/html)通過瀏覽器的網絡層到達渲染引擎后,渲染工作開始。每次通常渲染不會超過8K的數據塊,其中基礎的渲染流程圖:
webkit引擎渲染的詳細流程,其他引擎渲染流程稍有不同:
渲染流程有四個主要步驟:
以上步驟是一個漸進的過程,為了提高用戶體驗,渲染引擎試圖盡可能快的把結果顯示給最終用戶。它不會等到所有HTML都被解析完才創建并布局渲染樹。它會在從網絡層獲取文檔內容的同時把已經接收到的局部內容先展示出來。
DOM樹的構建過程是一個深度遍歷過程:當前節點的所有子節點都構建好后才會去構建當前節點的下一個兄弟節點。DOM樹的根節點就是document對象。
DOM樹的生成過程中可能會被CSS和JS的加載執行阻塞,具體可以參見下一章。當HTML文檔解析過程完畢后,瀏覽器繼續進行標記為deferred模式的腳本加載,然后就是整個解析過程的實際結束觸發DOMContentLoaded事件,并在async文檔文檔執行完之后觸發load事件。
生成DOM樹的同時會生成樣式結構體CSSOM(CSS Object Model)Tree,再根據CSSOM和DOM樹構造渲染樹Render Tree,渲染樹包含帶有顏色,尺寸等顯示屬性的矩形,這些矩形的順序與顯示順序基本一致。從MVC的角度來說,可以將Render樹看成是V,DOM樹與CSSOM樹看成是M,C則是具體的調度者,比HTMLDocumentParser等。
可以這么說,沒有DOM樹就沒有Render樹,但是它們之間不是簡單的一對一的關系。Render樹是用于顯示,那不可見的元素當然不會在這棵樹中出現了,譬如 <head>。除此之外,display等于none的也不會被顯示在這棵樹里頭,但是visibility等于hidden的元素是會顯示在這棵樹里頭的。
DOM對象類型很豐富,什么head、title、div,而Render樹相對來說就比較單一了,畢竟它的職責就是為了以后的顯示渲染用嘛。Render樹的每一個節點我們叫它渲染器renderer。
一棵Render樹大概是醬紫,左邊是DOM樹,右邊是Render樹:
從上圖我們可以看出,renderer與DOM元素是相對應的,但并不是一一對應,有些DOM元素沒有對應的renderer,而有些DOM元素卻對應了好幾個renderer,對應多個renderer的情況是普遍存在的,就是為了解決一個renderer描述不清楚如何顯示出來的問題,譬如有下拉列表的select元素,我們就需要三個renderer:一個用于顯示區域,一個用于下拉列表框,還有一個用于按鈕。
另外,renderer與DOM元素的位置也可能是不一樣的。那些添加了 float或者 position:absolute的元素,因為它們脫離了正常的文檔流,構造Render樹的時候會針對它們實際的位置進行構造。
上面確定了renderer的樣式規則后,然后就是重要的顯示元素布局了。當renderer構造出來并添加到Render樹上之后,它并沒有位置跟大小信息,為它確定這些信息的過程,接下來是布局(layout)。
瀏覽器進行頁面布局基本過程是以瀏覽器可見區域為畫布,左上角為 (0,0)基礎坐標,從左到右,從上到下從DOM的根節點開始畫,首先確定顯示元素的大小跟位置,此過程是通過瀏覽器計算出來的,用戶CSS中定義的量未必就是瀏覽器實際采用的量。如果顯示元素有子元素得先去確定子元素的顯示信息。
布局階段輸出的結果稱為box盒模型(width,height,margin,padding,border,left,top,…),盒模型精確表示了每一個元素的位置和大小,并且所有相對度量單位此時都轉化為了絕對單位。
在繪制(painting)階段,渲染引擎會遍歷Render樹,并調用renderer的 paint() 方法,將renderer的內容顯示在屏幕上。繪制工作是使用UI后端組件完成的。
回流(reflow):當瀏覽器發現某個部分發生了點變化影響了布局,需要倒回去重新渲染。reflow 會從 <html>這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的?,F在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質上是元素的顯示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。
重繪(repaint):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。
每次Reflow,Repaint后瀏覽器還需要合并渲染層并輸出到屏幕上。所有的這些都會是動畫卡頓的原因。Reflow 的成本比 Repaint 的成本高得多的多。一個結點的 Reflow 很有可能導致子結點,甚至父點以及同級結點的 Reflow 。在一些高性能的電腦上也許還沒什么,但是如果 Reflow 發生在手機上,那么這個過程是延慢加載和耗電的??梢栽赾sstrigger上查找某個css屬性會觸發什么事件。
reflow與repaint的時機:
在瀏覽器拿到HTML、CSS、JS等外部資源到渲染出頁面的過程,有一個重要的概念關鍵渲染路徑(Critical Rendering Path)。例如為了保障首屏內容的最快速顯示,通常會提到一個漸進式頁面渲染,但是為了漸進式頁面渲染,就需要做資源的拆分,那么以什么粒度拆分、要不要拆分,不同頁面、不同場景策略不同。具體方案的確定既要考慮體驗問題,也要考慮工程問題。了解原理可以讓我們更好的優化關鍵渲染路徑,從而獲得更好的用戶體驗。
現代瀏覽器總是并行加載資源,例如,當 HTML 解析器(HTML Parser)被腳本阻塞時,解析器雖然會停止構建 DOM,但仍會識別該腳本后面的資源,并進行預加載。
同時,由于下面兩點:
存在阻塞的 CSS 資源時,瀏覽器會延遲 JavaScript 的執行和 DOM 構建。另外:
所以,script 標簽的位置很重要。實際使用時,可以遵循下面兩個原則:
下面來看看 CSS 與 JavaScript 是具體如何阻塞資源的。
<style>
p { color: red; }
</style>
<link rel="stylesheet" href="index.css">
這樣的 link 標簽(無論是否 inline)會被視為阻塞渲染的資源,瀏覽器會優先處理這些 CSS 資源,直至 CSSOM 構建完畢。
渲染樹(Render-Tree)的關鍵渲染路徑中,要求同時具有 DOM 和 CSSOM,之后才會構建渲染樹。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因為包括我們希望顯示的文本在內的內容,都在 DOM 中存放,那么可以從 CSS 上想辦法。
最容易想到的當然是精簡 CSS 并盡快提供它。除此之外,還可以用媒體類型(media type)和媒體查詢(media query)來解除對渲染的阻塞。
<link href="index.css" rel="stylesheet">
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">
第一個資源會加載并阻塞。第二個資源設置了媒體類型,會加載但不會阻塞,print 聲明只在打印網頁時使用。第三個資源提供了媒體查詢,會在符合條件時阻塞渲染。
關于CSS加載的阻塞情況:
沒有js的理想情況下,html與css會并行解析,分別生成DOM與CSSOM,然后合并成Render Tree,進入Rendering Pipeline;但如果有js,css加載會阻塞后面js語句的執行,而(同步)js腳本執行會阻塞其后的DOM解析(所以通常會把css放在頭部,js放在body尾)
JavaScript 的情況比 CSS 要更復雜一些。如果沒有 defer 或 async,瀏覽器會立即加載并執行指定的腳本,“立即”指的是在渲染該 script 標簽之下的HTML元素之前,也就是說不等待后續載入的HTML元素,讀到就加載并執行。觀察下面的代碼:
<p>Do not go gentle into that good night,</p>
<script>console.log("inline1")</script>
<p>Old age should burn and rave at close of day;</p>
<script src="app.js"></script>
<p>Rage, rage against the dying of the light.</p>
<script src="app.js"></script>
<p>Old age should burn and rave at close of day;</p>
<script>console.log("inline2")</script>
<p>Rage, rage against the dying of the light.</p>
這里的 script 標簽會阻塞 HTML 解析,無論是不是 inline-script。上面的 P 標簽會從上到下解析,這個過程會被兩段 JavaScript 分別打斷一次(加載、執行)。
解析過程中無論遇到的JavaScript是內聯還是外鏈,只要瀏覽器遇到 script 標記,喚醒 JavaScript解析器,就會進行暫停 (blocked )瀏覽器解析HTML,并等到 CSSOM 構建完畢,才去執行js腳本。因為腳本中可能會操作DOM元素,而如果在加載執行腳本的時候DOM元素并沒有被解析,腳本就會因為DOM元素沒有生成取不到響應元素,所以實際工程中,我們常常將資源放到文檔底部。
defer 與 async 可以改變之前的那些阻塞情形,這兩個屬性都會使 script 異步加載,然而執行的時機是不一樣的。注意 async 與 defer 屬性對于 inline-script 都是無效的,所以下面這個示例中三個 script 標簽的代碼會從上到下依次執行。
<script async>
console.log("1")
</script>
<script defer>
console.log("2")
</script>
<script>
console.log("3")
</script>
上面腳本會按需輸出 1 2 3,故,下面兩節討論的內容都是針對設置了 src 屬性的 script 標簽。
先放個熟悉的圖~
藍色線代表網絡讀取,紅色線代表執行時間,這倆都是針對腳本的;綠色線代表 HTML 解析。
<script src="app1.js" defer></script>
<script src="app2.js" defer></script>
<script src="app3.js" defer></script>
defer 屬性表示延遲執行引入 JavaScript,即 JavaScript 加載時 HTML 并未停止解析,這兩個過程是并行的。整個 document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無關),會執行所有由 defer-script 加載的 JavaScript 代碼,再觸發 DOMContentLoaded(初始的 HTML 文檔被完全加載和解析完成之后觸發,無需等待樣式表圖像和子框架的完成加載) 事件 。
defer 不會改變 script 中代碼的執行順序,示例代碼會按照 1、2、3 的順序執行。所以,defer 與相比普通 script,有兩點區別:載入 JavaScript 文件時不阻塞 HTML 的解析,執行階段被放到 HTML 標簽解析完成之后。
async 屬性表示異步執行引入的 JavaScript,與 defer 的區別在于,如果已經加載好,就會開始執行,無論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(HTML解析完成事件)之后。需要注意的是,這種方式加載的 JavaScript 依然會阻塞 load 事件。換句話說,async-script 可能在 DOMContentLoaded 觸發之前或之后執行,但一定在 load 觸發之前執行。
從上一段也能推出,多個 async-script 的執行順序是不確定的,誰先加載完誰執行。值得注意的是,向 document 動態添加 script 標簽時,async 屬性默認是 true。
使用 document.createElement 創建的 script 默認是異步的,示例如下。
console.log(document.createElement("script").async); // true
所以,通過動態添加 script 標簽引入 JavaScript 文件默認是不會阻塞頁面的。如果想同步執行,需要將 async 屬性人為設置為 false。
如果使用 document.createElement 創建 link 標簽會怎樣呢?
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = "index.css";
document.head.appendChild(style); // 阻塞?
其實這只能通過試驗確定,已知的是,Chrome 中已經不會阻塞渲染,Firefox、IE 在以前是阻塞的,現在會怎樣目前不太清楚。
結合渲染流程,可以針對性的優化渲染性能:
這里主要參考Google的瀏覽器渲染性能的基礎講座,想看更詳細內容可以去瞅瞅~
setTimeout(callback)和setInterval(callback)無法保證callback函數的執行時機,很可能在幀結束的時候執行,從而導致丟幀,如下圖:
requestAnimationFrame(callback)可以保證callback函數在每幀動畫開始的時候執行。注意:jQuery3.0.0以前版本的animate函數就是用setTimeout來實現動畫,可以通過jquery-requestAnimationFrame這個補丁來用requestAnimationFrame替代setTimeout
JS代碼運行在瀏覽器的主線程上,與此同時,瀏覽器的主線程還負責樣式計算、布局、繪制的工作,如果JavaScript代碼運行時間過長,就會阻塞其他渲染工作,很可能會導致丟幀。前面提到每幀的渲染應該在16ms內完成,但在動畫過程中,由于已經被占用了不少時間,所以JavaScript代碼運行耗時應該控制在3-4毫秒。如果真的有特別耗時且不操作DOM元素的純計算工作,可以考慮放到Web Workers中執行。
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// 主線程不受Web Workers線程干擾
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = e.data;
// Web Workers線程執行結束 // ...});
由于Web Workers不能操作DOM元素的限制,所以只能做一些純計算的工作,對于很多需要操作DOM元素的邏輯,可以考慮分步處理,把任務分為若干個小任務,每個任務都放到 requestAnimationFrame中回調執行
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var nextTask = taskList.pop();
// 執行小任務
processTask(nextTask);
if (taskList.length > 0) {
requestAnimationFrame(processTaskList);
}
}
打開 ChromeDevTools>Timeline>JSProfile,錄制一次動作,然后分析得到的細節信息,從而發現問題并修復問題。
添加或移除一個DOM元素、修改元素屬性和樣式類、應用動畫效果等操作,都會引起DOM結構的改變,從而導致瀏覽器要repaint或者reflow。那么這里可以采取一些措施。
盡量保持class的簡短,或者使用Web Components框架。
.box:nth-last-child(-n+1) .title {}
// 改善后
.final-box-title {}
由于瀏覽器的優化,現代瀏覽器的樣式計算直接對目標元素執行,而不是對整個頁面執行,所以我們應該盡可能減少需要執行樣式計算的元素的個數。
布局就是計算DOM元素的大小和位置的過程,如果你的頁面中包含很多元素,那么計算這些元素的位置將耗費很長時間。布局的主要消耗在于:1. 需要布局的DOM元素的數量;2. 布局過程的復雜程度
當你修改了元素的屬性之后,瀏覽器將會檢查為了使這個修改生效是否需要重新計算布局以及更新渲染樹,對于DOM元素的幾何屬性修改,比如width/height/left/top等,都需要重新計算布局。對于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看布局的耗時,以及受影響的DOM元素數量。
老的布局模型以相對/絕對/浮動的方式將元素定位到屏幕上,而Floxbox布局模型用流式布局的方式將元素定位到屏幕上。通過一個小實驗可以看出兩種布局模型的性能差距,同樣對1300個元素布局,浮動布局耗時14.3ms,Flexbox布局耗時3.5ms。IE10+支持。
根據渲染流程,JS腳本是在layout之前執行,但是我們可以強制瀏覽器在執行JS腳本之前先執行布局過程,這就是所謂的強制同步布局。
requestAnimationFrame(logBoxHeight);
// 先寫后讀,觸發強制布局
function logBoxHeight() {
// 更新box樣式
box.classList.add('super-big');
// 為了返回box的offersetHeight值
// 瀏覽器必須先應用屬性修改,接著執行布局過程
console.log(box.offsetHeight);
}
// 先讀后寫,避免強制布局
function logBoxHeight() {
// 獲取box.offsetHeight
console.log(box.offsetHeight);
// 更新box樣式
box.classList.add('super-big');
}
在JS腳本運行的時候,它能獲取到的元素樣式屬性值都是上一幀畫面的,都是舊的值。因此,如果你在當前幀獲取屬性之前又對元素節點有改動,那就會導致瀏覽器必須先應用屬性修改,結果執行布局過程,最后再執行JS邏輯。
如果連續快速的多次觸發強制同步布局,那么結果更糟糕。比如下面的例子,獲取box的屬性,設置到paragraphs上,由于每次設置paragraphs都會觸發樣式計算和布局過程,而下一次獲取box的屬性必須等到上一步設置結束之后才能觸發。
function resizeWidth() { // 會讓瀏覽器陷入'讀寫讀寫'循環 for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; }}
// 改善后方案var width = box.offsetWidth;function resizeWidth() { for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = width + 'px'; }}
注意:可以使用FastDOM來確保讀寫操作的安全,從而幫你自動完成讀寫操作的批處理,還能避免意外地觸發強制同步布局或快速連續布局,消除大量操作DOM的時候的布局抖動。
Paint就是填充像素的過程,通常這個過程是整個渲染流程中耗時最長的一環,因此也是最需要避免發生的一環。如果Layout被觸發,那么接下來元素的Paint一定會被觸發。當然純粹改變元素的非幾何屬性,也可能會觸發Paint,比如背景、文字顏色、陰影效果等。
繪制并非總是在內存中的單層畫面里完成的,實際上,瀏覽器在必要時會將一幀畫面繪制成多層畫面,然后將這若干層畫面合并成一張圖片顯示到屏幕上。這種繪制方式的好處是,使用transform來實現移動效果的元素將會被正常繪制,同時不會觸發其他元素的繪制。
瀏覽器會把相鄰區域的渲染任務合并在一起進行,所以需要對動畫效果進行精密設計,以保證各自的繪制區域不會有太多重疊。另外可以實現同樣效果的不同方式,應該采用性能更好的那種。
打開DevTools,在彈出的面板中,選中 MoreTools>Rendering選項卡下的Paint flashing,這樣每當頁面發生繪制的時候,屏幕就會閃現綠色的方框。通過該工具可以檢查Paint發生的區域和時機是不是可以被優化。通過Chrome DevTools中的 Timeline>Paint選項可以查看更細節的Paint信息
使用transform/opacity實現動畫效果,會跳過渲染流程的布局和繪制環節,只做渲染層的合并。
TypeFuncPositiontransform: translate(-px,-px)Scaletransform: scale(-)Rotationtransform: rotate(-deg)Skewtransform: skew(X/Y)(-deg)Matrixtransform: matrix(3d)(..)Opacityopacity: 0-1
使用transform/opacity的元素必須獨占一個渲染層,所以必須提升該元素到單獨的渲染層。
應用動畫效果的元素應該被提升到其自有的渲染層,但不要濫用。在頁面中創建一個新的渲染層最好的方式就是使用CSS屬性will-change,對于目前還不支持will-change屬性、但支持創建渲染層的瀏覽器,可以通過3D transform屬性來強制瀏覽器創建一個新的渲染層。需要注意的是,不要創建過多的渲染層,這意味著新的內存分配和更復雜的層管理。注意,IE11,Edge17都不支持這一屬性。
.moving-element { will-change: transform; transform: translateZ(0);}
盡管提升渲染層看起來很誘人,但不能濫用,因為更多的渲染層意味著更多的額外的內存和管理資源,所以當且僅當需要的時候才為元素創建渲染層。
* { will-change: transform; transform: translateZ(0);}
開啟 Timeline>Paint選項,然后錄制一段時間的操作,選擇單獨的幀,看到每個幀的渲染細節,在ESC彈出框有個Layers選項,可以看到渲染層的細節,有多少渲染層,為何被創建?
用戶輸入事件處理函數會在運行時阻塞幀的渲染,并且會導致額外的布局發生。
理想情況下,當用戶和頁面交互,頁面的渲染層合并線程將接收到這個事件并移動元素。這個響應過程是不需要主線程參與,不會導致JavaScript、布局和繪制過程發生。但是如果被觸摸的元素綁定了輸入事件處理函數,比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數執行完畢才能執行,也就是用戶的滾動頁面操作被阻塞了,表現出的行為就是滾動出現延遲或者卡頓。
簡而言之就是你必須確保用戶輸入事件綁定的任何處理函數都能夠快速的執行完畢,以便騰出時間來讓渲染層合并線程完成他的工作。
輸入事件處理函數,比如scroll/touch事件的處理,都會在requestAnimationFrame之前被調用執行。因此,如果你在上述輸入事件的處理函數中做了修改樣式屬性的操作,那么這些操作就會被瀏覽器暫存起來,然后在調用requestAnimationFrame的時候,如果你在一開始就做了讀取樣式屬性的操作,那么將會觸發瀏覽器的強制同步布局操作。
通過requestAnimationFrame可以對樣式修改操作去抖動,同時也可以使你的事件處理函數變得更輕
家好,很高興又見面了,我是"高級前端?進階?",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!
Free JavaScript form builder library with integration for React, Angular, Vue, jQuery, and Knockout.
SurveyJS 表單庫是一個免費的 MIT 許可客戶端組件,允許開發者在任何 JavaScript 應用程序中渲染基于 JSON 的動態表單、收集響應并將所有表單數據提交到目標數據庫。
開發者可以將 SurveyJS 用于任意長度和復雜程度的多頁表單、彈出式調查、測驗、計分調查、計算器表單等。SurveyJS 表單庫對 React、Angular、Vue 和 Knockout 等框架都具有原生支持,jQuery 通過 Knockout 版本的包裝器也能獲得支持。
SurveyJS 產品系列還包括一個強大的表單構建器庫,可自動生成 JSON 格式的表單配置文件。表單構建器具有拖放式 UI、CSS 主題編輯器和用于條件邏輯和表單分支的 GUI。
目前 SurveyJS 在 Github 通過 MIT 協議開源,有超過 4k 的 star、1k 的 fork、代碼貢獻者 150+、妥妥的前端優質開源項目。
SurveyJS Form Library for React 由兩個 npm 包組成:survey-core(獨立于平臺的代碼)和 survey-react-ui(渲染代碼)。首先需要運行以下命令安裝 survey-react-ui,survey-core 包將作為依賴項自動安裝。
npm install survey-react-ui --save
SurveyJS 表單庫附帶了幾個預定義主題(如下所示)以及基于 CSS 變量的靈活主題自定義機制。
import 'survey-core/defaultV2.min.css';
接著需要指定模型,以下模型架構聲明了兩個文本問題,每個問題都有一個標題和一個名稱。標題顯示在屏幕上,名稱用于在代碼中識別問題。
const surveyJson = {
elements: [{
name: "FirstName",
title: "Enter your first name:",
type: "text"
}, {
name: "LastName",
title: "Enter your last name:",
type: "text"
}]
};
接著可以導入 Survey 組件并渲染表單,將其添加到模板中:
// Uncomment the following line if you are using Next.js:
// 'use client'
import {Survey} from 'survey-react-ui';
// ...
const surveyJson = {...};
function App() {
const survey = new Model(surveyJson);
return <Survey model={survey} />;
}
SurveyJS 與其他框架,比如:Angular、Vue 、 Knockout、jQuery 等的集成也非常簡單,這里不再贅述,可以參考文末資料。
https://github.com/surveyjs/survey-library
https://surveyjs.io/form-library/documentation/get-started-react
https://surveyjs.io/documentation/surveyjs-architecture
*請認真填寫需求信息,我們會在24小時內與您取得聯系。