動態

詳情 返回 返回

Zess:⚡ 一個性能超越 Vue 且直逼 Solid 的輕量編譯型 JS 框架 - 動態 詳情

引言

作為一名前端開發者,我一直有個未完成的計劃,那就是獨立構建一個符合個人技術理念的 JavaScript 框架。最初的想法是圍繞虛擬 DOM 進行設計,但進入2025年,前端技術日新月異,各類框架層出不窮且競爭激烈,虛擬 DOM 已不再是當下的主流方案。因此我調整了計劃,並決定立即開啓項目。

2025年上半年,這個項目進入實際開發階段,並於今年10月在 npm 上發佈。

本文不會深入技術細節,而是側重於經驗分享,以易於理解的方式介紹本框架的優勢特點與技術選型考慮。希望這些內容能為同樣有興趣探索框架開發的同行,提供一些可借鑑的思路。

背景

自從 React 憑藉虛擬 DOM 與函數式組件獲得廣泛採納之後,前端開發便進入了以虛擬 DOM 為主導的時期,各類基於相似理念的框架不斷涌現,連 Vue 等主流框架也全面接納了該技術。業內長期存在一個共識:DOM 操作成本高昂,需要謹慎對待。由於單個 DOM 節點包含數百個屬性,虛擬 DOM 通過將其抽象為輕量級 JavaScript 對象,僅保留節點類型、屬性和子節點等關鍵信息,從而有效規避直接操作 DOM 的負擔。再結合高效的 diff 算法,比較新舊虛擬 DOM 的差異,實現最小化 DOM 更新,最終達到優異的運行時性能。

然而到了2019年,情況開始發生變化。新興框架 Svelte 另闢蹊徑,通過在編譯階段將組件模板轉換為簡練的 JavaScript 代碼,大幅減少運行時開銷。在 js-framework-benchmark 等框架性能測試中,Svelte 表現突出,甚至超越了多數主流框架。它揭示了一個新思路:將複雜的優化工作提前到編譯時處理,而非留到運行時解決。這證明了即使不依賴虛擬 DOM,通過編譯時優化同樣能實現卓越性能。

順便一提,Svelte 的作者是知名前端開發者 Rich Harris,其代表作還包括 ReactiveRollupDegit 等受歡迎的工具庫。

如果説 Svelte 是編譯型 JavaScript 框架的開創者,那麼隨後出現的 Solid,則在編譯器設計與響應式機制上實現了更深層次的結合。它不僅繼承了 Svelte 的編譯時優化理念,更進一步引入了基於 Signal 的響應式系統。與 Svelte 所使用的髒檢查機制相比,Signal 能夠在數據變化時實現更精準高效的追蹤更新。

在這兩種技術的結合下,Solid 將 DOM 更新的粒度優化到了接近“原子級”的水平,僅更新真正發生變化的部分。憑藉這種極致的優化,Solid 在 js-framework-benchmark 性能測試中取得了1.11分的驚人成績,無限接近原生 JavaScript 實現的1.00分,至今仍在該榜單中保持領先地位。

solid-benchmark

當然,Solid 的優勢不僅在於其出色的性能,還在於它保留了與 React 相似的 JSX 語法和 API 設計。相比基於模板的 Svelte,這種高度相似的語法與顯著降低的上手難度,使其能夠更快被社區接受。

正是這些特點,使 Solid 在發佈後便迅速吸引我的關注。作為一名曾經對虛擬 DOM 深信不疑的開發者,我逐漸意識到,編譯時優化才是實現高性能前端框架的根本出路。而我的框架——Zess,也正是在此背景下誕生的。

介紹

zess-logo

Zess(發音為 /zɛs/)是一個基於編譯器的 JavaScript 框架,專注於構建基於標準 HTML、CSS 和 JavaScript 之上的用户界面。與傳統的專注於運行時的框架不同,Zess 將其主要工作轉移到編譯階段。通過靜態分析和編譯時優化,它將聲明式組件轉換為精簡高效的命令式代碼。這帶來了更少的運行時開銷、更快的首屏加載速度,以及接近原生水平的用户體驗性能。

作為一個編譯器驅動的 JavaScript 框架,Zess 在設計之初就以 Solid 為參考對象,目標是在保持與其接近的性能水平、基本相同的 JSX 語法與 API 設計的同時,實現更小的體積和更低的開發門檻,類似於 Preact 與 React 之間的定位關係。從架構上看,這類編譯型框架主要由編譯器響應式系統兩大核心構成,下文將分兩個章節對它們進行具體介紹。

1. 編譯器

構建 JSX 編譯器的流程可分為三個核心階段:解析轉換代碼生成。即先將 JSX 解析為 AST(抽象語法樹),再按需修改 AST 節點,最終轉換為相應的 JavaScript 代碼。

解析與生成目前社區已有成熟方案,無需重複實現。常見解析器如 babelesprimaespree 和 acorn 等均支持 JSX。受早期技術習慣影響,我在項目初期也選擇了 babel——它工具鏈完整,生態豐富,且被 Solid 等框架的 JSX 插件所採用。

但在實際開發中,babel 顯得“過重”,其 AST 與 ESTree 標準存在細微差異,所有操作都須依賴其特定 API,這在需要高度定製化的場景中限制了靈活性。編譯型框架的編譯器需應對複雜邏輯,遠非簡單表達式轉換所能涵蓋。

隨着前端工具鏈的“鏽化(Rustify)”,像 swc 這樣的高性能解析器逐漸興起,官方稱其性能遠超 babel。然而在我啓動項目時,Rust 方案尚不成熟:swc 的 AST 不符合 ESTree 標準,集成成本較高;oxc-parser(這是 Vite 的下一代構建工具 Rolldown 的底層依賴)仍處於早期階段,尚未達到生產狀態。

最終我選擇了 meriyah + astring 這一組合。meriyah 是當前最快的 JavaScript 解析器,支持 JSX(TypeScript 可藉助 Vite 的 esbuild 處理);astring 則是輕量且高效的代碼生成器。根據 astring 官方的性能測試顯示,meriyah 與 astring 組合在解析和生成代碼的速度上遠超 acornsucrase 等競爭對手。更重要的是,兩者均嚴格遵循 ESTree 標準,在兼容性、靈活性與性能之間取得了良好平衡,非常契合本項目的需求。

meriyah-astring-benchmark

為落實上述設計目標,Zess 的 JSX 編譯器(@zessjs/compiler)致力於實現與 Solid 一致的代碼生成效果,目前已支持包括但不限於以下幾種優化:

  • 動態表達式追蹤:識別 JSX 中的動態表達式(如無參數調用的 Signal 函數),並將其轉換為 effect 副作用函數,實現響應式更新。
  • 屬性批量更新:將同一個 DOM 節點的多個動態屬性合併至單個 effect 函數中監聽,減少副作用函數數量,提升更新效率。
  • Fragment 表達式處理:將 Fragment 節點作為數組處理,其中帶有動態值的子元素會轉換為 memo 計算值以進行響應式追蹤,並對嵌套於 DOM 結構中的 Fragment 執行展開操作。
  • JSX 特殊屬性轉換:針對 ref 等特殊屬性,根據其值的類型生成對應邏輯;對 on* 類事件屬性,則根據事件名稱的大小寫形式進行差異化編譯處理。

礙於篇幅所限,上文僅介紹了部分核心優化。在整體設計思路上,Zess 的編譯器與 Solid 高度一致,但在靜態節點的編譯優化上存在顯著區別。請看以下示例:

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count => count + 1);

  return (
    <button type="button" onClick={increment}>
      <p>{count()}</p>
    </button>
  );
}

render(() => <Counter />, document.getElementById("app"));

以上代碼經過 Solid 編譯後如下所示:

import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
var _tmpl$ = /*#__PURE__*/_$template(`<button type=button><p>`);
import { render } from "solid-js/web";
import { createSignal } from "solid-js";
function Counter() {
  const [count, setCount] = createSignal(1);
  const increment = () => setCount(count => count + 1);
  return (() => {
    var _el$ = _tmpl$(),
      _el$2 = _el$.firstChild;
    _el$.$$click = increment;
    _$insert(_el$2, count);
    return _el$;
  })();
}
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
_$delegateEvents(["click"]);

Zess 的編譯結果是這樣的:

import {createComponent as _$createComponent, createElement as _$createElement, delegateEvents as _$delegateEvents, insert as _$insert, setAttribute as _$setAttribute} from "@zessjs/core";
import {render, useSignal} from "@zessjs/core";
function Counter() {
  const [count, setCount] = useSignal(1);
  const increment = () => setCount(count => count + 1);
  return (() => {
    const _el$ = _$createElement("button");
    _el$.$$click = increment;
    _$setAttribute(_el$, "type", "button");
    const _el$2 = _$createElement("p");
    _$insert(_el$2, count);
    _el$.append(_el$2);
    return _el$;
  })();
}
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
_$delegateEvents(["click"]);

通過對比 Solid 與 Zess 的編譯產物可以看出,兩者在靜態內容處理上採取了不同的技術路線:

Solid 將 JSX 中的靜態內容提取為模板工廠函數,在需要獲取動態節點時克隆模板結果,並通過節點樹關係定位動態內容。該方案也被最新的 Svelte 5 和 Vue Vapor Mode 所採納。

而 Zess 則採用 createElement 函數逐個創建 DOM 結構,僅對動態節點添加監聽,靜態節點則直接批量插入父節點,這種思路與舊版 Svelte 類似。相較於模板方案,Zess 的優勢在於:

  • 所有節點變量可直接訪問,無需通過節點樹查找動態內容
  • 省去模板克隆帶來的性能開銷
  • 在多數場景下性能表現持平甚至更優,實現負擔更輕

該方案的主要缺點是節點數量過多時可能生成較多重複代碼。不過在實際開發中,多節點通常通過 <For> 組件或 map 函數批量生成,因此該影響較為有限。總體而言,這是一種值得采用的取捨策略。

2. 響應式系統

與編譯器部分相比,響應式系統的實現原理並沒有太多需要展開詳述的內容。這並非意味着實現一個響應式系統很簡單,而是因為 Signal 這一概念在當今前端領域已不新鮮。

早在至少十年前,開發者 Adam Haile 就創建了一個名為 S.js 的響應式庫,其中已經運用了依賴圖管理等算法來實現響應式機制,其 API 設計與今天多數基於 Signal 的框架非常接近,可謂極具前瞻性。

S.js 不僅 API 設計優秀,其最初目標就是作為操作 DOM 的響應式工具庫,能夠與各類框架無縫配合。基於這一理念,Adam 還開發了 Surplus 框架,它構建於 S.js 之上,其設計思路與今天的 Solid 等現代框架已十分接近,讓人不得不欽佩作者的開創性思維。

儘管 S.js 本身並未廣泛流行,但它對 Solid 框架的作者 Ryan Carniato 產生了深遠影響。從 Solid 的早期代碼中,可以清晰地看到對 S.js 的借鑑與致敬,最初版本甚至直接引入 S.js 作為響應式核心。隨着 Solid 的逐漸成熟與社區影響力的擴大,經過 Ryan 與社區的持續迭代和優化,Solid 如今已發展出一套獨具特色、自成體系的 Signal 響應式機制。Solid 的響應式系統獨特之處主要在於以下兩點:

  • 推送與拉取混合的依賴響應模型(Push-pull hybrid):不同於傳統的以 Preact Signals 為代表的拉取為主的惰性求值模型以及以 MobX 為代表的主動推送更新並即時計算值的方式,Solid 將上述兩種模式相結合,當有信號被修改時,會向下遊推送更新通知,標記可能需要更新的計算值為髒狀態,而計算值被讀取時會向上遊拉取其依賴的信號是否發生變更等信息,檢查是否需要重新計算。通過這種方式既實現了更精準的響應式更新,又避免了過度計算帶來的性能浪費。
  • 基於數組與索引的依賴管理系統:不同於主流使用 Set 或雙向鏈表等結構的依賴管理方案,Solid 採用了數組配合索引數組的聯動機制。該方案通過維護雙向索引映射關係,在依賴的添加與移除等操作上實現了與 Set 相同的 O(1) 時間複雜度,同時憑藉數組順序存儲的特性帶來了更快的遍歷速度,且避免了 Set 的額外創建開銷。這使得它在依賴數量較少的日常使用場景中,能夠實現更出色的性能,讓響應式更新的整體效率更高。

js-reactivity-benchmark

和 JavaScript 框架類似,Signal 響應式框架也有專門的性能基準測試 js-reactivity-benchmark。從官方測試結果來看,alien-signals 表現最為突出,其優異的性能已被 Vue 採納成為新響應式系統的底層實現;而 S.js 儘管已停止維護多年,性能依然位居前列,可見其算法設計之精妙;Preact Signals 排名略高於 Solid,部分得益於其惰性求值特性在測試中的優勢,加之其實現較為簡單,缺少 effect 嵌套等功能(由 uSignal 作者 Andrea Giammarchi 所指出),使其在測試中具有一定優勢,因此不能將該榜單看作性能的唯一指標。Solid 雖不頂尖但也仍算優秀,更重要的是其響應式系統與框架設計理念高度契合,在功能與性能之間得到理想平衡。

Zess 作為對標 Solid 的輕量編譯型框架,在響應式系統的實現上同樣遵循了簡潔高效的設計理念。在保留 Solid 核心機制與常用 API 的基礎上,對原有實現進行大幅精簡,移除了當前場景下非必要的功能,做到了真正的短小強悍。根據 Bundlephobia 的數據,其核心包 @zessjs/core 經 Gzip 壓縮後僅 5.7kB,可以説是非常輕量了。

bundlephobia

性能表現

經過編譯器與響應式系統的深度優化,Zess 在最新的 js-framework-benchmark 性能測試中取得了令人矚目的 1.20 分(分數越低性能越優),這一成績不僅超越了 VueReactAngular 等主流框架,更是大幅領先於絕大多數虛擬 DOM 實現。所有測試項目均呈現綠色評級,證明其性能表現全面均衡無短板。

在 select row 和 swap rows 這兩項關鍵測試中,Zess 的表現尤為亮眼。這兩項測試表面上是簡單的行操作,實則需要處理成千上萬行數據的精準定位與交換,虛擬 DOM 的 diff 比較機制在此時便成為負擔。而編譯型框架能完美規避這種場景,展現出了壓倒性的性能優勢。

zess-benchmark

當然,Zess 的表現也並非完美無缺。從測試結果可以看出,其成績仍略低於 Solid 和 Svelte 等以性能著稱的框架,這説明在 DOM 操作處理和響應式系統算法方面仍有提升空間,後續將重點針對這些環節進行深度優化。

總結

Zess 是我完全參照現代前端框架標準開發的開源項目,採用 pnpm + monorepo 架構進行項目管理,通過 GitHub Actions 實現了從測試、發佈到部署的全流程自動化。文檔使用 Rspress 構建,並集成了 Algolia DocSearch 以提供全文搜索能力。

作為我的首個開源項目,Zess 將持續迭代完善,重點優化 Signal 算法、實現 SSR 渲染與水合等功能。同時我也計劃推出更多有價值的開源工具。如果您對這個項目感興趣,歡迎關注我的動態,也請為 Zess 點個 Star,您的支持將是我持續創作的最大動力!🙏

GitHub:Zess ⚡ The compiler-driven JavaScript framework for building user interfaces.

文檔:Zess - The compiler-driven JavaScript framework

Add a new 評論

Some HTML is okay.