新時代的 SSR 框架破局者:qwik
作者簡介
19組清風(fēng),攜程資深前端開發(fā)工程師,負(fù)責(zé)商旅前端公共基礎(chǔ)平臺建設(shè),關(guān)注NodeJs、研發(fā)效能領(lǐng)域。
引言
今天這篇文章中和大家聊一聊號稱世界上第一個 O(1) 的 JavaScript SSR 框架:qwik。
別擔(dān)心,如果你不是特別了解 SSR 也沒關(guān)系,文章大概會從以下幾個方面作為切入點(diǎn):
- 首先會圍繞對比 SSR 與 SPA 各自的優(yōu)劣勢,從而展開 SSR 的運(yùn)行機(jī)制以及 SSR 相較于 SPA 究竟為了解決什么問題。
- 之后,會根據(jù) NextJs 的運(yùn)行機(jī)制思考針對目前主流 SSR 框架設(shè)計思路上存在的不足從而引出 qwik 為何會在眾多成熟框架中脫穎而出。
- 最后,會針對于 qwik 提出自己的看法以及聊聊目前 qwik 存在的“問題”。
諸如社區(qū)內(nèi)部 SSR 框架其實(shí)已經(jīng)產(chǎn)生了非常優(yōu)秀的作品,比如大名鼎鼎的 NextJS 以及新興勢力代表的 Remix 和 isLands 架構(gòu)的 Astro、Fresh 等等優(yōu)秀框架。
為何 qwik 可以在眾多老牌優(yōu)秀框架中脫穎而出。接下來,讓我們一起來一探究竟吧。
一、SSR & CSR
目前業(yè)內(nèi)存在非常多基于 SSR 的優(yōu)秀框架,比如 Next、Remix、Nuxt 等等。
針對于 Qwik 我們先來聊聊基于 Next 體系的傳統(tǒng) SSR 方案。
1.1 Client Side Rendering
在開始 SSR 之前我們先來聊聊它的對立面,所謂的 CSR(Client Side Rendering)。
服務(wù)器端渲染 (SSR) 是一種在服務(wù)器中進(jìn)行渲染 HTML 而不是由瀏覽器中執(zhí)行 JS 獲得網(wǎng)頁(SPA)的技術(shù)。
目前國內(nèi)社區(qū)中主流框架比如 VueJs、React 等嚴(yán)格意義上來說都是基于 CSR(Client Side Rendering) 的產(chǎn)物。
所謂 CSR 的意味著當(dāng)發(fā)出一個請求時,服務(wù)器會返回一個空的 HTML 頁面以及對應(yīng)的 JavaScript 腳本。
比如:
當(dāng)瀏覽器下載完成對應(yīng)的 JS 腳本后才會動態(tài)執(zhí)行對應(yīng)的 JS 腳本然后在返回的 HTML 頁面上進(jìn)行渲染頁面內(nèi)容。
你可以簡單的理解為上述的 ./index.js 會在客戶端下載完成后執(zhí)行該腳本,從而執(zhí)行 document.getElementById('root').innerHTML = '...' 來進(jìn)行頁面渲染。
這種方式并不是從服務(wù)端下發(fā)的 HTML 文件來進(jìn)行渲染頁面,相反而是通過瀏覽器獲取到服務(wù)端下發(fā) HTML 中的所有的 JS 文件后執(zhí)行 JS 代碼從而在客戶端通過腳本進(jìn)行頁面渲染。
以及通常在 CSR 中當(dāng)我們點(diǎn)擊任何頁面中的導(dǎo)航鏈接并不會向服務(wù)端發(fā)起請求,而是通過下載的 JS 腳本中的路由模塊(比如 ReactRouter、VueRouter 這樣的模塊)重新執(zhí)行 JS 來處理頁面跳轉(zhuǎn)從而進(jìn)行頁面重新渲染。
上面的概念是非常典型的 CSR ,瀏覽器僅僅接受一個用作網(wǎng)頁容器的 HTML 頁面,這樣的方式通常也被稱為單頁面應(yīng)用 (SPA)。
1)優(yōu)勢
那么上述我們提到的 CSR 廣泛存在于目前大量頁面中,必然存在它自己的優(yōu)勢。
在頁面初始化訪問后加載速度極快且響應(yīng)非常迅速。在頁面初始化后,網(wǎng)站所有的 HTML 內(nèi)容都是在客戶端通過執(zhí)行 JS 生成,并不需要再次請求服務(wù)器即可重新渲染 HTML 。
此外,有關(guān)任何實(shí)時的數(shù)據(jù)獲取都可以通過 AJAX 請求對于頁面進(jìn)行局部更新從而刷新頁面。
2)劣勢
可是,CSR 真的有那么完美嗎。任何一件技術(shù)方案一定存在它的兩面性,我們來看看 CSR 方式究竟存在哪些問題:
- 初始加載時間長。
首次請求完服務(wù)器獲取到 HTML 頁面后,初始化的頁面仍然需要在一段時間內(nèi)處于白屏狀態(tài)。
在初始渲染之前,瀏覽器必須等待 HTML 頁面中的所有 Javascript 腳本加載完成并且執(zhí)行完畢,此時頁面才會進(jìn)行真正的渲染。
當(dāng)然,使用代碼拆分或延遲加載等多種方案可以有效的減少上述的問題。但是這些方式始終是治標(biāo)不治本,因?yàn)樗]有從本質(zhì)上解決 CSR 存在的問題。
- SEO(搜索引擎優(yōu)化) 的負(fù)面影響。
上邊我們提到過,所謂 CSR 本質(zhì)上首先會返回一個空的 HTML 頁面,所以這也就造成了在搜索引擎對于該頁面的數(shù)據(jù)爬取中會認(rèn)為它是一個空頁面。從而影響對應(yīng)的搜索結(jié)果排名。
雖然說在最新的 Google 中已經(jīng)可以觸發(fā)執(zhí)行 JS 對于網(wǎng)站進(jìn)行關(guān)鍵字排名,但是在 JS 體積足夠大的時候針對于 SEO 仍然是存在一部分問題導(dǎo)致無法解析出正確的關(guān)鍵字匹配。
當(dāng)然 CSR 還存在一些其他方面的缺點(diǎn),比如網(wǎng)站強(qiáng)依賴于 JS 當(dāng)用戶禁用 JS 時網(wǎng)站只能是白屏展現(xiàn)給用戶等等之類。
1.2 Server Side Render
簡單聊完客戶端渲染后,我們稍微來看看所謂的服務(wù)端渲染是什么含義。
基于舊時代的類似 Java 的 JSP 頁面我在這里就不贅述了,顯然 JSP 的方式每個 HTML 都需要單獨(dú)請求服務(wù)器返回對應(yīng)的 HTML 內(nèi)容嚴(yán)格意義上來說這也是 SSR 的方式但是很明顯這已經(jīng)被時代淘汰了。
目前國內(nèi)各家公司廣泛應(yīng)用的服務(wù)端渲染技術(shù)大概的思路是這樣的(Next 的 SSR 模式也是同樣的思路):
當(dāng)用戶首次訪問你的應(yīng)用站點(diǎn)時:
- 首先服務(wù)器會根據(jù)對應(yīng)的 URL 在服務(wù)端根據(jù)對應(yīng)路徑渲染對應(yīng)的 HTML 模版。注意這里渲染的 HTML 模版是具有該頁面真正的內(nèi)容。同時它并不具備任何交互邏輯(比如 DOM 元素的點(diǎn)擊事件),這是一份完全的靜態(tài)站點(diǎn)。
- 服務(wù)器會下發(fā)這份僅具有靜態(tài)內(nèi)容的 HTML 模版,同時這份模版中也會包含對應(yīng)的 JavaScript 執(zhí)行腳本。第一時間會展示給用戶對應(yīng)的 HTML 頁面,此時對于訪問站點(diǎn)的用戶來說首屏渲染相較于 SPA 應(yīng)用來說會非常快。因?yàn)樗⒉恍枰诳蛻舳藶g覽器上再次下載和執(zhí)行 JavaScript 腳本來進(jìn)行頁面渲染。
- 其次,針對于 SEO 的優(yōu)化也會非常良好,因?yàn)榉?wù)器上下發(fā)的 HTML 頁面是包含當(dāng)前站點(diǎn)的真實(shí) HTML 結(jié)構(gòu),對于搜索引擎的爬蟲來說會非常容易的匹配到當(dāng)前關(guān)鍵字。
- 之后,瀏覽器會下載當(dāng)前這份 HTML 的 JS 腳本。
- 因?yàn)槭紫瘸尸F(xiàn)給用戶的一份靜態(tài)的 HTML 頁面,并不具備任何交互效果。我們需要為頁面上的元素增加對應(yīng)交互,HTML 頁面中的 JS 腳本中會包含網(wǎng)站的交互邏輯。
- 最后,當(dāng)下載完 HTML 腳本中的 JS 腳本后,自然會執(zhí)行這些 script 腳本。從而發(fā)生一種被稱為 # hydrate(水合) 的方式,從而為頁面上靜態(tài) HTML 元素再次添加對應(yīng)的事件處理從而保證頁面具有交互性。
當(dāng) hydration 過程完成后,會由我們的客戶端框架接管網(wǎng)站的后續(xù)渲染。在后續(xù)的導(dǎo)航鏈接跳轉(zhuǎn)和頁面渲染中和服務(wù)器已經(jīng)沒有任何關(guān)系了,我們完全可以利用客戶端的路由切換(History Api/Hash Api)利用 JS 進(jìn)行頁面渲染從而保證切換頁面不用再次請求瀏覽器保證非常及時的頁面交互。
1)hydration
上述過程中有一個非常重要的關(guān)鍵字 hydration(水合)。
首次訪問頁面時,頁面的靜態(tài) HTML 是在服務(wù)端生成的。在服務(wù)端我們將生成的靜態(tài) HTML 以及 HTML 中攜帶的 JS 腳本發(fā)送到客戶端。
此時靜態(tài) HTML 會立即顯示在用戶視野中,然后瀏覽器會利用網(wǎng)絡(luò)進(jìn)程下載當(dāng)前 HTML 腳本中的 JS 腳本。
當(dāng) JS 腳本下載完成后,會立即執(zhí)行同時發(fā)生一種被稱為 hydration 的過程。
所謂的 hydration 簡單來說,也就是客戶端下載完成 JS 腳本后,瀏覽器會執(zhí)行下載的 JS 腳本這些腳本中有部分內(nèi)容會將已經(jīng)存在的 HTML 內(nèi)容通過執(zhí)行下載的 JS 腳本添加上對應(yīng)的事件監(jiān)聽器從而保證頁面的交互。
注意,在 React、Vue 中 hydration 并不意味著重新渲染。因?yàn)樵?Server 端已經(jīng)渲染了和 Client 完全相同的 DOM 結(jié)構(gòu)所以完全沒有必要在此重新渲染。
所以 hydration 的過程是給當(dāng)前頁面中已經(jīng)生成的 HTML 頁面添加上對應(yīng)的事件監(jiān)聽器。
這也是為什么在 Next 等框架中為什么必須要保證 Server 端和 Client 的渲染 HTML 結(jié)構(gòu)必須一致的原因。
比如我們以 Next 舉例來說(Vue 也是同樣的道理):
- 當(dāng)用戶訪問 ?www.trip.biz? 時,服務(wù)端接收到請求調(diào)用 ReactDOMServer.renderToString() 生成當(dāng)前頁面的 HTML 靜態(tài)結(jié)構(gòu)。
- 服務(wù)器會下發(fā)這個 HTML 頁面給客戶端,同時這個 HTML 頁面上也會攜帶一部分 JS 腳本 script 標(biāo)簽。
- 用戶的瀏覽器中會立即展現(xiàn)到該 HTML 頁面,同時也會下載對應(yīng) JS 腳本并執(zhí)行。
- 當(dāng) JS 腳本執(zhí)行完畢后,客戶端會調(diào)用 ReactDOM.hydrate() 發(fā)生水合為當(dāng)前頁面的 HTML 頁面添加事件交互處理,同時后續(xù)由 JS 接管頁面的跳轉(zhuǎn)渲染。
針對于第一步 Next 中存在 ??Automatic Static Optimization?? 的優(yōu)化,并不一定會在每次訪問時調(diào)用 renderToString 方法,有可能在構(gòu)建時也會直接生成對應(yīng)的 HTML 模版。
當(dāng)然,在最新的 Next 版本中已經(jīng)支持了Stream以及 Server Components。
整個過程就像是這張圖中的樣子:
2)優(yōu)勢
簡單聊過了所謂 SSR 的原理后,如果你有認(rèn)真看上述的內(nèi)容。其實(shí)我相信相較于 CSR ,SSR 這種方式的好處不言而喻:
- 更好的搜索引擎優(yōu)化 SEO 方式,HTML 模板是從服務(wù)端直接下發(fā)這也就導(dǎo)致搜索引擎爬蟲中更多的關(guān)鍵字匹配。
- 更快的首屏渲染,因?yàn)橄噍^于 SPA 它少了在 Client 中下載和執(zhí)行 JS 腳本后渲染的過程。
- 頁面不需要 JS 也可以正常渲染,雖然沒有 JS 意味著頁面失去了可交互性。但對于禁用 JS 的用戶來說,展示一些靜態(tài)內(nèi)容總比 SPA 應(yīng)用的白屏來的更加友好一些對吧。
3)劣勢
當(dāng)然,任何技術(shù)方案在不同場景下也存在它自己的不足。
- 強(qiáng)依賴于服務(wù)。
針對于 CSR 的方式它是一種純靜態(tài)資源。我們可以直接將它放在 CDN 上就可以良好的用戶訪問到,而 SSR 的方式必須依賴于一個服務(wù)器進(jìn)行服務(wù)端預(yù)渲染。(當(dāng)然純 SSG 應(yīng)用我們不在這個討論范圍之內(nèi))
同時,有服務(wù)的地方就存在并發(fā)壓力。當(dāng)你需要為你的應(yīng)用考慮服務(wù)端渲染的方式時,一定不要忘記為你的服務(wù)器進(jìn)行壓測。 - ??Time to Interactive?? 可交互時間 (TTI) 的增長,雖然說 SSR 的方式有效的縮短了首屏加載的方式,但是會增加所謂的TTI(可交互時間)。
所謂的 TTI 指標(biāo)測量頁面從開始加載到主要子資源完成渲染,并能夠快速、可靠地響應(yīng)用戶輸入所需的時間。
因?yàn)?SSR 的方式在用戶訪問時會下發(fā)當(dāng)前頁面中靜態(tài)的 HTML 內(nèi)容,也就是所謂的 ?First Contentful Paint? 首次內(nèi)容繪制 (FCP) 會非常快速,但是頁面需要用戶交互效果又需要下載和執(zhí)行完成 JS 腳本發(fā)生 hydatrion 后才具有交互性。
這也就造成頁面的 TTI 相較于 CSR 方式會有所差勁,因?yàn)?CSR 在渲染完成后就會立即具有交互性(不需要其他任何多余步驟)。
1.3 qwik
上述聊了那么多前置內(nèi)置,終于要和大家切入正題了。
所謂磨刀不費(fèi)砍柴功,上邊和大家強(qiáng)調(diào)現(xiàn)階段 SSR 的方案以及對應(yīng)的優(yōu)劣勢就是為了引入下面的內(nèi)容。
首先,這篇文章的目的是為了讓大家在當(dāng)前眾多 SSR 框架中思考性能方面是否可以有所提升的,在服務(wù)器方面不會過多的深入。
我們可以稍微思考下上述服務(wù)器端渲染的過程:
第一步我們需要在服務(wù)端獲取對應(yīng)頁面的 HTML 頁面,大多數(shù)情況(非純靜態(tài)頁面)就需要在服務(wù)端掉用對應(yīng)渲染方法渲染出 HTML 頁面。
那么,如果我們能在第一步渲染 HTML 頁面時,就添加對應(yīng)的事件處理。后續(xù)的 3 步是不是完全可以省略下來了對吧。
其實(shí)社區(qū)內(nèi)部之前已經(jīng)有非常多的方案來提升所謂 SSR 框架的性能方案。
比如 Remix 的 HTTP stale-while-revalidate 緩存指令
比如 astro 等新興框架的 Islands 架構(gòu)方案,關(guān)于 Islands 有興趣的朋友可以參考神三元的這篇 Islands 架構(gòu)原理和實(shí)踐。
針對于上面的概念,我們直接來看看 qwik 中提到的 Hydration is Pure Overhead (完全多余的 Hydration)。
1)Hydration 造成的開銷
首先針對于 Hydration 的過程,我們提過到首先會在服務(wù)器上進(jìn)行一次靜態(tài) HTML 渲染,之后當(dāng) HTML 下發(fā)到客戶端后又會再次進(jìn)行 hydrate 的過程,在客戶端進(jìn)行重新執(zhí)行腳本添加事件。
Hydration 過程的難點(diǎn)就在于我們需要知道需要什么事件處理程序,以及將該事件處理程序附加在哪個對應(yīng)的 DOM 節(jié)點(diǎn)上。
這個過程中,我們需要處理:
- 每一個事件處理程序中的內(nèi)容,絕大多數(shù)框架中的狀態(tài)都作為閉包函數(shù)保存在內(nèi)容中。所以需要 hydration 的過程來重新獲取狀態(tài)。
- 其次,在搞清楚了每個事件處理函數(shù)的內(nèi)容后。我們也需要將對應(yīng)的事件處理函數(shù)附加到對應(yīng)的 DOM 節(jié)點(diǎn)上,同時還要確保該監(jiān)聽器的正確事件類型。
更加復(fù)雜每個事件處理函數(shù)中的內(nèi)容是一個閉包函數(shù),這個函數(shù)內(nèi)部需要處理兩種狀態(tài),APP_STATE 以及 FRAMEWORK_STATE。
- APP_STATE:應(yīng)用程序的狀態(tài)。簡單來說應(yīng)用程序的狀態(tài)就是 HTML 事件中的各個狀態(tài)事件,如果不存在這些事件狀態(tài)那么所有的內(nèi)容都是沒有任何交互效果的。
- FRAMEWORK_STATE:框架內(nèi)部狀態(tài)。通常我們會利用諸如 React 或者 Vue 等框架進(jìn)行接替渲染。如果沒有 FRAMETER_STATE,框架內(nèi)部就不知道應(yīng)該更新哪些DOM節(jié)點(diǎn),也不知道應(yīng)該在什么時候更新它們。
通俗來說 Hydration 就是在客戶端重新執(zhí)行 JS 去修復(fù)應(yīng)用程序內(nèi)部的 APP_STATE 以及 FRAMEWORK_STATE。
同樣還是這這張圖:
在圖中的前三個階段可以被稱為 RECOVERY 階段,這三個階段主要是在重建你的應(yīng)用程序。
當(dāng)從 Server 端下發(fā)的 HTML 靜態(tài)頁面后,我們希望它是具有交互效果的 HTML 正常應(yīng)用程序。
那么此時 hydartion 的過程必須經(jīng)歷下載 HTML 、下載所有相關(guān) JS 腳本、解析并且執(zhí)行下載的 JS 腳本。
RECOVERY 階段是和 hydartion 的頁面的復(fù)雜性成正比,在移動設(shè)備上很容易花費(fèi) 10 秒。
由于RECOVERY是昂貴的部分,大多數(shù)應(yīng)用程序的啟動性能都不是最佳的,尤其是在移動設(shè)備上。
前三個階段被稱為 RECOVERY 的階段其實(shí)是完全沒有必要的,因?yàn)樵诜?wù)端我們已然渲染過對應(yīng)的 HTML ,但是為了應(yīng)用程序的可交互性以及服務(wù)端僅保留了靜態(tài)的 HTML 模版導(dǎo)致不得不在 Client 上繼續(xù)執(zhí)行一次 Server 端的邏輯。
總而言之,hydration 其實(shí)是通過下載并重新執(zhí)行 SSR/SSG 呈現(xiàn)的 HTML 中的所有 JS 腳本并執(zhí)行來恢復(fù)組建中的事件處理程序。
同一個應(yīng)用程序,會被發(fā)送到客戶端兩次,一次作為 HTML,另一次作為 JavaScript。
此外,框架必須立即執(zhí)行 JavaScript 以恢復(fù)在服務(wù)器上被丟掉的 APP_STATE和FRAMEWORK_STATE。所有這些工作只是為了檢索服務(wù)器已經(jīng)擁有但丟棄的東西!!
比如這樣一個例子:
上邊的例子中我們編寫了一個 Counter 的計數(shù)器組件,在傳統(tǒng) SSR 過程中該組件會被渲染成為:
可以看到上邊的兩個按鈕不擁有任何處理狀態(tài)的能力。
要使網(wǎng)頁具有交互性,必須要做的就是通過下載對應(yīng) HTML 頁面中的 script 腳本并執(zhí)行代碼從而恢復(fù)按鈕上的交互邏輯和狀態(tài)。
為了具有交互性,客戶端不得不執(zhí)行代碼實(shí)例化組件后重新創(chuàng)建狀態(tài)。
當(dāng)上述過程完成后,你的應(yīng)用程序才會真正具有可交互性。無疑,同一個組件的渲染邏輯被執(zhí)行了兩遍,這是一個非常冗余且耗費(fèi)性能的過程。
2)Resumability: 更加優(yōu)雅的 hydartion 替代方案
所以為了消除額外的開銷,我們需要思考如何避免重復(fù)的 RECOVERY 階段。同時還要避免上面的第四步,第四步是執(zhí)行腳本后給現(xiàn)有的 HTML 附加正確的事件處理程序。
qwik 中提出了一個全新的思路來規(guī)避 RECOVERY 帶來的外開銷:
- 將所有必需的信息序列化為 HTML 的一部分。qwik 將需要的狀態(tài)以及事件序列化保存在 Server 端下發(fā)的 HTML 模版中,需要序列化信息需要包括WHAT(事件處理函數(shù)內(nèi)容), WHERE(哪些節(jié)點(diǎn)需要哪些類型的事件處理函數(shù)), APP_STATE(應(yīng)用狀態(tài)), 和FRAMEWORK_STATE(框架狀態(tài))。
- 依賴于事件冒泡來攔截所有事件的全局事件處理程序。qwik 中事件處理程序是在全局處理的,這樣我們就不必在特定的 DOM 元素上單獨(dú)注冊所有事件。
- qwki 內(nèi)部存在一個可以延遲恢復(fù)事件處理程序的工廠函數(shù)。
該工廠函數(shù)主要用于處理 WHAT 階段,也就是用來識別某個事件處理函數(shù)中應(yīng)該存在什么腳本邏輯。
我們可以看到所謂的 Resumable 對比 Hydration 明顯可以省略不需要后三個階段,直接獲取 HTML 后頁面其實(shí)就已經(jīng)準(zhǔn)備完畢,這無疑對于性能的提升是巨大的。
對比傳統(tǒng)的 hydration 方案,在客戶端獲得服務(wù)端下發(fā)的 HTML 后會立即請求需要的 JS 腳本并執(zhí)行從而為頁面附加對應(yīng)的交互效果。
而 qwik 提出的概念恰恰相反,獲取完服務(wù)端下發(fā)的 HTML 頁面后所有的交互效果實(shí)際上都是一種惰性創(chuàng)建的效果。
因?yàn)槲覀冊?HTML 中的每個元素中都已經(jīng)通過序列化從而在它的標(biāo)簽屬性上記錄了對應(yīng)事件處理函數(shù)的位置以及腳本內(nèi)容(自然內(nèi)容中也包含對應(yīng)的狀態(tài)),所以當(dāng)獲得 HTML 頁面后其實(shí)就可以說此時頁面已經(jīng)加載完畢了而不需要任何實(shí)時的 JS 執(zhí)行。
這樣做的好處是在 qwki 中完全可以省略 hydration 的多余步驟,甚至可以說完全拋棄了 hydration 的概念。
客戶端完全不必和服務(wù)端的 HTML 進(jìn)行水合,相同的渲染內(nèi)容僅僅是在 Server 端進(jìn)行一次渲染客戶端即可擁有對應(yīng)的事件處理內(nèi)容。
簡單來講Qwik的工作原理就是在服務(wù)端序列化 HTML 模版,從而在客戶端延遲創(chuàng)建事件處理程序,這也是它為什么非常快速的原因。
3)qwik 工作機(jī)制
上邊我們講到了 qwik 的原理部分,同樣拿上邊的計數(shù)器的例子我們來對比下:
在 qwik 編譯后,服務(wù)端會序列化對應(yīng)組件的 HTML 結(jié)構(gòu)從而下發(fā)如下的模板:
我們可以看到經(jīng)過 qwik 編譯后的 html 結(jié)構(gòu)并不單單只有 DOM 元素,同時會在對應(yīng)需要狀態(tài) & 事件的 DOM 元素上通過 HTML 元素屬性來記錄當(dāng)前元素的事件和狀態(tài)信息,這既是 qwik 中的序列化。
比如上邊 button 的 on:click 屬性記錄了該元素后續(xù)需要恢復(fù)的所有信息。
需要注意的是序列化這一步是在服務(wù)端渲染時完成的,這也就意味著后續(xù)客戶端可以通過服務(wù)端序列化的屬性信息進(jìn)行反序列化從而達(dá)到所謂的可恢復(fù)性而不需要重復(fù)執(zhí)行組件。
當(dāng)然你可能會好奇 qwik 是如何進(jìn)行這些事件 & 狀態(tài)的恢復(fù),qwik 正是通過在返回的 HTML 頁面中內(nèi)嵌的所謂 qwikloader 的 script 腳本(這段腳本的大小不超過 1kb)配合 qwikjson 映射表,從而在全局進(jìn)行恢復(fù)事件和狀態(tài)的邏輯。
正因?yàn)檫@個原因,使得 qwik相較于傳統(tǒng) SSR 的 hydration 在 Client 中再次執(zhí)行渲染從而水合頁面狀態(tài)和事件處理程序,這簡直可以說是接近零 JS 的執(zhí)行過程。
最終在用戶觸發(fā)事件時候達(dá)到惰性的創(chuàng)建事件并執(zhí)行,這個過程中完全沒有重復(fù)任何服務(wù)器已經(jīng)完成的任何工作。
整個工作過程就像下面這張圖描述的那樣:
上邊的這張圖完美的描述了 qwik 的工作原理,相信經(jīng)過上述的描述大家對于這張圖中想表達(dá)的思想已經(jīng)可以完美的理解了。
利用 qwik 的這個優(yōu)勢,在絕大多數(shù)應(yīng)用中我們可以利用 qwik 保證你的 SSR 應(yīng)用在保證快速的 FCP 的前提也同樣擁有與之不相上下的 TTI 體驗(yàn)效果。
4)惰性加載腳本會影響用戶交互體驗(yàn)嗎
當(dāng)然上文說過任何框架的優(yōu)勢和劣勢都不是絕對的,在我們看來 qwik 的確會存在以下一些問題。
大多數(shù)同學(xué)看完上邊的內(nèi)容我相信也會存在“惰性加載腳本會影響用戶交互體驗(yàn)嗎”這樣的疑問。
首先,qwik 中既然選擇在觸發(fā)用戶行為時,再惰性加載并執(zhí)行響應(yīng)的 JS 腳本。那么難免需要在用戶觸發(fā)交互時動態(tài)生成對應(yīng)的事件處理函數(shù)進(jìn)行執(zhí)行。
這樣的方式相較于傳統(tǒng) hydration 的確會存在一些不足,需要額外生成事件會額外造成交互響應(yīng)時間的損耗而傳統(tǒng) SSR 方式在頁面首次加載時就已經(jīng)綁定好(相當(dāng)于生成了)相應(yīng)的事件處理函數(shù)。
就惰性加載生成事件這點(diǎn):
針對于動態(tài)加載 JS 腳本,其實(shí)已經(jīng)存在諸如非常多的 prefetch 等等預(yù)加載技術(shù)。
無論是基于傳統(tǒng) Next 方案還是基于 qwik 這種惰性可恢復(fù)的方案,利用 prefetch 等預(yù)加載技術(shù)優(yōu)先在網(wǎng)絡(luò)空閑時加載響應(yīng)重要的 JS 腳本都是非常有必要的,所以這點(diǎn)在我看來并不是特別重要的問題。
5)延遲加載會帶來 bundle 數(shù)量的上升嗎
qwik 推崇的延遲加載其實(shí)已經(jīng)是一項(xiàng)非常成熟的構(gòu)建技術(shù)了。無論是使用 webpack、rollup 又或是其他任何構(gòu)建工具都存在延遲加載 & 代碼分割的技術(shù)。
傳統(tǒng)構(gòu)建工具中關(guān)于代碼分割會帶來以下兩點(diǎn)的困難:
- 需要開發(fā)人員自行去處理更加細(xì)粒度的代碼分割,當(dāng)然這并不是最主要的。因?yàn)槟壳拔覀兛梢岳?/span> ?Magic Comments? 配合 ?Dynaic Imports? 來解決需要手動切入多個入口點(diǎn)的問題。
當(dāng)然,這一系列事情比起魔法注釋。因?yàn)?qwik 本身提倡的就是所謂的延遲加載,所以在框架內(nèi)部已經(jīng)幫我們足夠智能的去處理這個過程。
但是需要注意的是這并不意味著開發(fā)者無法自主去控制這個過程。只是在使用框架的過程中,qwik 希望開發(fā)者更加專注于他們自身的業(yè)務(wù)邏輯。
- 當(dāng)存在非常多的延遲加載時,傳統(tǒng)構(gòu)建工具會從一個大 bundle 分割成為無數(shù)個小的 bundle 。
延遲加載模塊的確會存在多個 small bundle 的問題,可是當(dāng)我們擁有的 bundle 越多,其實(shí)我們就擁有更多的自由度去以各種各樣的方式去拼裝成為單個大的 bundle。
qwik 中存在足夠的方式提供給我們將多個小的 chunk 自由組合成為一個從而有效的減少細(xì)碎 chunk 的數(shù)量,當(dāng)然這個點(diǎn)在傳統(tǒng)構(gòu)建工具中也是這樣。
6)動態(tài)創(chuàng)建事件函數(shù)會造成內(nèi)存泄漏嗎
qwik 的設(shè)計思想在與每次事件觸發(fā)時通過 qwikloader 來動態(tài)創(chuàng)建事件處理函數(shù),相信有的同學(xué)存在疑問“那么多次觸發(fā)事件會造成額外的開銷嗎”。
qwik 的作者 mi?ko hevery 在 ??Hydration is Pure Overhead?? 中明確的表示過 qwik 會在每次事件執(zhí)行完畢后釋放函數(shù),相當(dāng)于每次事件執(zhí)行完畢都會進(jìn)行一次“去水合”的過程。
所以,當(dāng)你觸發(fā)一次事件和無數(shù)次事件函數(shù)在執(zhí)行過程中對于內(nèi)存占用來說是相差無幾的。
當(dāng)然相較于傳統(tǒng) hydration 的方式(在頁面首次渲染時在內(nèi)存中記錄所有狀態(tài)),無疑 qwik 這種并不在內(nèi)存中記錄任何狀態(tài)的方式恰恰對于內(nèi)存的占用比 dyration 更加輕量化。
7)qwik 真的有那么快嗎
說了那么多,那么 qwik 真的有那么快嗎。
上圖是利用 qwik 搭建的 ??builder.io?? 官方網(wǎng)站,我相信 builder.io 的數(shù)據(jù)已經(jīng)告訴我們答案了。
固然上述的數(shù)據(jù)并不僅僅只是單純一個 qwik 框架帶給網(wǎng)站的優(yōu)化,一定會有代碼層面或者構(gòu)建層面等等方面的優(yōu)化配合而來的數(shù)據(jù)。但是針對于 FCP 和 TTI 時間上的一致性這在一個中型 SSR 應(yīng)用程序其實(shí)可以稱得上是非常優(yōu)秀了,我相信這足以說明了 qwik 的確名副其實(shí)。
二、結(jié)語
我們可以看出來,qwik 的核心思路還是通過更加細(xì)粒的代碼控制配合惰性加載事件處理程序以及事件委托來縮短首屏 TTI。
文章中我們也講到了 qwik 其實(shí)并不是因?yàn)槭褂昧硕嗝磁1频乃惴▽?dǎo)致它有多么快,而它的速度正是得益于它的設(shè)計思路,省略了傳統(tǒng) SSR 下首屏需要加載龐大的 JS 進(jìn)行 hydration 的過程。
當(dāng)然,我們對于 qwik 也仍是在學(xué)習(xí)階段。后續(xù)會在公司里的更多項(xiàng)目嘗試 qwik 之后也會和大家分享關(guān)于它的更多心得。
總而言之,qwik 的“無水合”設(shè)計思路目前看來的確會在框架層面帶來巨大的性能提升。大家如果有機(jī)會的話也可以在項(xiàng)目中嘗試一下 qwik ,相信會給你帶來意想不到的收益效果。