這篇文章是一篇(非常長的)觀點文章,儘管我希望它能透過伴隨的軼事得到一些佐證。簡單來說,根據我的經驗,標題在絕大多數情況下都是正確的陳述,我們應該盡可能偏好更以伺服器為中心的方法。

我用這個術語來指任何依賴將大量 JavaScript 運送到瀏覽器,由瀏覽器執行作為使用 Web 應用程式的內在部分,且經常處於關鍵路徑中的方法。這類方法通常與單頁應用程式 (SPA) 相關,但也不幸地出現在多頁應用程式 (MPA) 中。

如果您不認識我,嗨!👋 我在 Automattic 全職從事 Web 效能工作,屬於一個小型 PerfOps 團隊,我們的使命是改善公司所建置或從事之各種專案的效能。我們的團隊也建置和維護公司的效能監控基礎設施,但我們大部分時間都花在尋找和修復各種堆疊中的效能問題,從優化資料庫查詢到修復卡頓的 CSS 動畫。

個人而言,我專注於瀏覽器端。作為這項工作的一部分,我曾多次處理載入效能問題、執行階段效能問題、套件大小增加、特定框架問題(例如 React 中昂貴的重新渲染)等的除錯工作,並且我一次又一次地看到一些根本原因。這些問題不一定無法克服,但它們加起來揭示了我們多年來被推銷的「現代 Web 開發方法」敘事中一些重要的差距和明顯的謬誤。

我不認為我會帶來任何突破性的發現,也沒有大量的數據來證實任何事情是無可爭議的事實。這一切都基於我個人的經驗,可能不適用於任何特定專案,特別是那些由專注、敬業的團隊維護的小型 Web 應用程式。

另外請注意,我會在文章中跳躍於 Web 效能的不同面向,不經仔細思考。這是因為雖然它們各不相同,但都很重要!您不會希望應用程式載入時間過長,也不會希望它在渲染使用者輸入的字元時反應遲鈍,或者在應用程式內導航時出現破壞性的延遲。

撇開這些說明,讓我們來看看主要主題:所謂「現代」開發的長期效能特性,這通常涉及某種框架,例如 React。

我知道關於 React 是框架還是函式庫有一些討論,有些人——特別是作者和維護者——堅持稱其為函式庫。我不知道他們為什麼這麼做,但就約定的術語和共享詞彙而言,這顯然是錯誤的。

在電腦科學中,兩者之間普遍接受的區別是,函式庫是您的程式碼調用的東西,而框架是您將控制權交給它,讓它在適當的時候調用您的程式碼。也就是說,框架透過「控制反轉」運作,而函式庫則不然。

根據這個定義,React 是一個框架,因為一個慣用的 React 應用程式絕大多數是由 React 調用的程式碼組成的,並處於其控制之下。

像 React 這樣的框架通常被視為加速器,甚至被認為是進行 Web 開發的唯一可行方式。有一種觀念認為,更「現代」的堆疊(讀作:以 JavaScript 為主,最終在使用者瀏覽器上執行的 JavaScript)能讓您更敏捷,更少錯誤地發布,使程式碼更易於維護,並最終推出更好的網站。簡而言之,聲稱這種方法將極大地改善開發者體驗,而這些開發者體驗的益處將惠及使用者。

但多年來,這種說法被證明至少是不切實際的。實際上,對於任何規模相當大的以 JavaScript 為主的專案,您應該預期您建置的內容會比廣告宣傳的慢,隨著持續的開發,它會變得越來越慢,而且開發和維護的難度會比您原先預期的要大,錯誤也和其他方法一樣多。

在效能方面,需要注意的重要一點是,以 JavaScript 為主的方法(特別是基於 React 及相關技術的)很可能不是一個好的起點;事實上,它很可能會變成一個效能的雷區,您需要不斷地重新審視,每一次新的提交都可能引爆。

在這篇文章中,我將首先探討我所見過的最常見問題類別的一些根本原因,然後提出您可以採取的緩解策略來預防其中一些問題。

之後,我將反思以 JavaScript 為主的方法是否值得,效能是否是可選的,然後探討以伺服器為中心開發作為一種替代方案,以及為什麼它往往表現更好。

最後,我將呼籲我們的產業改變我們做事的方式。

一個更直接的問題是,以 JavaScript 為主的方法經常深度依賴 npm 全域套件儲存庫來獲取執行階段的 JavaScript(即使用者瀏覽器將執行的 JavaScript)。雖然這可以作為快速推出產品的捷徑,但它也可能帶來一些不那麼明顯的成本,通常以套件大小來衡量。

注意:雖然套件大小不是使用者直接體驗的指標,但它通常與其他指標(例如各種繪製指標)高度相關,特別是在應用程式的大部分內容作為關鍵路徑載入的情況下。

將大型套件拉入您的套件中是很常見的,例如用於時間和時區管理的龐大函式庫、大量的 JavaScript 工具函式集合,以及用於啟動 UI 建置的整個元件系統。其中一些套件設計良好,並且建置得小巧且模組化,因此您只需為您使用的部分付費。然而,許多套件並非如此,由於其設計(例如像 moment 這樣的流暢 API)、內部大量重複使用自身(例如 lodash-es),或未能考慮到 bundler 需要進行有效的 tree-shaking,最終變得龐大或接近龐大。

大型依賴本身就夠糟糕了,但更糟的是:許多這些套件會隨著時間而變大。這意味著我們不僅背負著沉重的包袱,它還在不斷變重!

這些增加通常隱藏在幕後,並不容易被發現,因為大多數工具預設不會提供這些資訊:

而且更糟糕的是,堅持使用套件的舊版本通常不是一個選項,因為新版本包含安全性或錯誤修復,或者因為另一個套件強制您升級共享依賴。

但假設您在選擇依賴時很謹慎,並且仔細審查了每個版本升級,以確保套件大小不會膨脹。您也小心不要添加太多自己的程式碼。這是否意味著您的 Web 應用程式會很快?

一個穩健、對開發者友善的系統應該讓做正確的事情比做錯的事情更容易。通常不可能完全避免錯誤,但確保遵循錯誤的方法會帶來額外的阻力就足夠了。

不幸的是,在效能方面,以 JavaScript 為主的方法通常表現相反:它們讓做錯事比做對事更容易。

我將在一些範例中專注於 React 和 Redux,因為我對它們的經驗最多,但其中大部分適用於其他框架和以 JavaScript 為主的方法。以下都是我在不同程式碼庫中多次遇到的問題:

開發人員經常被教學資源或文件誤導,這加劇了這個問題。為了讓內容更容易理解和學習,程式碼保持簡單——正如我們所見,直接的策略通常是錯誤的,至少在效能方面是如此。有時文件會提到他們展示的是簡化程式碼片段,而實際生產環境的程式碼可能需要不同。有時則不會。

這些方法是脆弱的,並且讓任何人都可以隨時破壞效能。

這引導我到下一個點。一個最初快速的以 JavaScript 為主的應用程式在開發過程中保持快速的可能性有多大?

如果一個系統是脆弱的,這意味著即使它一開始是穩固的,隨著時間的推移也很難保持完整。

不幸的現實是,其中一些效能問題是轉捩點,因為一個錯誤的舉動可能會有效地使您先前為避免某類效能問題所做的所有努力都白費:

這些問題對經驗豐富的開發者來說可能很明顯,但對於剛加入團隊的新手,或主要經驗是其他框架或函式庫的程式設計師來說,可能就不那麼明顯了。任何依賴開發者紀律來維持效能(或任何其他特徵)的系統都是脆弱的,需要持續的警惕,特別是對於大型團隊或專案。

因此,雖然透過回滾變更或修復新程式碼來解決情況可能很容易,但現實是,如果您沒有仔細監控您的 Web 應用程式,您可能會完全錯過問題,並在不知不覺中將情況弄得比以前更糟,儘管您已經做了所有工作來預防問題。

多年來,我一次又一次地看到這種情況;某些效能改進的努力往往持續不了多久,因為再次破壞事情實在太容易了。

「好吧」,您可能會想,「但這些框架及其相關技術聲稱專注於開發者體驗,這意味著它們肯定也附帶了一些很棒的除錯效能問題的工具,對吧?」

不幸的是,情況往往並非如此。

即使在最好的情況下,效能也從來都不容易除錯。

也就是說,多年來,瀏覽器引擎在建置穩健的 Web 開發工具方面付出了巨大的努力。瀏覽器開發工具幾乎可以讓您深入了解您網站效能的各個方面,甚至了解瀏覽器在載入和渲染您的網站時所做的事情。它們為資源載入顯示易於理解的瀑布圖,以及為 JavaScript 執行和渲染提供有用的火焰圖。它們可以節流 CPU 和網路,並運行結合各種資訊的詳細分析。它們現在甚至可以為您分析這些分析,指出問題並提供建議!這些工具輕鬆地媲美甚至經常超越它們的伺服器端對應工具,而且它們是我們日常依賴的絕佳優勢。

這就是為什麼當像 React 這樣的框架(再次強調,它們表面上以對開發者友好為榮)卻將所有這些都拋諸腦後,並糟糕地建置自己的開發工具時,會讓人感到極度沮喪。

當然,並非所有框架都是如此;例如,Preact 採取了正確的方法,透過增強現有的瀏覽器開發工具來提供框架特定的資訊。因此,如果您在啟用其開發工具的情況下對您的 Preact Web 應用程式運行瀏覽器分析,您將在原生瀏覽器分析中看到一些有用的標記,指示元件何時渲染等。這讓您可以理解框架層導致的瀏覽器層發生的情況,因此您可以更快地鎖定看似導致大量垃圾回收或在不方便的時間觸發佈局刷新的元件。

但 React 並非如此:它有自己的工具,完全獨立。您可以運行 React 分析,但它不會創建一個配套的瀏覽器分析,您也無法將 React 資訊與瀏覽器中的 JavaScript 抽樣資訊結合起來以獲得時間花費的完整圖景。這意味著,即使您設法弄清楚問題出在哪裡,例如,某個特定元件對 DOM 的提交,您也無法理解原因。您可能需要重複兩次重現問題(每次使用一個分析器),仔細比較兩個單獨的分析,並進行一些邏輯推斷。

此外,React 開發工具分析器(從 UI 角度來看)可能難以使用,並且缺少關鍵資訊,例如時間花費的匯總視圖。

這不僅僅是 React 開發工具的問題。您需要的除錯資訊通常不可用,即使可用,您也經常被迫採取額外的步驟:

現在,別誤會我的意思:Web 平台本身可能是一個充滿挑戰的效能環境,有許多怪癖需要注意。但框架很少(如果有的話)明確解決這些問題,事實上,它們使效能除錯比基準更困難,因為它們增加了額外的抽象層。

框架的程式設計模型與底層平台的差異越大,在兩者之間的介面中出現複雜效能問題的機會就越大。所以,如果它們至少能幫助解決這個問題,那就太好了!

那麼,既然我們知道了問題以及它們出現的容易程度,我們能做些什麼呢?如果您被困在建置以 JavaScript 為主的客戶端應用程式,請不要絕望!您仍然可以採取一些措施來提高應用程式變快的機會,並防止它隨著時間變得太慢。

不過請注意:根據我的經驗,即使您遵循了所有這些建議,也不應期望應用程式能始終保持高性能。但遵循這些建議有助於止血,並防止情況在您注意到之前變得太糟。

如果您覺得這需要大量的開銷,我們絕對有共識。這就像為了原地不動而需要大量奔跑,而且這個地方可能也不是一個值得久留的好地方。

實施所有這些緩解措施將花費大量時間,並且持續的警惕將迫使您擁有一個專門的團隊積極應對這些影響,或者以某種方式教育所有開發人員了解所有陷阱,並說服他們自己保持這種程度的警惕,持續不斷。

如果您的組織非常龐大或資金充足,這種程度的開銷可能是可行的,但對於其他人來說則不太可行。

考慮到所有這些成本和效能不佳的前景,我們可能應該看看我們從這些以 JavaScript 為主的方法中獲得了哪些好處。我們已經談論了承諾,但現實如何?

我將使本節更具體一些,並專門討論 React 及其生態系統。它們佔據主導地位,存在已久,並且是我擁有最多 SPA 經驗的,所以它們是我最好的例子。

我確實理解使用 React 建置的吸引力,真的,我理解。它是一個有趣的程式設計模型,它所大力推動的元件範式具有固有的組織效益。您也可以在外面找到很多可重用的東西,也許這讓您有信心在初始實施中花費更少的時間,讓您有更多時間進行改進和潤飾。

而且這很公平,您可能找不到比這更大的元件、函式庫和互補框架的生態系統了!但除了普遍較差的效能外,它們還有另一個相當大的問題:它們似乎持續不了多久。根據我的經驗,React 生態系統,甚至整個 JavaScript 生態系統,並沒有多少穩定性。也許最近有所改變,但從歷史上看,我曾見過多個大型應用程式在一段時間後重寫了大部分程式碼,不是因為產品原因,而是因為依賴關係:

當然,我也是一名開發者,我理解需要權衡。當我們建置一個專案時,需要仔細權衡許多不同的考量,以便它最終能夠啟動,並處於一個還算不錯的狀態。因此,接受所有 JavaScript 生態系統的不穩定性以及之後的一些程式碼變動可能是合理的,並將它們視為快速啟動的合理代價。這很公平。

然而,這仍然留下了效能問題。

那麼效能呢?如果我們處理程式碼變動、新功能和錯誤修復,那麼我們可能沒有太多時間關注它,因為正如我們所見,它在以 JavaScript 為主的應用程式中需要大量的開銷。

所以也許我們決定效能對我們來說並不那麼重要,我們最好放棄一些上述的警惕和監控。如果我們能做到,那就太好了!如果我們做不到,應用程式仍然可以工作,這才是最重要的。這可以是一個完全合理的決定;工作量少是一件好事,不必擔心效能也是!

但最終,將會有人做出決定:我們的受眾。他們將選擇是否使用我們的網站,以及是否為其付費。這個選擇將涉及他們能多快完成事情,以及我們的網站使用起來感覺有多「好」,這在很大程度上受到他們使用速度的影響。

很有可能,一個未經監控的以 JavaScript 為主的 Web 應用程式對我們所有的使用者來說都不會運作良好。當然,對某些人來說它會運作得很好,那些擁有快速設備和與典型開發者相似連接的使用者,但我們的受眾可能更廣泛,擁有各種設備和連接。因此:

放棄為廣泛使用者提供絕佳體驗是否值得?我相信意見會有所不同,但我的答案將是堅決的「否」。我們可以做得更好。

因此,正如我們所見,當我們主要由 JavaScript 建置 Web 應用程式時,會出現一系列問題。其中大部分源於 JavaScript 在使用者設備上執行起來非常昂貴,以及很容易意外地達到所需工作量超過設備在可接受時間內完成的能力。

並非所有位元組都生而平等,而 JavaScript 是瀏覽器處理中最昂貴的資源之一。解析和編譯它都很昂貴,因為它語法複雜且類型靈活。當它最終執行時,它主要在單一執行緒上運行,因此只能利用您設備 CPU 或 SoC 的單一核心——如果碰巧是異質核心架構中的一個小核心,它可能會非常慢。

顯然的替代方案是將所有或大部分工作轉移到伺服器,這樣即使它仍然是 JavaScript(例如 nodejs),它也可以利用更強大的 CPU、顯著更高的功率預算(無需擔心電池壽命!)、更長的位元組碼快取,以及更好的多核心利用率。

以伺服器為中心架構向瀏覽器提供現成的頁面內容,而不是提供配方和一些讓它們自行建置內容的食材,讓它們自行承擔成本。

然而,並非所有伺服器端工作都同樣有效。雖然其中一些以 JavaScript 為主架構確實涉及伺服器端渲染方面,但它通常只是權宜之計,而且效果不佳,這抵消了我所談論的許多好處。

最好的例子可能是單體水合作用(monolithic hydration),您在伺服器上渲染整個頁面,並將初始標記和應用程式程式碼都發送給使用者。這樣做的想法是,瀏覽器可以使用標記快速顯示頁面,而無需等待 JavaScript,JavaScript 僅在之後「水合」頁面以實現互動性。

雖然這種方法肯定有助於初始繪製時間,但您仍然可能向不知情的使用者瀏覽器發送數 MB 的 JavaScript。該瀏覽器仍然需要在啟動時獲取、解析、編譯和運行所有這些程式碼,這最終可能導致應用程式可見和實際可用之間出現顯著延遲。因此,即使您解決了繪製問題,仍然存在互動性問題——而且您可能無意中使這個問題變得更糟,如果您現在發送更多 JavaScript 來水合初始狀態!

部分水合作用策略可以幫助減輕這些成本,但您仍然需要注意一次要運行多少 JavaScript 並密切關注它,這意味著又多了一件需要擔心和追蹤的事情。

在我看來,真正的替代方案是切換到以伺服器為中心的程式設計模型,其中大部分程式碼預計永遠不會離開伺服器。如果您願意,它仍然可以是 JavaScript!只是您永遠不需要將其發送給某人的設備。

伺服器是一個更可預測且可擴展的環境,更重要的是,您對它有一定的控制權。效能日誌記錄和除錯可能更容易,因為大部分程式碼運行在您以某種方式管理的機器上,而不是運行在完全超出您控制的各種異質計算設備上。而且大多數時候您根本不需要擔心程式碼大小!

在基於 HTML 的方法中,如果您的處理能力和內容分發策略足以滿足您的需求,那麼使用者使用的是高端桌面電腦還是廉價手機,就變得有些無關緊要了。您的 Web 應用程式對使用者的設備和連接的要求會低得多,為每個人提供絕佳的體驗。

最明顯的一個是簡單的全頁導航,這是 Web 的基礎架構。當與穩健、全球分佈的託管基礎設施配對以減少網路延遲時,它在實現效能目標方面具有經過驗證的良好記錄。遵循這種方法並不意味著您不能使用 JavaScript;但您應該傾向於將其保留為一些互動式功能的點綴,而不是整個東西的引擎。您可以使用獨立的 JavaScript 腳本,或其他方法,如漸進增強的 Web 組件,並且您可以根據需要使用或不使用建置過程來轉換原始碼和生產程式碼。

除此之外,多年來我們還出現了各種 JavaScript 輕量級架構,它們仍然表現出色,並且一直在不斷改進。例如,透過新的伺服器渲染的 HTML 負載替換頁面部分的方法(例如由 turbolinks 推廣)仍然非常有效,並且現在甚至透過 View Transitions API 獲得了一定程度的原生支援。像 htmx 或 WordPress 的 Interactivity API 這樣的 HTML 增強方法是另一種 JavaScript 輕量級方法,並且也可以作為許多不同類型專案的基礎。

當然,我們需要現實一點:並非所有 Web 應用程式都可以這樣建置,其中一些應用程式確實主要作為客戶端應用程式才有意義。高度互動的應用程式,如編輯器,就是其中一個例子,伺服器往返盡可能少,以便使用者可以專注於他們正在編寫的內容,而不必擔心每次點擊某個東西時的延遲。他們甚至可能願意容忍比平常更長的頁面載入時間,如果他們認為這是一個足夠的「目的地」,並且他們即將進行的任務(例如撰寫文章)將花費足夠長的時間來證明等待是合理的。

但即使在完全客戶端應用程式的背景下,您也可以採取許多不同的方法,而在 2026 年,React 坦白說是最不具吸引力的一種。它龐大、緩慢、充滿效能陷阱,而且也不再新穎刺激。它確實有一個您可以依賴的龐大生態系統,這是一個非常重要的點——儘管這表明如果這是您決定性因素,您可能將開發速度置於使用者需求之上。

如果您確實需要進行客戶端渲染,請嘗試其他框架!框架運行時通常更小,它們使用更細粒度的響應性(這有助於模組化和 UI 響應能力),其中一些使用根本不同的方法來擺脫不斷 diff 大型樹的效能雷區。其中一些已經存在一段時間了,所以您不一定依賴未經證實的依賴。

總結來說:根據我的經驗,以 JavaScript 為主的 Web 應用程式通常在效能方面起點不佳,而且它們往往會隨著時間而惡化。緩解這種情況需要大量的開銷,這些開銷設置和維護成本高昂,而且通常仍然會導致效能下降——儘管速度較慢。

所謂的好處對我來說也不像他們聲稱的那樣明顯。框架真的讓我們更敏捷,並帶來更好的整體使用者體驗,還是它們只是用一套問題換取另一套問題?它們真的能從長遠來看節省我們的開發時間和精力,還是持續專案中依賴驅動的程式碼變動會抵消其中大部分?我無法明確回答這些問題,但我確實覺得我們所付出的成本,特別是長期成本,存在一些盲點。

儘管如此,行業的看法似乎是以 JavaScript 為主的方法在效能方面「還可以」,而且好處是值得的。我發現這過於樂觀,因為在我工作中接觸過的以 JavaScript 為主的應用程式中,沒有一個能夠長期穩定地達到良好的效能。選擇偏差?也許吧,但更廣泛的格局並沒有提供多少與我的軼事相反的觀點。

我確實相信,在一個健康、長期運行的以 JavaScript 為主的專案中維持良好效能所需的努力如此之高,以至於在大多數情況下是不可持續的。總有一天,您可能會發現您的優化工作被不相關的變更所抵消,儘管您設置了監控。即使引入問題的開發者意識到了,當截止日期與效能目標發生衝突時,截止日期通常會獲勝。

這就是為什麼我強烈主張使用更穩健的系統,從一開始就更難破壞效能。

現在,別誤會我的意思:在伺服器端建置東西並非萬靈丹,也不是最適合所有專案。在伺服器上建置緩慢的東西是可能的,而且效能下降也可能在那裡發生。所有系統都有其效能挑戰,伺服器也不例外。

但根據我的觀察,在伺服器上維持良好的效能往往更容易,因為您將昂貴的工作放在有意義的地方:您擁有、管理或租用的強大專用伺服器;而不是您受眾使用的緩慢、功率不足的設備。追蹤和記錄伺服器上發生的事情也通常容易得多,這將極大地有助於查找和修復效能錯誤,甚至應用程式邏輯錯誤。而且,如果您的應用程式程式碼永遠不會離開伺服器,那麼需要透過網路傳輸的內容也會少得多。

是時候停止欺騙自己,認為以 JavaScript 為主的客戶端方法是當今開發 Web 應用程式的方式了。我們真的應該停止習慣性地為我們建置的所有東西 npm install 某些框架樣板,並停止基於我們最熟悉的堆疊來做架構決策。

許多用例根本更適合以伺服器端、以 HTML 為中心的方法,因為效能權衡更符合預期用途——這確實應該是決策過程的一部分。與其習慣性地 npm install 某些框架樣板,我們應該花點時間考慮一下,我們是否不應該轉而在伺服器上建置一個多頁應用程式。

客戶端渲染真的有幫助嗎,還是使用者最終還是會卡在等待 API 調用?

使用者是否會透過稍後節省大量應用程式內導航或操作時間來彌補初始載入等待時間,還是他們完成預期的幾件事後就會離開?

他們是否會從中獲得任何好處,因為我們借用了他們的設備來處理我們可以在伺服器上處理的工作?

作為一個產業,我們真的欠使用者一個更好的體驗,因為現在我們經常按照我們想要的方式建置,而不是按照他們需要的方式建置。

我是 Sérgio,我從事 Web 前端程式碼工作。有時我會在這裡寫作。