本文簡單介紹了web應用各類渲染方案,其中包括客戶端渲染、服務器端渲染等各類渲染方案。文章翻譯自: https://developers.google.com...。由我所在的團隊共同翻譯完成,併發布在前端技術公衆號: 方凳雅集上,轉載於此。方凳雅集是阿里CBU前端技術專業號,有興趣的小夥伴能夠關注一發。
做爲開發人員,咱們常常面臨影響應用程序整個架構的決策。咱們必須作出的核心決策之一是在什麼地方實現業務邏輯和渲染邏輯。這可能很困難,由於有不少不一樣的方法來構建網站。對這個領域的理解來自於過去幾年咱們在一些大型網站的工做。從廣義上說,咱們鼓勵開發人員選用帶有rehydration(下一小節有解釋)的服務器端渲染或靜態化渲染。爲了更好理解咱們在作決定時所選擇的架構,須要對每種方法和術語有充分的理解,經過不一樣渲染方式的頁面性能能夠幫助咱們理解它們之間的差別。javascript
渲染:
SSR:服務器端渲染。
CSR:客戶端渲染。
Rehydration:在服務器端渲染的dom樹和數據的基礎上,瀏覽器端利用JavaScript再次渲染。
Prerendering:在構建時生成靜態HTML和頁面的初始狀態。性能:
TTFB:Time to First Byte —— 瀏覽器發出資源請求到接受到資源第一個字節的時間。
FP:First Paint —— 頁面打開到可視內容第一個像素渲染出來的時間。
FCP:First Contentful Paint —— 頁面打開到頁面主要內容可見的時間。
TTI:Time To Interactive —— 頁面打開到變得能夠交互的時間。html
這裏只是簡單的介紹了這些性能術語,想要詳細的瞭解它們,能夠看一下咱們以前寫的兩篇文章性能優化篇——以用戶爲中心的指標(一)和性能優化篇——以用戶爲中心的指標(二)。前端
服務器端渲染:打開頁面時服務器將完整的HTML生成好了並返回。這避免了在瀏覽器端取數而後渲染所產生的消耗,由於這些事情已經在服務器端響應用戶以前就已經作好了。java
服務器端渲染會帶來快速的FP和FCP,在服務器端處理業務邏輯和渲染邏輯,能夠避免向瀏覽器發送大量JavaScript,這有助於實現快速的TTI,這種方法適用於各類設備和網絡環境,若是你開啓了一些流瀏覽器優化,好比文檔採用流的方式解析(streaming document parsing)。react
對於服務器端渲染,用戶不太可能會去等瀏覽器執行其餘的耗CPU的JavaScript代碼執行完畢再去操做,由於頁面內容已經顯示出來了。在第三方js沒法避免時(廣告),雖然服務器端渲染能夠減小FP和FCP渲染的JavaScript消耗,可是可能會給接下來要執行的js帶來必定的「負擔」。服務器端渲染的主要缺點在於,渲染須要消耗時間,因此可能TTFB會比較大。git
服務器渲染是否能夠知足應用程序,很大程度上取決於您正在構建的體驗類型。關於服務器渲染與客戶端渲染的哪一個更好的爭論從未停過。可是咱們須要記住的時,咱們能夠有選擇讓一些頁面進行服務器端渲染,其餘頁面使用客戶端渲染。一些網站使用這種混合渲染模式就取得了不錯的效果,好比Netflix對他的登陸頁面採用了服務器端渲染,同時爲重交互的頁面預取js,爲那些重交互而且客戶端渲染的頁面加快頁面加載的機會。github
許多現代框架、庫和架構使得同一個應用同時在服務器端和瀏覽器端都能渲染成爲可能。這些技術雖然能夠用於服務器端渲染,但須要注意的是,在服務器和客戶端上進行渲染的體系結構具備很是不一樣的性能特徵和權衡。React用戶能夠用它的renderToString方法或者其餘基於該方法的框架進行服務器端渲染,好比Next.js; Vue用戶能夠去看它的服務器端渲染指南或者Nuxt; Angular用戶能夠去看Universal。web
靜態化渲染:在構建(build)時將頁面中不會變化的內容直接渲染成出來,而後打到HTML中去。在瀏覽器端須要執行的js有限的假設下,該方法可以提供快速的FP、FCP和TTI。與服務器端渲染不一樣,它還能提供快速的TTFB,由於服務器端不須要生成HTML。通常而言,靜態化渲染須要爲每一個URL生成一個單獨的HTML,當用戶訪問的時候,直接將預先渲染好的HTML返回就好。另外渲染出的HTML也能夠部署到CDN上,經過edge caching(邊緣緩存)緩存作一些優化。對於不瞭解edge caching的同窗,能夠去看一下咱們以前寫的React緩存小記,裏面有關於它的介紹。shell
靜態化渲染也有不一樣的方案,好比像Gatsby這樣的工具旨在讓開發人員感受他們的應用程序是動態渲染的,而不是在構建過程當中產生靜態的HTML;像Jekyl和Metalsmith這樣的工具擁抱的靜態特性,提供了不少模板驅動的方法。瀏覽器
靜態化渲染須要爲每一個URL生成單獨的HTML,這是它的一個缺點。若是您沒法提早預測這些URL的內容,或者或一個網站存在大量的URL,靜態化渲染多是不合適的。對於靜態化渲染,React用戶可能對Gatsby、Next.js的static export或者Navi比較熟悉。
這裏咱們須要重點理解一下靜態化渲染和預渲染之間的差別:靜態化渲染的頁面在瀏覽器端變得可以交互以前只須要執行不多的js代碼,甚至不須要;而預渲染雖然可以實現快速的FP、FCP,但應用必須在瀏覽器端執行js代碼才能變得可交互。
若是您不能肯定究竟是使用靜態化渲染仍是預渲染,那就測試一下唄,在應用加載的時候,禁止JavaScript的加載和執行。禁止JavaScript後,對於靜態化渲染,頁面的大多數功能仍是可以正常運行;而對於預渲染,頁面可能只有一些基礎的功能可以工做,好比鏈接跳轉。
另外一個有用的測試方式是經過Chrome的開發者工具DevTools減慢網絡速度,觀察在頁面變爲交互以前下載了多少JavaScript。預渲染一般須要更多的JavaScript,而且複雜度每每也比使用漸進加強的靜態化渲染要高。
服務器端渲染不是一顆銀彈,它的動態特性會帶來巨大的計算開銷。服務器端渲染會加大TTFB或發送雙倍的數據(好比將客戶端的State數據打到HTML中去),在React中,renderToString會很慢,由於它是同步而且單線程的。爲了讓服務器端渲染可以」正確「運行,咱們須要關注組件緩存、內存消耗、memoization技術應用和其餘問題。一般狀況你可能須要屢次渲染同一個應用——一次在服務器端,一次在客戶端。服務器端渲染只能讓須要展現的內容顯示的的更快,並無減小工做量。
服務器端渲染能夠根據不一樣的URL生成不一樣的內容,相較於靜態化渲染,它的速度可能會慢一些。其實咱們能夠作一些工做來緩解這個問題,服務器渲染 + HTML緩存能夠大大減小渲染時間。服務器端渲染的優點在於它可以獲取實時數據,而且所能處理的請求集比靜態化渲染要大。由於靜態化渲染只能處理那些提早可以預測內容的頁面,對於須要個性化的頁面(千人千面),靜態化渲染就沒法處理了。
在構建PWA時,服務器端渲染也有用武之地,服務器端對頁面片斷進行渲染,前端使用Service Worker進行緩存。
客戶端渲染:是指在瀏覽器中直接使用JavaScript來渲染頁面,全部的邏輯、數據的獲取、模板和路由都是在客戶端而不是服務器上處理的。
在移動端,客戶端渲染很難得到並保持一個較快的渲染速度。有時咱們只須要作不多的工做,就能讓客戶端渲染的性能與服務器端渲染的性能相差無幾,好比儘量的保持小的JS體積和少的RTT(https://en.wikipedia.org/wiki...)。甚相當鍵的腳本和數據若是使用HTTP/2的服務器端推送,或者使用<link rel=preload>,咱們還可讓解析工做更早開始。另外,像PRPL這樣的技術也能夠幫助咱們加快頁面的初始化及其後續導航。
但事情並非這麼簡單。客戶端渲染的主要問題是,所需的JavaScript會隨着應用程序的增加而增加。隨着新的JavaScript庫、兼容組件和第三方代碼的添加,控制腳本的規模會變得格外困難——尤爲是這些代碼和庫都常常都須要在頁面內容渲染以前被加載。因此對於那些腳本規模很大的應用,應該優先考慮使用代碼拆分的方案。特別的,對於懶加載的JavaScript,要確保只在有須要時才加載必要的代碼。而對於只有不多或者沒有什麼交互的應用,服務器渲染是一個可擴展性更好的方案。
若是你想構建SPA(單頁)應用,使用app shell緩存頁面中交互的核心組件會給你很大的幫助。若是結合PWA的Service Work技術,還能夠有效提升頁面重複訪問時的性能。
一般稱爲同構渲染或者直接簡單地稱爲"SSR",這種方式嘗試在客戶端渲染和服務端渲染之間尋找平衡,但願可以減小二者的弊端。
頁面導航致使跳轉或刷新時,服務器會輸出頁面的HTML文檔,並把該頁面所須要的javascript和(用於渲染的)數據內聯到文檔一塊兒輸出。若是實現得當,這種方式確實能夠像服務端渲染那樣實現較快的首次內容繪製(First Contentful Paint),以後客戶端會經過一種叫rehydration的技術繼續(在客戶端)渲染。這是個新穎的技術,但它會引發比較大的性能問題。
使用reydration技術進行服務端渲染的主要問題在於它會對可交互時間(Time To Interactive)有明顯的負面影響,儘管它縮短了首次繪製時間(First Paint)。服務端渲染的頁面每每讓人感受已經加載完畢並能夠開始交互了,但實際上只有等到客戶端的js腳本執行並完成DOM事件綁定才能響應用戶的交互(例如用戶的輸入行爲)。在一些手機終端,這個過程會耗費幾秒甚至幾分鐘的時間。
也許你本身也經歷過這樣的場景:一個頁面看起來已經加載完成了,可是在頁面執行點擊或者輕觸的動做,結果卻什麼也沒發生。這很快變得使人沮喪......「爲何(頁面)沒有反應?爲何我不能滾動?」
Rehydration問題:重複
Rehydration的問題不止於此,一般比因js致使的交互延遲更糟糕。爲了讓客戶端js可以準確地渲染,而不用從新向服務器請求渲染所需的數據,目前服務端渲染一般會把UI所需的數據序列化並內聯到HTML文檔的script標籤裏。最終的HTML文檔包含了更高層面的重複:
能夠看到,對於頁面導航請求,服務器會返回了相應的UI描述(HTML),但它一樣返回了渲染UI所需的數據。同時,客戶端腳本一樣包括了UI的描述(譯者注:前端渲染須要包含對UI的描述,例如JSX),以便在客戶端繼續渲染。只有當bundle.js完成下載和執行後,UI才進入可交互狀態。
從使用rehydration方案的一些真實網站蒐集到的性能數據來看,該方案是極度不推薦的。究其緣由,仍是回到用戶體驗上:這種方式很容易讓用戶停留在「神祕的峽谷」之中。(譯者注:即界面可見但不可交互的狀態)
儘管如此,外界對rehydration方案仍是有些許期待的。簡單來講,使用服務端渲染時,只對須要高度緩存的內容纔會下降首字節時間(TTFB),獲得和預渲染(prerendering)相似的結果。
在將來,rehydtration方案可能會逐漸地、或者部分地被應用,併成爲服務端渲染的關鍵。
服務端渲染在過去幾年中有了長足的進展。
流式服務端渲染容許你以塊的形式發送HTML,同時瀏覽器端接收並逐一渲染,這種方案能夠實現快速的FP和FCP。React提供了一個異步的、以流的方式傳輸的方式 renderToNodeStream,與同步的renderToString相比,它可以更好處理服務器壓力。
漸進式Rehydration也值得關注,React團隊正在作一些有趣的探索(https://github.com/facebook/r...)。使用這種方法,服務器渲染隨着時間的推移被「啓動」去渲染應用的各個片斷,而不是當前一次性渲染整個應用。這能夠幫助減小使頁面交互所需的JavaScript,由於這樣能夠延遲頁面中低優先級展現內容的渲染(好比非首屏的內容),同時能夠防止這些低優先級的渲染阻塞主線程。並且,這種方案也能避免帶Rehydration的服務端渲染的一個很大的陷阱:服務端渲染生成的DOM樹在瀏覽器端被銷燬而後被重建,這個問題大多數是由於同步的客戶端渲染生成DOM樹所須要的初始數據還沒準備好。
局部Rehydration
局部rehydration被證實難以實現。這個方案是漸進式Rehydration的一個擴展,須要分析出相互獨立的片斷(組件/視圖/樹)中哪些具備極少交互或者徹底沒有交互的部分進行漸進式rehydrated。對於這些近乎靜態的部分,相應JavaScript代碼會被改形成惰性的引用,從而將它們對客戶端的影響下降到近乎爲0。局部hydration爲緩存帶來了必定挑戰,客戶端導航意味着咱們不能假設應用程序的惰性部分即服務器渲染生成的HTML是可用的,除非頁面徹底加載完。
Trisomorphic渲染
若是Service Worker是你的一個選項,「trisomorphic」渲染也是一個有趣的點子。利用這項技術,你能夠利用流式服務端渲染生成初始的或不依賴JS的部分,而後在Service Worker install後利用它渲染生成html。這種方案能使緩存的組件和模板保持實時並且還支持SPA類型的應用,在同一會話中根據不一樣的導航渲染不一樣的視圖。這種方法在服務器端、客戶端和Service Worker中能夠複用模板和路由代碼。
在選擇在渲染方案,一般會考慮SEO。一般咱們會選擇服務器渲染來應對爬蟲。爬蟲可能能理解JavaScript,可是它們有不少侷限性,咱們須要重點關注一下他們是如何渲染的。雖然客戶端渲染能夠工做,可是通常都沒有作測試等工做,若是您的應用依賴於客戶端渲染,動態渲染是您值得考慮的一個選項,具體能夠參考https://developers.google.com...。
若是有疑問,Mobile Friendly Test能夠測試您選擇的方法是否符合預期,它能夠顯示頁面在爬蟲中的顯示方式、序列化的HTML內容(JavaScript執行以後)以及渲染過程當中出現的任何錯誤。
Mobile Friendly Test地址以下:
https://search.google.com/tes...
當決定用什麼方式渲染的時候,要知道咱們即將遇到的瓶頸是什麼。採用客戶端渲染仍是服務端渲染將決定你90%的架構設計。一個完美的解決方案一般服務端發送html跟最小的js來完成交互。如下是服務端-客戶端渲染的一個總結圖: