高性能前端架構(gòu)解決方案
這篇文章介紹了一些使前端應(yīng)用程序加載更快并提供良好用戶(hù)體驗(yàn)的技術(shù)。
我們將研究前端的總體架構(gòu),如何首先加載必需的資源,并最大化資源緩存的概率。
無(wú)論你的頁(yè)面是否需要成為客戶(hù)端應(yīng)用程序,還是如何優(yōu)化應(yīng)用程序的渲染時(shí)間,我都不會(huì)說(shuō)太多后端如何傳遞資源。
總覽
我將把應(yīng)用程序加載分為三個(gè)不同的階段:
- 初始渲染 – 用戶(hù)看到任何東西之前需要多長(zhǎng)時(shí)間?
- 應(yīng)用程序加載 – 用戶(hù)可以使用該應(yīng)用程序需要多長(zhǎng)時(shí)間?
- 下一頁(yè) – 導(dǎo)航到下一頁(yè)需要多長(zhǎng)時(shí)間?
初始渲染
在瀏覽器的初始渲染之前,用戶(hù)看不到任何東西。渲染頁(yè)面至少需要加載 HTML 文件,但是大多數(shù)時(shí)候需要加載其他資源,例如 CSS 和 JavaScript 文件。一旦這些都加載完畢,瀏覽器就可以開(kāi)始在屏幕上渲染。
在本文中,我將使用 WebPageTest 瀑布圖。你網(wǎng)站的請(qǐng)求瀑布可能看起來(lái)像這樣。
HTML 文檔將加載一堆其他文件,并在這些文件加載后渲染頁(yè)面。請(qǐng)注意, CSS 文件是并行加載的,因此每個(gè)其他請(qǐng)求不會(huì)增加明顯的延遲。
(備注:gov.uk 啟用了 HTTP/2,因此資產(chǎn)域可以重新使用與 www.gov.uk 的現(xiàn)有連接!我將在下面詳細(xì)討論服務(wù)器連接。)
減少渲染阻塞的請(qǐng)求
css 和(默認(rèn)情況下) script 文件會(huì)阻止其下方的任何內(nèi)容渲染。
你可以通過(guò)以下幾種方法來(lái)解決此問(wèn)題:
- 將腳本標(biāo)簽放在 body 標(biāo)簽的底部
- 使用 async 異步加載 script
- 內(nèi)聯(lián)使用小型的 JS 或 CSS 代碼段(如果需要同步加載)
避免順序渲染阻塞請(qǐng)求鏈
讓你的網(wǎng)站變慢的不一定是阻止渲染的請(qǐng)求數(shù)量。更重要的是每種資源的下載大小,以及瀏覽器發(fā)現(xiàn)需要加載資源的時(shí)間。
如果瀏覽器僅在另一個(gè)請(qǐng)求完成后才發(fā)現(xiàn)需要加載文件,則可以獲取同步請(qǐng)求鏈。發(fā)生這種情況可能有多種原因:
- CSS 中的 @import 規(guī)則
- CSS 文件中引用的 Webfonts
- JavaScript 注入鏈接或腳本標(biāo)簽
看一下這個(gè)例子:
這個(gè)網(wǎng)站在它的某個(gè) CSS 文件中使用 @import 加載 Google Fonts。這意味著瀏覽器需要一個(gè)接一個(gè)地發(fā)出這些請(qǐng)求:
- 文件 HTML
- 應(yīng)用程序的 CSS
- Google 字體 CSS
- Google Font Woff文件(在瀑布圖中未顯示)
要解決這個(gè)問(wèn)題,首先需要將 Google Fonts 的 CSS 請(qǐng)求從 @import 移動(dòng)到 HTML 中的 link 標(biāo)記,這就切斷了請(qǐng)求鏈條上的一個(gè)環(huán)節(jié)。
為了進(jìn)一步加快速度,建議直接在 HTML 或 CSS 文件中內(nèi)聯(lián) Google Fonts CSS 文件。
(記住,來(lái)自 Google Fonts 的 CSS 響應(yīng)取決于用戶(hù)代理。如果你用 IE8 發(fā)出請(qǐng)求,CSS會(huì)引用一個(gè) EOT 文件,IE11 會(huì)得到一個(gè) woff 文件,而現(xiàn)在的瀏覽器會(huì)得到一個(gè) woff2 文件。但是如果你不介意舊的瀏覽器使用系統(tǒng)字體,那么你可以復(fù)制粘貼 CSS 文件的內(nèi)容。)
即使頁(yè)面開(kāi)始呈現(xiàn)后,用戶(hù)仍可能無(wú)法對(duì)該頁(yè)面執(zhí)行任何操作,因?yàn)樵诩虞d字體之前,不會(huì)顯示任何文本??梢酝ㄟ^(guò) font-display swap 來(lái)避免這種情況,現(xiàn)在 Google Fonts 默認(rèn)情況下已經(jīng)開(kāi)始支持。
有時(shí),消除請(qǐng)求鏈?zhǔn)遣豢尚械?。在這些情況下,可以考慮使用 preload 或 preconnect 標(biāo)記。例如,在實(shí)際的 CSS 請(qǐng)求發(fā)出之前,上面的網(wǎng)站可以連接到 fonts.googleapis.com。
重復(fù)使用服務(wù)器連接以加快請(qǐng)求
建立新的服務(wù)器連接通常需要在服務(wù)器的瀏覽器之間進(jìn)行3次往返:
- DNS 查詢(xún)
- 建立 TCP 連接
- 建立 SSL 連接
連接就緒后,至少需要再進(jìn)行一次往返來(lái)發(fā)送請(qǐng)求并下載響應(yīng)。
下面的瀑布顯示連接已啟動(dòng)到四個(gè)不同的服務(wù)器:hostgator.com,optimize.com,googletagmanager.com 和 googelapis.com。
但是,對(duì)同一服務(wù)器的后續(xù)請(qǐng)求可以重新使用現(xiàn)有連接。因此,加載 base.css或 index1.css 的速度很快,因?yàn)樗鼈円餐泄茉?hostgator.com 上。
減小文件大小并使用CDN
除了文件大小之外,還有兩個(gè)其他因素會(huì)影響請(qǐng)求時(shí)間,這些因素都在你的控制范圍內(nèi):資源大小和服務(wù)器位置。
向用戶(hù)發(fā)送盡可能少的數(shù)據(jù),并確保將其壓縮(例如,使用 brotli 或 gzip )。
內(nèi)容交付網(wǎng)絡(luò)在大量位置提供服務(wù)器,因此其中之一可能位于你的用戶(hù)附近。用戶(hù)可以連接到與其附近的 CDN 服務(wù)器,而不必連接到中央應(yīng)用程序服務(wù)器。這意味著服務(wù)器的往返時(shí)間將大大縮短。這對(duì)于諸如 CSS,JavaScript和 Image 之類(lèi)的靜態(tài)資產(chǎn)特別方便,因?yàn)樗鼈円子诜职l(fā)。
使用 service workers 跳過(guò)網(wǎng)絡(luò)
service workers 允許你在請(qǐng)求進(jìn)入網(wǎng)絡(luò)之前攔截它們。這意味著你可以實(shí)現(xiàn)瞬時(shí)首屏渲染!
當(dāng)然,這只在你不需要網(wǎng)絡(luò)發(fā)送響應(yīng)時(shí)才有效。你需要已經(jīng)緩存了響應(yīng),所以用戶(hù)只有在第二次加載你的應(yīng)用時(shí)才會(huì)受益。
下面的 service workers 緩存呈現(xiàn)頁(yè)面所需的HTML和CSS。當(dāng)再次加載應(yīng)用程序時(shí),它會(huì)嘗試為緩存的資源提供服務(wù),如果資源不可用,則會(huì)返回到網(wǎng)絡(luò)。
- self.addEventListener("install", async e => {
- caches.open("v1").then(function (cache) {
- return cache.addAll(["/app", "/app.css"]);
- });
- });
- self.addEventListener("fetch", event => {
- event.respondWith(
- caches.match(event.request).then(cachedResponse => {
- return cachedResponse || fetch(event.request);
- })
- );
- });
應(yīng)用加載
好的,現(xiàn)在用戶(hù)已經(jīng)可以看到一些東西,然后他們?cè)诳梢允褂媚愕膽?yīng)用程序之前還需要什么?
- 加載應(yīng)用程序代碼(JS和CSS)
- 加載頁(yè)面的基本數(shù)據(jù)
- 加載其他數(shù)據(jù)和圖像
請(qǐng)注意,不僅僅是延遲從網(wǎng)絡(luò)加載數(shù)據(jù)會(huì)延遲渲染。加載代碼后,瀏覽器將需要解析,編譯和執(zhí)行它。
Bundle split:僅加載必要的代碼,并最大化緩存命中率
Bundle split 允許只加載當(dāng)前頁(yè)面所需的代碼,而不是加載整個(gè)應(yīng)用程序。Bundle split 還意味著可以緩存其中的一部分,即使其他部分已經(jīng)更改,需要重新加載。
通常,代碼被分成三種不同類(lèi)型的文件:
- 網(wǎng)頁(yè)本身專(zhuān)用代碼
- 共享應(yīng)用程序代碼
- 很少更改的第三方模塊(非常適合緩存?。?/li>
Webpack 可以使用 optimization.splitChunks 自動(dòng)拆分共享代碼以減少總下載量。確保啟用運(yùn)行時(shí)塊,以使 chunk 哈希穩(wěn)定,并從長(zhǎng)期緩存中受益。
分離頁(yè)面特定的代碼不能自動(dòng)完成,你需要識(shí)別可以單獨(dú)加載的位。通常這是一個(gè)特定的路徑或一組頁(yè)面。使用動(dòng)態(tài)導(dǎo)入來(lái)延遲加載代碼。
Bundle split 會(huì)導(dǎo)致更多的請(qǐng)求被發(fā)送來(lái)加載你的應(yīng)用程序。但是只要請(qǐng)求是并行發(fā)送的,這就不是什么大問(wèn)題,特別是當(dāng)你的站點(diǎn)開(kāi)啟了 HTTP/2 服務(wù)的時(shí)候。你可以看到在這個(gè)瀑布的前三個(gè)請(qǐng)求:
然而,這個(gè)瀑布圖還顯示了兩個(gè)按順序發(fā)出的請(qǐng)求。這些塊只在這個(gè)頁(yè)面中需要,并通過(guò) import() 調(diào)用動(dòng)態(tài)加載。
如果你知道需要這些塊,你可以通過(guò)插入預(yù)加載鏈接標(biāo)記來(lái)解決這個(gè)問(wèn)題。
但是,你會(huì)看到,與總頁(yè)面加載時(shí)間相比,這樣做的好處可能很小。
另外,使用預(yù)加載有時(shí)會(huì)適得其反,因?yàn)榧虞d其他更重要的文件時(shí)可能會(huì)延遲。
加載頁(yè)面數(shù)據(jù)
你的應(yīng)用程序可能是用來(lái)顯示一些數(shù)據(jù)的。下面是一些提示,你可以使用這些提示盡早加載數(shù)據(jù)并避免呈現(xiàn)延遲。
在開(kāi)始加載數(shù)據(jù)之前不要等待包
這是一個(gè)順序請(qǐng)求鏈的特殊情況:你加載應(yīng)用程序包,然后代碼請(qǐng)求頁(yè)面數(shù)據(jù)。
有兩種方法可以避免這種情況:
- 將頁(yè)面數(shù)據(jù)嵌入HTML文檔中
- 通過(guò)文檔中的內(nèi)聯(lián)腳本啟動(dòng)數(shù)據(jù)請(qǐng)求
將數(shù)據(jù)嵌入HTML可以確保你的應(yīng)用程序不必等待數(shù)據(jù)加載。這也降低了應(yīng)用程序的復(fù)雜性,因?yàn)槟悴槐靥幚砑虞d狀態(tài)。
但是,如果獲取數(shù)據(jù)會(huì)大大延遲你的文檔響應(yīng),那將不是一個(gè)好主意,因?yàn)檫@會(huì)延遲你的初始渲染。
在這種情況下,或者如果你通過(guò)服務(wù)工作者提供緩存的HTML文檔,則可以將內(nèi)聯(lián)腳本嵌入到HTML中以加載此數(shù)據(jù)。你可以將其作為全局 promise 提供,如下所示:
- window.userDataPromise = fetch("/me")
然后,如果數(shù)據(jù)準(zhǔn)備就緒,你的應(yīng)用程序可以立即開(kāi)始渲染,或者等到準(zhǔn)備就緒。
對(duì)于這兩種技術(shù),你都需要知道在應(yīng)用開(kāi)始呈現(xiàn)之前頁(yè)面必須加載哪些數(shù)據(jù)。對(duì)于與用戶(hù)相關(guān)的數(shù)據(jù)(用戶(hù)名,通知 ...),這往往很容易,但是對(duì)于特定于頁(yè)面的內(nèi)容,則比較棘手??紤]確定最重要的頁(yè)面并為這些頁(yè)面編寫(xiě)自定義邏輯。
等待非必需數(shù)據(jù)時(shí)不要阻塞渲染
有時(shí)生成頁(yè)面數(shù)據(jù)需要緩慢的復(fù)雜后端邏輯。在這些情況下,如果足以使你的應(yīng)用程序具有功能性和交互性,則可以首先加載較簡(jiǎn)單的數(shù)據(jù)版本。
例如,分析工具可以在加載圖表數(shù)據(jù)之前首先加載所有圖表的列表。這使用戶(hù)可以立即查找他們感興趣的圖表,還可以幫助將后端請(qǐng)求分散到不同的服務(wù)器上。
避免順序數(shù)據(jù)請(qǐng)求鏈
這可能與我先前關(guān)于在第二個(gè)請(qǐng)求中加載非必需數(shù)據(jù)的觀點(diǎn)相沖突,但是如果每個(gè)完成的請(qǐng)求都不會(huì)導(dǎo)致向用戶(hù)顯示更多信息,則避免順序請(qǐng)求鏈。
與其先發(fā)出關(guān)于用戶(hù)登錄身份的請(qǐng)求,然后再請(qǐng)求其所屬團(tuán)隊(duì)的列表,不如在用戶(hù)信息旁邊返回團(tuán)隊(duì)列表。你可以使用 GraphQL ,但自定義用戶(hù)呢? includeTeams=true endpoint 也很有用。
與其首先請(qǐng)求用戶(hù)登錄為誰(shuí),然后請(qǐng)求他們所屬的團(tuán)隊(duì)列表,
服務(wù)端端渲染
服務(wù)端端渲染意味著在服務(wù)器上預(yù)渲染你的應(yīng)用程序,并使用整頁(yè)HTML響應(yīng)文檔請(qǐng)求。這意味著客戶(hù)端可以看到完全呈現(xiàn)的頁(yè)面,而不必等待加載其他代碼或數(shù)據(jù)!
由于服務(wù)器只是將靜態(tài)HTML發(fā)送給客戶(hù)端,因此你的應(yīng)用尚無(wú)法進(jìn)行交互。需要加載應(yīng)用程序,它需要重新運(yùn)行呈現(xiàn)邏輯,然后將必要的事件偵聽(tīng)器附加到DOM。
如果看到非交互式內(nèi)容很有價(jià)值,請(qǐng)使用服務(wù)器呈現(xiàn)。如果你能夠?qū)⒊尸F(xiàn)的HTML緩存在服務(wù)器上并將其提供給所有用戶(hù)而又不會(huì)延遲初始文檔請(qǐng)求,那么它也將有所幫助。例如,如果你使用 React 來(lái)渲染博客文章,則服務(wù)器渲染非常合適。
下一頁(yè)
在某個(gè)時(shí)候,用戶(hù)將與你的應(yīng)用進(jìn)行交互并轉(zhuǎn)到下一頁(yè)。打開(kāi)初始頁(yè)面后,你可以控制瀏覽器中發(fā)生的事情,因此你可以準(zhǔn)備進(jìn)行下一次交互。
預(yù)取資源
如果你預(yù)加載了下一頁(yè)所需的代碼,則可以消除用戶(hù)啟動(dòng)導(dǎo)航時(shí)的延遲。使用 prefetch 標(biāo)記,或 webpackPrefetch 用于動(dòng)態(tài)導(dǎo)入:
- import(
- /* webpackPrefetch: true, webpackChunkName: "todo-list" */"./TodoList"
- )
注意你使用了多少用戶(hù)數(shù)據(jù)和帶寬,特別是當(dāng)他們使用移動(dòng)連接時(shí)。如果他們使用的是你網(wǎng)站的移動(dòng)版本,或者他們啟用了保存數(shù)據(jù)模式,你可以減少預(yù)加載。
對(duì)于用戶(hù)最可能需要的應(yīng)用程序部分,要有策略。
重用已經(jīng)加載的數(shù)據(jù)
在應(yīng)用程序中本地緩存 Ajax 數(shù)據(jù),并使用它來(lái)避免未來(lái)的請(qǐng)求。如果用戶(hù)從團(tuán)隊(duì)列表導(dǎo)航到“編輯團(tuán)隊(duì)”頁(yè)面,你可以通過(guò)重用已經(jīng)獲取的數(shù)據(jù)來(lái)立即進(jìn)行轉(zhuǎn)換。
請(qǐng)注意,如果你的實(shí)體經(jīng)常被其他用戶(hù)編輯,并且你下載的數(shù)據(jù)可能已經(jīng)過(guò)期,那么這種方法將不起作用。在這些情況下,在獲取最新數(shù)據(jù)時(shí),請(qǐng)首先考慮以只讀方式顯示現(xiàn)有數(shù)據(jù)。
結(jié)論
本文介紹了許多因素,這些因素可能會(huì)在加載過(guò)程的不同時(shí)刻使你的頁(yè)面速度減慢。使用 Chrome DevTools,WebPageTest和Lighthouse之類(lèi)的工具來(lái)確定其中哪些適用于你的應(yīng)用程序。
實(shí)際上,你幾乎不可能在所有方面進(jìn)行優(yōu)化。找出對(duì)用戶(hù)有最大影響的因素,并專(zhuān)注于此。
我在寫(xiě)這篇文章時(shí)意識(shí)到的一件事是,我根深蒂固地相信,發(fā)出許多單獨(dú)的請(qǐng)求對(duì)性能不利。過(guò)去,當(dāng)每個(gè)請(qǐng)求都需要一個(gè)單獨(dú)的連接時(shí),Thas就是這樣,而瀏覽器每個(gè)域只允許幾個(gè)連接。但是,使用 HTTP/2 和現(xiàn)代瀏覽器已不再是這種情況。
并且有強(qiáng)烈的理由支持拆分請(qǐng)求。它允許僅加載必要的資源,并可以更好地利用緩存的內(nèi)容,因?yàn)閮H需要重新加載已更改的文件。