引言
在當今用户體驗至上的數字時代,前端性能已不僅僅是技術指標,更是直接影響用户留存、轉化率和產品口碑的關鍵因素。然而,面對層出不窮的性能優化文章和碎片化的優化技巧,很多開發者陷入了"知其然而不知其所以然"的困境——我們熟悉各種優化手段,卻難以構建系統化的性能認知體系。
本文旨在打破這一僵局。通過深入分析前端性能的本質問題,我們提出了一套全新的六層性能優化模型,該模型將從前端工程化構建開始,逐層深入至網絡傳輸、應用加載、瀏覽器運行時、JS執行引擎,最終迴歸用户體驗與業務場景,構建起完整的性能優化知識圖譜。
不同於簡單的"優化清單",本文將揭示各優化層級的因果關係和底層原理:為什麼Vite能比Webpack更快?微任務如何阻塞主線程渲染?隱藏類怎樣影響代碼執行性能?更重要的是,我們將探討如何根據不同的業務場景,在性能與成本之間找到最佳平衡點。
前端性能優化模型
構建層(前端工程化)
如果簡單分析一下,構建打包層面常見的優化手段有 tree-shaing、壓縮和代碼分割等,這些主要是優化 bundle 體積,影響的是網絡傳輸,所以應該被歸類到網絡傳輸層。但是實際上,由於前端工程化的多樣性和複雜性,除了上述的打包優化手段,還可能涉及到工程化架構、代碼組織方式和微前端等方面,更重要的是,前端性能優化不單單的對用户來説,對於開發者來説,冷啓動和熱更新的性能也需要關注,因此構建層單獨作為第一層。
生產性能
減少 bundle 體積
Bundle 的體積將直接關係到頁面加載速度,bundle 體積越小,那麼網絡傳輸速度不變的前提下,網絡傳輸用時將減少,從而提高用户體驗。
在前端工程化中,幾乎所有主流打包工具都支持對 bundle 體積的分析和優化,其中最常用的優化方式如下:
| 優化方式 | 説明 | |
|---|---|---|
| 代碼分割 | 可以顯著降低首屏加載體積 | |
| Tree-Shaking | 自動剔除未使用的 JS 代碼 | |
| 輕量級依賴 | 例如用 dayjs 替換 moment | |
| 代碼壓縮 | 如刪除無用的空格和換行符,精簡變量名等,從而使 bundle 體積減少 | |
| 按需引入 | Ant Design、Element Plus 等均支持 babel-plugin 按需加載 | |
| 提取公共 vendor. js | 將公共依賴提取為 vendorjs 單一 bundle 文件加載,在微前端中,可以避免後續子系統重複加載 |
優化開發性能
開發性能主要需要關注兩方面:啓動和熱更新(HMR),其中啓動又分為冷啓動和熱啓動。現代大部分主流打包工具(如 vite、rspack 等)其實性能都已經非常優秀,然而某些情況下,還是可能會遇到開發性能問題,比如老項目中採用 webpack 打包。
Vite
Vite 啓動速度和熱更新速度都很快,這得益於 Vite 底層多方面的優化與設計,關鍵的一點在於,Vite 在開發環境下並不會將所有文件都打包成 bundle,而是藉助 ESM 特性進行按需加載,這是 Vite 那麼快的關鍵原因之一。
借用 Vite 官方的兩張圖可以很形象地展示兩者之間的區別。
此外,Vite 還通過使用 Esbuild 預構建依賴、使用 HTTP 緩存等多種方式來優化啓動和熱更新速度,這使得 Vite 的性能通常要比 webpack 要好。
Webpack 優化:Vite 所帶來的啓發
雖然 Vite 性能非常好,但是在某些情況下,我們還是不得不用 webpack,比如在開發一些老項目時。
webpack 比 vite 性能要差的核心原因在於,webpack 是基於 Bundle 打包的,而 vite 在開發環境下是基於 module 按需打包,這使得 webpack 每次啓動都需要打包大量文件,而 vite 則會在第一次構建(或者修改依賴或配置時)預構建依賴,只需要打包使用到的源代碼。
因此,如果我們能夠讓 webpack 少打包寫文件,只構建我們所需要的文件,那麼速度自然就能快起來了。
突破點在 router,以 vue-router 為例:
// 偽代碼
export routes = [{
path: '/a',
componet: /* webpackChunkName: a */ ()=>import('@/views/a.vue')
}]
在 Vue 實際消費這個 routes 時,我們可以提前攔截處理
import openRoutes from "./route-config"
// 這裏簡單展示思想只處理最外層,實際開發需要深度遍歷樹
const handledRoutes = routes.forEach((route)=>{
const component = route.component // 保存原組件
if(!openRoutes.includes(route)){
route.component = ()=>{
// 空組件,優化性能
return {
render(h){
return h('span', null)
}
}
}
}
return route
})
export default handledRoutes
這種方式的缺點在於需要手動配置打包的路徑,並且每次修改後都需要重啓 webpack,但是後面可以通過 webpack 插件來簡化這一過程。
網絡傳輸層
當用户打開一個頁面時,瀏覽器會自動發起對應 HTML 文件的請求,瀏覽器中的 HTML 解析器首先會加載並解析 HTML 文件,構建成 DOM,與此同時 HTML 解析器和預解析器還會加載 HTML 中聲明或 Script 中 JS 動態創建的資源(CSS、JS 、音視頻等)。
即使在構建層已經儘量壓縮了資源的體積,但是如果網絡傳輸中存在網絡延遲過高等問題,也依然會影響系統性能,因此網絡傳輸層也是我們關注的重點。可以通過 TTFB 等指標來分析網絡傳輸層的性能,我們可以通過 CDN、HTTP 緩存、keep-alive長鏈接等手段來優化這一層的性能問題。
除了網絡方面的問題外,從前端的角度來看,需要額外關注兩點:
- 隊頭阻塞
- 減少不必要、重複的請求
前者可以藉助 HTTP 2 和請求隊列等方式優化,後者則可以藉助節流和防抖等技術緩解。
總結來説,網絡傳輸層優化的本質,就是提高網絡傳輸速度、緩解隊頭阻塞問題、減少不必要的重複請求。
應用加載層
事實上,當從 HTML 被瀏覽器解析開始算起,就可以看作是已經進入到了瀏覽器的運行時一層中,但是為什麼還是單獨拆分了“應用加載”這一層?這是因為對於很多(toC)應用來説,首次渲染太重要了,它甚至直接決定了用户是否願意使用這個應用,畢竟用户等待的耐心是有限度的。
應用加載層可以理解為包含從 HTML 被瀏覽器加載到首次渲染這一整個過程,概括起來就是首次加載和首次渲染。
渲染架構
前端常見渲染架構有 SSR(服務端渲染)、CSR(客户端渲染)和 SSG(靜態站點生成),三者的渲染策略不同,主要區別如下:
| 渲染架構 | 渲染策略 | 適用場景 | 常見框架 |
|---|---|---|---|
| SSR | 服務端每次收到請求後動態地構建 HTML,服務器通過訪問數據庫或者發送請求獲取最新數據並填充到 HTML 中 | 新聞、官網等網站 | Next、Nuxt 等 |
| CSR | HTML 只有一個容器節點,頁面內容主要都是通過加載的 JS 動態創建 DOM 生成的,數據主要通過在瀏覽器中發送請求獲取 | 中後台系統 | Vue、React 等 |
| SSG | 構建過程中就生成了後面所需要的全部靜態資源,除非重新構建,否則內容不會更新 | 博客、文檔等靜態網站 | VuePress、Hexo 等 |
三者的優缺點如下:
| 維度 | SSR | CSR | SSG |
|---|---|---|---|
| DOM 生成來源 | HTML | JS | HTML |
| SEO | 好 | 差 | 非常好 |
| 時效性 | 非常好 | 好 | 非常差 |
| 首次加載性能 | 好 | 差 | 非常好 |
| 服務器壓力 | 高 | 低 | 非常低 |
| 瀏覽器壓力 | 低 | 高 | 低 |
總的來説,SSG 適合對時效性要求低、對 SEO 要求高的場景、且這種方式性能也非常好,如果對時效性和首次加載性能要求較高,那麼通常可以選擇 SSR 架構、如果對 SEO 和首次加載性能不在意,且服務器資源有限,則可以選擇 CSR 架構。
當然,除了上面三種典型的前端渲染架構外,還有一些新興的渲染架構,比如 ISR(增量靜態再生)。傳統的 SSR 每次收到請求都會重新生成一份新的 HTML,雖然時效性好,但是對服務器的壓力非常高,而 SSG 只在構建過程中才生成 HTML,此後不再生成 HTML,雖然性能非常好,但是時效性非常差,而 ISR 則是在兩者之間取得一個平衡。
和 SSR 一樣,ISR 也會獲取最新數據填充到 HTML,並返回給瀏覽器端,但是與 SSR 不同的時 ISR 會採用緩存策略,在緩存過期前,服務器每次收到都會直接返回之前生成的 HTML,當緩存過期後收到請求,服務器仍然會直接返回之前生成的 HTML,但是此時服務器會獲取最新數據重新生成 HTML,如果後面再次收到請求,則是返回後面生成的最新的那份 HTML。
性能優化是取捨與平衡的藝術,不可能面面俱到,每種渲染架構都有各自的優缺點和適用場景,根據不同的需求採用不同的架構,才有可能得到最理想的性能。
資源加載策略
避免阻塞
應用加載需要關注阻塞問題,這是因為 HTML 解析器解析 HTML 時,會被 script 標籤阻塞,script 的加載和執行都會阻塞 HTML 解析器,影響整體的首次加載速度。避免 script 的阻塞包括使用 defer 和 async。兩者區別如下:
- async:異步加載,並行解析執行
- defer: 異步加載,延遲到 DOM 解析完後執行
<script type="module"> 默認會異步加載,延遲執行,也就是説下面兩種寫法效果是一樣的。
<script type="module"><script type="module" defer>
此外,css 中的 @import 也會阻塞解析,因此在優化首次渲染時需要額外注意。
預加載和預獲取
在 HTML 規範中,資源的加載是有優先級的,通常情況下我們不需要特意關注,但是在一些情況下,我們可以利用瀏覽器提供的 API 來利用資源加載優先級來優化性能。
常見的 API 有 preload 和 prefetch,前者是預加載,可以在應用早期就儘快地加載資源,後者是預獲取,可以提前加載後續可能用得到的資源。
舉一個簡單的場景説明下這兩個 API,在一個電商平台的首頁,輪播圖圖片可能比較大,加載時間會比較長,影響用户體驗,但是如果設置了 preload,那麼就可以確保瀏覽器會盡早地加載這張圖片,減少圖片加載時間;首頁輪播圖的下面是熱賣商品鏈接,如果設置了 prefetch 預獲取,那麼瀏覽器就會在空閒時提前加載商品鏈接中的資源,你點擊這些鏈接時就可以很快地進入到頁面中。
// layout.jsx
export default function Layout(){
return <>
{/* ... */}
<link rel="preload" as="image" href="/banner.jpg" />
</>
}
// Page.jsx
export default function Page() {
// 實際上Next的Image組件已經封裝了preload,這裏只是為了演示,實際寫法更簡單
return (
<img src="/banner.jpg" alt="首頁輪播圖"/>
<Link href="/shop/xxx" prefetch>
商品鏈接
</Link>
);
}
首屏優先加載策略
應用加載層追求的是首次加載渲染速度儘可能的快,也就是白屏時間儘可能的短,而如果需要做到這一點,除了做好前面的架構選擇和減少阻塞外,還得實現首屏優先加載策略。
所謂的首屏優先加載策略,核心就是首次加載只加載首次屏幕上能看到的內容,而看不到的內容延遲加載。一個常見的例子是購物網站,剛進去時只會渲染屏幕視口中的商品,如果剛進去時快速滾動頁面,可以發現首屏下面的內容還沒有加載。首屏視口中的內容用户最先看到,因此優先級最高,需要儘可能早的加載渲染,而首屏視口後面的內容用户後面才會看到,因此優先級相對較低,延遲加載渲染,這有利於提高用户體驗。
我們用 React 來展示一個簡單版本的首屏優先加載策略實現方式:
// first.jsx
function FirstComp(){
return <div className="px-2">首屏內容</div>
}
// second.jsx
export default function SecondComp(){
return <div>延遲加載</div>
}
// home.jsx
import {useEffect, useState} from "react"
import dynamic from "next/dynamic"
const SecondCompLazy = dynamic(()=>import('./second'),{
ssr: false, // 不參與 SSR
loading: () => <div>Loading...</div>, // 佔位符
})
function Page(){
const [isLoadComp, setIsLoadComp] = useState(false)
useEffect(()=>{
setTimeout(()=>setIsLoadComp(true), 1000)
}, [])
return <div>
<FirstComp />
{
isLoadComp && <SecondCompLazy />
}
</div>
}
運行時
當應用加載完畢且完成首次渲染後,就可以認為進入了運行時階段。瀏覽器存在有一個執行棧和事件循環模型,這是 JS 執行和渲染的基石,事件循環模型每個週期都會檢測是否需要重新渲染,當用户與頁面進行交互時,可能會觸發重新渲染,則會重新前面的渲染管線中的操作,不過不需要重新加載 HTML。
運行時的主要瓶頸在於主線程和渲染管線,主線程除了執行 JS 外還負責 GC 和渲染的部分任務,因此如果長任務阻塞主線程,那麼就很容易造成丟幀卡頓。另一個瓶頸在於渲染管線,渲染管線是由主線程、合成線程和 GPU 共同完成的,即使主線程未被阻塞,大量重複渲染也依然可能影響合成線程和 GPU 的性能,從而導致卡頓。
主線程
JS 是單線程的,通過執行棧和事件循環模式實現同步和異步任務的調度和執行。JS 單線程模型簡化的開發複雜度,但是同時也帶來了一個問題:阻塞。瀏覽器中主線程通常是“身兼多職”,除了執行 JS 任務外,還需要負責垃圾回收、頁面渲染(部分)等任務,因此阻塞主線程會影響的影響非常大。
好在,JS 引擎一直在嘗試緩解這個問題,例如在垃圾回收中,V8 引擎通過空閒時間 GC、併發、並行等多種技術來進行優化,減少對主線程的阻塞。但是如果開發者忽視主線程的 “負載邊界”,寫出不合理的代碼,依然會讓引擎的優化效果大打折扣,甚至直接觸發主線程阻塞,導致頁面卡頓、交互延遲等問題。
主線程優化的核心在於避免主線程阻塞,對於避免主線程阻塞,很多人都認為主要是避免長任務,但是實際上,JS 任務可以分為同步任務、宏任務和微任務,同步任務的優先級最高,會立即執行,微任務和宏任務屬於異步任務,調度時機受到事件循環機制的控制。在一個事件循環週期內,首先會執行一個宏任務,然後執行完所有微任務,之後檢查是否需要更新渲染。
因此對於主線程來説,微任務和同步任務都有可能會阻塞主線程,例如在微任務中創建新的微任務。下面代碼會阻塞主線程,導致頁面卡死崩潰。
function createMicroTask(){
Promise.resolve().then(createMicroTask)
}
主線程的優化關鍵在於非阻塞,在不同維度有不同的處理方法,下面列出了一些常見的優化思路。
| 優化方法 | 原理 |
|---|---|
| Web worker | 新開線程,避免單一線程阻塞 |
| 時間切片 | 將長任務拆分成多個更細的任務,然後間隔時間插入到主線程中執行,避免長時間連續性的阻塞。 |
| 防抖/節流 | 緩解高頻事件的執行 |
時間切片可以採用 setTimeout 定時器、MessageChannel、requestAnimationFrame 或者 requestIdleCallback,但是注意不要採用微任務 API,例如 queueMicrotask,因為事件循環機制一個週期內會執行完所有微任務,不符合時間切片間隔插入主線程的要求。
渲染管線
渲染管線(Rendering Pipeline)是指瀏覽器從接收 HTML 到最終轉換成屏幕中像素這一有序、協同、流水線式的處理步驟。對於前端來説,渲染管線尤為重要,如果渲染管線被阻塞,那麼瀏覽器就會因為沒有及時完成渲染幀而出現“丟幀”現象,從而導致頁面卡頓。
不同瀏覽器的渲染管線步驟並不完全相同,但是大部分瀏覽器的渲染管線通常都包含下面這幾個步驟:
- 解析:瀏覽器加載 HTML,HTML 解析器解析 HTML 並轉換為 DOM 樹,預解析器非阻塞性地加載資源,CSS 解析器解析 CSS。
- 樣式計算:CSS 引擎遍歷 DOM 樹並應用 CSS 規則,生成渲染樹
- 佈局:計算每個元素的寬高、位置等幾何屬性
- 繪製:CPU生成繪製指令
- 合成:合成線程將繪製指令傳遞給 GPU 進程,由 GPU 進程中的光柵化線程池進行光柵化,最終渲染到屏幕上。
有關渲染管線的具體流程以及常見瀏覽器渲染引擎之間渲染管線的差異詳見[[深入瀏覽器引擎 III:Chromium分層合成 vs Firefox WebRender的GPU革命]]
渲染管線優化的核心在於消除渲染管線中各流程節點的性能瓶頸。我們所看到的頁面和動畫(如頁面滾動)都是經過渲染管線處理後生成的渲染幀組成的。例如常見的屏幕刷新率是 60 HZ,這意味着理想狀態下應該在 1s 內生成 60 個渲染幀,也就是 16.66 ms 生成一幀,才能保證頁面動畫的流暢性。如果因為阻塞或者高強度的渲染導致無法及時地生成渲染幀,就會導致頁面卡頓。
下面是一些常見的優化渲染管線的方法,需要注意的是,由於主線程也承擔渲染管線的部分任務,因此主線程的優化其實也是渲染管線優化的一部分,不過有關主線程的優化已經在上一節內容解析過,這裏就不再贅述了。
| 方法 | 原理 |
|---|---|
| 減少迴流、重繪 | 減少重複渲染 |
| 減少強制同步佈局 | 減少無用渲染 |
| 使用 opacity、transform 來實現動畫 | 啓動 GPU 加速,性能比 position 更好 |
| 通過 will-change | 提升圖層單獨渲染,並告知瀏覽器提前做好優化準備,但是要避免濫用 |
| 懶加載/虛擬滾動 | 避免短時間大量的渲染 |
| Script 添加 defer 或 async 屬性 | 避免阻塞 HTML 解析 |
content-visibility: auto |
跳過不可見內容的渲染 |
| contain 屬性 | 告訴瀏覽器將元素視為獨立渲染單元,限制其對頁面其他部分的影響 |
內存管理
由於 JS 引擎會自動進行垃圾回收,並沒有暴露手動內存分配與回收的相關 API,因此內存問題也很容易被忽略,但是一旦發生內存問題,那麼影響也會很大,例如內存泄露很容易導致頁面崩潰,影響用户體驗。
常見的前端內存溢出問題有兩種:
- 棧溢出
- 堆溢出
棧溢出是指執行棧中任務數量超出執行棧的最大限制,這通常是大量遞歸導致的,相較而言比較容易識別和處理,常見的優化方式是將遞歸改為迭代。
堆溢出則是堆空間出現內存溢出,有兩種常見的情況可能導致堆溢出
- 內存泄漏
- 消費不及時(生產速度大於消費速度)
內存泄漏是一個很深很廣的範圍,例如筆者曾經遇到並解決過 Vue Devtools 的內存泄漏問題,最終排查發現是 Vue Devtools 時間軸功能默認開啓並長時間深拷貝 Vuex 執行 commit 中的參數導致的,由於 Vue Devtools 將這些數據保存在全局變量中,導致垃圾回收機制無法進行回收。
要避免或者解決內存泄露問題,就必須要了解瀏覽器的垃圾回收機制和 JS 底層原理,瀏覽器垃圾回收機制可以參考這篇文章[[深入瀏覽器引擎 IV:V8 垃圾回收機制]],JS 底層原理可以參考筆者的《瀏覽器篇》和《深入 JavaScript 篇》,這裏不再贅述。
“消費不及時”這種情況也會導致內存溢出問題,例如在 node 服務端如果將日誌保存在數據庫中,由於數據庫是文件存儲,存儲速度(消費速度)較慢,而日誌生產速度較快,導致消費速度小於生產速度,從而導致內存累積,直到出現內存溢出。
又比如在瀏覽器發送日誌的場景中,由於擔心頻繁地進行日誌發送可能導致瀏覽器隊頭阻塞,所以可能會採用緩存隊列的方式來按順序、間隔地發送日誌,但是如果發送速度小於日誌的產生速度,那麼就很可能導致內存溢出。
對於“消費不及時”這種內存問題的最佳方法是設立閾值和建立監控,例如上面那個瀏覽器採用緩存隊列發送日誌的例子,如果定期地檢查緩存隊列的長度,當緩存隊列的長度超過規定閾值時,則改為使用批量發送的方式並降低發送間隔時間,直到緩存隊列的長度降到安全的範圍。
在瀏覽器中,對於堆內存問題的定位和修復通常需要藉助 devtools 內存工具,這部分後面會出一篇專門的文章介紹。
一個例子
可以用一個常見的場景來體現着三層性能優化的重要性:長列表。
例如在某個移動端社交小程序中需要以長列表的方式展示評論,如果不考慮任何優化,一次性加載所有數據,那麼渲染管線會在短時間內承擔大量的渲染任務,與渲染管線有關的主線程、合成線程、GPU進程以及其他相關的線程都會受到影響,造成性能懸崖。
此時你會想到採用懶加載的形式去優化長列表,但是會發現當加載的節點達到一定數值時,會造成內存溢出,從而導致內存崩潰。這是由於大量節點和相關內存佔用(例如事件處理程序)過多,影響了內存管理。
你會進一步優化,採用事件委託+虛擬滾動(又稱虛擬列表)技術來進一步優化內存。虛擬滾動簡單來講就是通過計算來確定當前應該展示列表中哪幾項在屏幕視口中,不再視口中的項由於用户不可見,因此不需要渲染,從而節省內存佔用。
例如列表中的每一項高度是 100px,當前滾動偏移量是 1000px,視口高度是 700px,那麼此時應該渲染 list.slice(1000/100, (1000/100) + (700/100)) 範圍內的元素。當然實際實現時會更加複雜,比如需要在計算好的範圍內+-5 來設置緩存區,避免在用户快速滑動時出現由於渲染不及時導致的空白問題,這裏就不展開講述了。
在這個方案中需要監聽 scroll 事件來確定滾動偏移量,但是如果你沒有采用防抖技術來優化,而是直接在 el.addEventListener('scroll', ()=>{/**/}) 中進行計算,那麼由於 scroll 事件觸發非常頻繁,會導致主線程在短時間內承擔大量的計算工作,從而阻塞主線程,因此這種場景下通常需要加上防抖來優化性能。
通過上述步驟的逐步優化,我們可以實現一個簡易版虛擬滾動方案。該方案充分兼顧了渲染管線、內存管理和主線程三個層面的性能 —— 正如短板效應所示,唯有三者性能均得到優化,才能最終實現頁面的卓越性能。
執行引擎層
在這一層中,你需要深入理解更加底層的知識才能進行更好的優化,可以簡單的劃分成兩層:
- 框架層
-
JS 層
框架層
深入理解框架底層可以幫忙我們寫出性能更好的代碼,例如對於 Vue 和 React 這些基於虛擬 DOM 的框架,列表節點通常建議採用唯一固定值作為列表元素的 key 值,這是為了在框架底層進行 diff 時來標識節點,以 Vue3 為例,有 key 和沒有 key 兩種場景下 Vue 採用的策略完全不一樣,性能上也有較大差異。
| 策略 | 性能 | |
|---|---|---|
| 有 key | 通過key精確複用節點,利用LIS最長遞增子序列優化移動 | 接近 O (n) |
| 無 key | 儘量複用節點,但是由於缺少key,無法精確找到對應的節點 | 最壞情況是 O (n^2) |
如果你對key的不夠了解,可能會將index設置為key,但是index並不是穩定的,加入中間某個節點被刪除,那麼同一個節點在新舊節點的key就不相同,導致複用錯誤,從而影響性能。
除此之外,比如React會提供useMemo和useCallback等API來優化性能,Vue提供computed等API來優化性能,通過了解使用框架的設計思想和實現方式,可以更好地利用框架底層機制來優化性能,寫出更優雅的代碼。
JS 引擎層
JS 是一個弱類型的解釋型語言,通過 JS 引擎來優化、執行,常見的 JS 引擎如下所示:
| 地位 | 應用 | |
|---|---|---|
| SpiderMonkey | 世界上第一個 JavaScript 引擎 | Firefox 瀏覽器 |
| V8 | 當前應用最廣泛的 JavaScript 引擎 | Chromium、Nodejs |
深入瞭解 JS 引擎能夠幫助開發者在極端性能場景下找到優化突破口,其中隱藏類(Hidden Classes)和內聯緩存便是 JS 引擎普遍採用的關鍵優化手段。簡單來説,隱藏類是 JS 引擎在運行時為對象動態創建的內部數據結構,它的核心作用是將動態的 JavaScript 對象 “靜態化”,從而讓引擎能夠像處理靜態類型語言(如 Java、C++)中的對象一樣高效地訪問屬性。具體可以參考[[深入JavaScript II:從JS引擎看JS的運行與優化]]
以一個例子來對比隱藏類優化生效和未生效兩種情況下的性能:
// ✅ 穩定結構,隱藏類可優化
function createStable() {
return {a: 1, b: 2};
}
// ❌ 動態結構,隱藏類未生效
function createUnstable() {
const obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3
delete obj.c
return obj;
}
// 測試函數
function test(obj) {
for (let i = 0; i < 10e7; i++) {
obj.a = i;
}
}
// === 性能測試 ===
console.time('穩定隱藏類');
test(createStable());
console.timeEnd('穩定隱藏類');
// 穩定隱藏類: 70.7470703125 ms
console.time('不穩定隱藏類');
test(createUnstable());
console.timeEnd('不穩定隱藏類');
// 不穩定隱藏類: 361.491943359375 ms
兩者執行所消耗的時間相差足足有五倍,並且隨着數據量的增多,兩者的性能差異會越來越大。
用户體驗與業務層
前面五層是通用場景優化,覆蓋常見的性能場景,但是最後一層關注的是用户體驗與實際的業務場景。
用户體驗
前端是離用户最近的開發,因此前端不能僅僅關注技術上的性能優化,對於前端而言,有一些可以提高用户感知的性能優化方法,這類方法雖然不能夠在客觀上提高性能,但是可以在用户主觀上提高用户體驗和性能感知,讓用户“感覺加載速度比較快了”。
常見的技術有:
- 骨架屏
- loading
- 佔位圖
-
首屏優先渲染
業務相關
性能優化最終要服務於業務目標,脱離業務場景的“極致優化”往往是資源浪費,甚至可能引入不必要的複雜性。因此,在實際項目中,性能策略必須與業務優先級、用户行為和產品生命週期階段對齊。
例如中後台系統,對於中後台系統而言,用户多為企業內部員工,對 SEO 無要求,但需頻繁操作複雜表單和表格,用户可能可以容忍稍長的首屏加載時間,但是對運行時響應性能要求較高。
總結
這六層優化模型是一種方法論,它並不能直接告訴你用什麼方法去優化,但是可以給你提供優化的方向以及性能瓶頸的判定方法。通過這個性能優化模型,可以將原本零散的性能優化點組織成一個有層次、有因果關係的性能優化體系。