非新手教程,這裡主要列舉一些性能優化的關鍵點。
對於用戶來說,首屏加載頁面主要體現為 Login 登錄頁或者各個角色首頁等。而構建快速加載網站的第一步是及時從伺服器收到頁面 HTML 的響應。當你在瀏覽器的地址欄中輸入 URL 時,瀏覽器會向伺服器發送一個 GET 請求以檢索它,確保 HTML 快速到達且延遲最少是關鍵的性能目標。
TTFB#
HTML 的初始請求會經歷多個步驟,每個步驟都需要花費一些時間,而減少每個步驟所花費的時間可以縮短首次字節時間 (TTFB)。
雖然 TTFB 不是前端關注頁面加載速度時應該關注的唯一指標,但較高的 TTFB 確實會讓達到指定 “良好” 閾值變得困難,例如最大內容繪製 (LCP) 和首次內容繪製 (FCP)。由此可以得出一個簡單結論是:單單靠前端優化無法將性能做到極致,還需要跟後端進行配合。
大多數網站應努力將 TTFB 控制在 0.8 秒或更短。
Server-Timing
響應頭可用於來測量可能導致高延遲的接口。
// Two metrics with descriptions and values
Server-Timing: db;desc="Database";dur=121.3, ssr;desc="Server-side Rendering";dur=212.2
任何帶有 Server-Timing
響應標頭的頁面都可以通過 Navigation Timing API 中的 serverTiming
屬性來獲取:
// Get the serverTiming entry for the first navigation request:
performance.getEntries("navigation")[0].serverTiming.forEach(entry => {
// Log the server timing data:
console.log(entry.name, entry.description, entry.duration);
});
關於如何優化 TTFB 的方法,可以參考這篇文章:
Optimize Time to First Byte | Articles | web.dev
靜態資源響應壓縮#
基於靜態文件(如 HTML、JavaScript、CSS 和 SVG 圖像)響應需要進行壓縮,減小其在網絡上的傳輸成本,以便更快地下載它們。當前使用最廣泛的壓縮算法是 gzip
和 Brotli
,其中 Brotli
比 gzip
提高了約 15% 到 20%。
大多數 CDN 服務提供商通常會自動設置壓縮,但如果能夠自己配置或調整壓縮設置,則儘可能使用 Brotli
。 Brotli
比 gzip
提供了相當明顯的改進,並且所有主流瀏覽器都支持 Brotli
。
儘可能使用 Brotli
,但如果網站本身在舊版瀏覽器上有大量用戶使用,請確保使用 gzip
作為兼容處理,因為任何壓縮都比不壓縮要好。
CDN#
內容分發網絡(CDNs)通過利用一個分佈式的伺服器網絡來向用戶分發資源,從而提升網站性能。由於 CDNs 能夠減輕伺服器的負擔,它們能夠降低伺服器的成本,並且非常適合應對流量突增的情況。
CDN 旨在降低延遲,其通過把資源分發至地理位置離用戶更近的伺服器來實現這一目標,也正因如此,CDN 的核心優勢在於它能夠提升加載性能,特別是,在引入 CDN 後,資源的首字節響應時間(Time to First Byte,TTFB)可以得到顯著改善,而這對 LCP 指標的提升起到了至關重要的作用。
更多關於使用 CDN 提高網站加載速度可以參考該文章:
Content delivery networks (CDNs) | Articles | web.dev
blocking="render"
- 實驗性功能#
作為實驗性功能,現在可以將 blocking=render
作為屬性及其值添加到 <script>
、<style>
或樣式表的 <link>
標籤中,從而明確地設定為渲染阻塞。主要用途是為了防止未加樣式內容的閃現或者防止用戶與一個未完全加載的頁面進行交互,這種情況一般是由腳本插入的腳本 / 樣式表或客戶端的 A/B 測試等引起的。
瀏覽器兼容情況:
"blocking" | Can I use... Support tables for HTML5, CSS3, etc
目前所有的瀏覽器都內置了渲染阻塞機制:頁面導航後,瀏覽器在 <head>
中的所有樣式表及同步腳本加載和處理完畢之前,不會向屏幕渲染任何像素。這樣做可以預防未加樣式內容的閃現現象(FOUC),同時確保諸如框架代碼等關鍵腳本得到執行,使得首次渲染周期後,頁面功能可正常使用。
https://github.com/whatwg/html/pull/7474
CSS#
在最基礎的層面上,CSS 壓縮是一個能夠有效增強網站性能的優化方法,它能夠改善網站的首次內容繪製(First Contentful Paint, FCP),在某些情況下,甚至能改進最大內容繪製(Largest Contentful Paint, LCP)。打包工具(如 Webpack、Vite 等)能在你的生產環境構建中自動執行這些優化。
在渲染頁面內容前,瀏覽器必須下載並解析所有的 CSS 樣式表,這一解析過程還包括了那些目前頁面上未被用到的樣式,這部分樣式實際上是不必要的。如果你使用的打包工具會把所有 CSS 合併到一個文件中,這可能導致用戶下載的 CSS 超出了渲染當前頁面所必需的量。
Chrome DevTools 中的覆蓋率工具可用於檢測當前頁面未使用的 CSS(或 JavaScript)。
JavaScript#
JavaScript 負責大部分網頁的交互功能,但這背後是有成本的。
加載過多的 JavaScript 代碼會使網頁在加載過程中響應緩慢,甚至可能引起交互響應緩慢的問題。
async
和 defer
屬性使外部腳本能在不阻塞 HTML 解析器的情況下加載,而帶 module
類型的腳本(包括內聯腳本)會自動延遲加載。不過,理解 async
和 defer
之間的一些關鍵區別非常重要。
Sourced from https://html.spec.whatwg.org/multipage/scripting.html
使用 async
屬性加載的腳本下載完畢後會立即解析和執行,而通過 defer
屬性加載的腳本則要等到 HTML 文檔解析完畢時才會執行 —— 這與瀏覽器的 DOMContentLoaded 事件同步發生。同時,async
屬性的腳本可能不按順序執行,而 defer
屬性的腳本會依照在頁面中出現的順序依次執行。
此外,JavaScript 的壓縮比對其他資源的壓縮(例如 CSS)進行得更徹底,在 JavaScript 壓縮的過程中,不只是移除了空格、制表符和註釋等非代碼內容,源代碼中的變量和函數名也會被縮短。這個過程有時也被稱作 “醜化”(uglification)。
Preconnect
預連接#
通過使用 preconnect
,可以預測到瀏覽器即將需要連接到一個特定的跨域伺服器,並且瀏覽器應當立刻開啟該連接,理想情況下是在 HTML 解析器或預加載掃描器開始工作之前。
preconnect
常用於 Google 字體服務。Google 字體建議預先連接到 https://fonts.googleapis.com 域名,它用於提供 @font-face 聲明,同時也建議連接到 https://fonts.gstatic.com 域名,該域名用於提供字體文件。
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
dns-prefetch
dns 預取#
雖然提前打開跨域伺服器的連接可以顯著加快頁面最初的加載時間,但讓網頁同時建立多個跨域連接可能既不合理也不可行。
如果擔心可能在過度使用 preconnect
,一個更節省資源的辦法是採用 dns-prefetch
。正如其名稱所示,dns-prefetch
並不是建立伺服器連接,而是只進行域名的 DNS 解析。
在將域名解析為相對應的 IP 地址的過程中,雖然設備和網絡級別的 DNS 緩存能夠加速這一過程,但仍然會消耗一些時間。
preload
預加載#
preload
指令用於提前請求頁面渲染必須的資源:
<link rel="preload" href="/font.woff2" as="font" crossorigin>
如果 <link>
元素在 preload
指令中沒有設置 as
屬性,資源會被下載兩次。關於各種 as
屬性的值,可查閱 MDN 文檔中的相關說明。
prefetch
預取#
prefetch
指令用於啟動對可能在未來導航中用到的資源的低優先級請求:
<link rel="prefetch" href="/next-page.css" as="style">
在某些情況下,prefetch
可以很有幫助 —— 比如,如果你識別出網站上大多數用戶都會遵循的特定用戶流程,對這些未來頁面的關鍵資源進行預加載(prefetch
)有助於縮短它們的加載時間。
注意:由於 prefetch
本質上是一種推測性操作,它的一個潛在缺點是,如果用戶沒有訪問到最終需要該預加載資源的頁面,那麼獲取資源所消耗的數據可能就會浪費。你需要依據網站的分析數據或其他使用模式信息來決定是否應用 prefetch
。此外,對於那些設置了減少數據使用偏好的用戶,你也可以利用 Save-Data
提示來避免進行預加載操作。
通常建議避免使用 <link rel="prefetch">
預獲取跨域文檔,有一個關於預獲取跨域文檔的公開問題,它會導致發起重複請求。 同樣,應該避免預獲取那些已個性化的同源文檔 —— 比如,為認證會話動態生成的 HTML 響應,因為這類資源一般不會被緩存,極可能閒置不用,從而最終浪費了帶寬。
在基於 Chromium 的瀏覽器中,可以利用 Speculation Rules API 來預獲取文檔,Speculation Rules 定義為 JSON 對象,可以內嵌於頁面的 HTML,或通過 JavaScript 動態注入:
<script type="speculationrules">
{
"prefetch": [{
"source": "list",
"urls": ["/page-a", "/page-b"]
}]
}
</script>
像 Quicklink 這類庫,通過對用戶可視範圍內的頁面鏈接進行動態預獲取或預渲染,來優化頁面導航體驗,與預獲取頁面上所有鏈接的方式相比,這種方法更能提高用戶最終瀏覽到這些頁面的概率。
Prerender
預渲染頁面#
除了預獲取資源,還可以通過瀏覽器來提前渲染用戶即將訪問的頁面。這種做法能夠實現近乎瞬時的頁面加載,因為頁面及其資源已經在後台加載並處理好了。當用戶訪問該頁面時,它會立即顯示。
預渲染功能可以通過 Speculation Rules API 來實現:
<script type="speculationrules">
{
"prerender": [
{
"source": "list",
"urls": ["/page-a", "page-b"]
}
]
}
</script>
Chrome 同樣支持使用 <link rel="prerender" href="/page">
這種資源提示方式。不過,從 Chrome 63 開始,這種做法引入了無狀態預獲取 (NoState Prefetch),僅用於加載頁面所需的資源,而不會進行頁面渲染或執行 JavaScript。
完整預渲染也會運行預渲染頁面中的 JavaScript,鑑於 JavaScript 是一種體積大、計算量高的資源,建議儘可能謹慎地使用 prerender
,並且只有在您确信用戶準備訪問那個預渲染頁面的情況下才使用。
Service Worker 預緩存#
Service worker 的預緩存功能可以借助 Cache API 去獲取並存儲資源,這使得瀏覽器能夠僅通過 Cache API 響應請求,而無需聯網操作。Service worker 預緩存採用了一種非常高效的緩存策略,即所謂的 “僅緩存策略”,這種方式極其高效,資源一旦存入 service worker 緩存,在請求時可以幾乎瞬間被取用。
要利用 service worker 對資源進行預緩存,可以採用 Workbox 這個工具。當然,如果更希望手動控制,也完全可以自己編寫代碼來緩存特定的文件集合。
無論你以哪種方式來實現資源預緩存,都必須明白,這個過程是在 service worker 安裝的時候進行的。一旦安裝完成,所有預緩存的資源就可以被你網站上任何一個由 service worker 管理的頁面調用和使用。
Workbox | Chrome for Developers
像使用資源提示或猜測規則進行資源的預獲取或預渲染一樣,service worker 的預緩存同樣會佔用網絡帶寬、存儲空間和 CPU 處理能力。因此,建議只對可能會被使用的資源進行預緩存,避免在預緩存列表中包含過多的資源。當不確定需要預緩存哪些資源時,寧可少緩存一些,而將填充 service worker 緩存的工作交給運行時緩存,並採用多種模式以協調加載速度與資源更新度。要獲取關於預緩存資源的更多實踐提示和禁忌,請閱讀《預緩存的正確與錯誤之道》。
Fetch Priority API
#
通過 fetchpriority
屬性,可以利用 [Fetch Priority API](https://web.dev/articles/fetch-priority)
提升資源的加載優先級。該屬性適用於 <link>
、<img>
和 <script>
元素。
<div class="gallery">
<div class="poster">
<img src="img/poster-1.jpg" fetchpriority="high">
</div>
<div class="thumbnails">
<img src="img/thumbnail-2.jpg" fetchpriority="low">
</div>
</div>
圖像#
圖像通常是網頁中體積最大且最常見的資源。因此,優化圖像可以顯著提高網頁的性能。大多數情況下,優化圖像意味著減少傳輸的數據量來縮短網絡傳輸時間,但也可以通過提供適合用戶設備大小的圖像來優化傳輸數據量。
現代瀏覽器支持多種圖像文件格式。相比 PNG 或 JPEG,現代圖像格式如 WebP 和 AVIF 能提供更好的壓縮效果,從而使圖像文件體積更小,下載時間更短。使用現代格式服務圖像,能夠減少資源的加載時間,可能會降低最大內容渲染(Largest Contentful Paint, LCP)。
JavaScript 代碼拆分#
加載大型 JavaScript 資源會嚴重影響頁面加載速度,如果把 JavaScript 分割成更小的分塊 (chunk),並且在頁面啟動時只下載對頁面功能來說必要的代碼,能夠顯著提升頁面的加載響應速度。
當頁面下載、解析並編譯大型 JavaScript 文件時,可能會出現暫時性的無響應狀態,雖然頁面元素已經可見,因為它們屬於頁面最初的 HTML,且已應用 CSS 樣式。然而,負責驅動這些互動元素的 JavaScript 以及其他被頁面加載的腳本可能正在執行,導致它們功能失常,這導致用戶可能感受到交互被明顯延遲,或甚至完全不可用。
Lighthouse 會在 JavaScript 執行時間超過 2 秒時顯示警告,超過 3.5 秒時則判斷為失敗。無論在頁面生命周期的哪個階段,過度的 JavaScript 解析和執行都有可能問題,因為這可能會在用戶與頁面互動時增加輸入延遲,尤其是與 JavaScript 處理和執行任務的主線程同步運行時。
此外,過量的 JavaScript 執行和解析在頁面初始化加載期間特別問題,因為這時用戶非常可能與頁面進行互動。實際上,總阻塞時間(Total Blocking Time, TBT)— 一項衡量加載響應性的指標 — 與交互到下一次繪製(INP)高度相關,這意味著用戶在頁面最初加載時嘗試互動的可能性很高。
通過使用動態 import()
函數,可以實現代碼分割。這種函數 —— 與在啟動時請求特定 JavaScript 資源的 <script>
元素不同 —— 可以在頁面生命周期的後期階段請求 JavaScript 資源。
document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
// Get the form validation named export from the module through destructuring:
const { validateForm } = await import('/validate-form.mjs');
// Validate the form:
validateForm();
}, { once: true });