Svelte——不止於快

Svelte小結

最近組裏準備對我們框架中基於 VDOM 來更新 UI 的這一套架構有所不滿,所以此番我調研了一番近期比較火的Svelte,想看看如果不用 VDOM ,能否搞出一番新天地。

背景 && 痛點

首先講下 Svelte 框架出現的背景,現在的前端框架日益膨脹,有的時候僅僅爲了顯示一個簡單的頁面,都要加載幾百KB的資料,以github上很火的 RealWorld 項目爲例,可以看到用 Svelte 來開發並生成的 bundleSize 非常小,而其他框架最後得到的bundleSize則很大,這是因爲很多前端框架都會有 runtime 這一個概念

runtime指的是框架本身的代碼也會被打包進用戶的代碼裏,生成一個bundle.js供用戶下載,用戶的各種操作都會執行runtime中的一些輔助函數(以React爲例,這些輔助函數包括了state的計算、state的合併、diff算法的執行……)

image.png

Svelte 是不依賴於 VDOM 的,這與當下流行的一些前端框架(React、Vue)有很大的不同,那麼爲什麼 Svelte 要摒棄 VDOM,朝另一個方向上走呢?下面就要來講講 VDOM 的優缺點

Virtual DOM的缺點

VDOM 的優點不多說,網上的分析文章一大堆,總結起來就3點

  1. 組件的高度抽象化
  2. 可以更好的實現 SSR,同構渲染等
  3. 基於VDOM的框架可以跨平臺

下面來說說 VDOM 的缺點:
首先,Virtual DOM高效是一個誤解,React 從來沒有說過它的 VDOM 性能很好,有的人會說Virtual DOM高效的一個理由就是它不會直接操作原生的DOM節點,因爲這個很消耗性能。
當組件狀態變化時它會通過某些diff算法去計算出本次數據更新真實的視圖變化,然後只改變“需要改變”的DOM節點。用過React的人可能都會體會到React並沒有想象中那麼高效,框架有時候會做很多無用功,這體現在很多組件會被“無緣無故”進行重渲染(re-render),特別是在用了 Redux 後,這個現象會愈發明顯。

所謂的re-render是你定義的class Component的render方法被重新執行,或者你的組件函數被重新執行,並不是說原生DOM被重新渲染。組件被重渲染是因爲Vitual DOM的高效是建立在diff算法上的,而如果要有diff,則一定要將組件重渲染才能知道組件的新狀態和舊狀態有沒有發生改變,從而才能計算出哪些DOM需要被更新。

這裏提一下可能有的朋友會說React Fiber不是出來了嗎,這個應該不是問題了吧?其實Fiber這個架構解決的問題是不讓組件的重渲染和reconcile的過程阻塞主線程的執行,組件重渲染的問題依然存在,而且據反饋,React Hooks出來後組件的重渲染更加頻繁了。正是因爲框架本身很難避免無用的渲染,React才允許你使用一些諸如shouldComponentUpdate,PureComponent和useMemo的API去告訴框架哪些組件不需要被重渲染,可是這會引入了很多模板代碼(boilerplate)。

這裏引申講下VDOM的開銷在哪裏, 這和它必不可少的3個步驟有關(以更新某個元素的text值爲例):

  1. 調用render函數生成一顆新的VDOM
  2. 遍歷元素上的新屬性和舊屬性,查看是否需要添加/刪除/更新屬性
  3. 訪問到此元素,然後發現text值需要被更新,則更新

具體代碼實例如下:

function MoreRealisticComponent(props) {
  const [selected, setSelected] = useState(null);

  return (
    <div>
      <p>Selected {selected ? selected.name : 'nothing'}</p>

      <ul>
        {props.items.map(item =>
          <li>
            <button onClick={() => setSelected(item)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

這裏 props.items 這個數據,只要state發生了變化,我們都會重新生成一堆虛擬的 li ,這是毫無意義的,這些瑣屑的操作累加起來,最終對性能會造成影響,如果你想要更快的話,一個比較好的做法就是去除這些多餘的操作。

核心思想

一句話總結 Svelte 的核心思想:
通過靜態編譯減少框架運行時的代碼量
舉例來說,當前的框架無論是 React、Angular 還是 Vue,不管你怎麼編譯,使用的時候必然需要“引入”框架本身,也就是所謂的運行時 (runtime)。但是用 Svelte 就不一樣,一個 Svelte 組件編譯了以後,所有需要的運行時代碼都包含在裏面了,除了引入這個組件本身,你不需要再額外引入一個所謂的框架運行時.
當然,這不是說 Svelte 沒有運行時,但是出於下面2個原因,這個代價可以變得很小:

  1. Svelte 的編譯風格是將模板編譯爲命令式 (imperative) 的原生 DOM 操作。
// 模板代碼(編譯前)
<a>{{ msg }}</a>

// 運行代碼(編譯後)
function renderMainFragment ( root, component, target ) {
	var a = document.createElement( 'a' );
  
	var text = document.createTextNode( root.msg );
	a.appendChild( text );
	
	target.appendChild( a )

	return {
		update: function ( changed, root ) {
			text.data = root.msg;
		},
		teardown: function ( detach ) {
			if ( detach ) a.parentNode.removeChild( a );
		}
	};
}
  1. 對於特定功能,Svelte 依然有對應的運行時代碼,比如組件邏輯,if/else 切換邏輯,循環邏輯等等…但它在編譯時,如果一個功能沒用到,相應的代碼就不會被編譯到結果裏去,這個有點類似於 webpack 的 Tree Shaking。

優缺點

說完了核心思想,接下來講講 Svelte 的優缺點吧。

優點

這裏我認爲最重要的優點是這3個:

  1. 完成同樣功能所需的代碼量相對較少
    Svelte支持靜態編譯,無需引入框架自身,所有需要的運行時代碼都包含在裏面了,除了引入這個組件本身,你感覺不到框架存在,同時 Svelte 提供了一些簡單好用的模板,使得維護和編寫代碼都變得比較簡單,相對於react,開發同樣功能的代碼大概要少30%(以 RealWorld 爲準)
  2. 在一些不那麼複雜的場景下擁有更高的性能
    Svelte 編譯之後的源碼幾乎與手寫原生代碼相同,對於大多數純展示性頁面 + 少量交互的場景,其實VDOM能發揮作用的地方不多,這個時候 Svelte 能很好地發揮出優勢來。
  3. 製作獨立分發的小組件時有得天獨厚的優勢
    因爲生成的組件沒有runtime,沒有額外的依賴,所以所有組件都可以單獨使用,可以無縫地在React、Vue或Angular等其他框架裏直接import
// SvelteComponent.js 是已經編譯後的組件
import SvelteComponent from './SvelteComponent';

const app = new SvelteComponent({
    target: document.body
});

缺點

  1. 在一些case下存在很多重複代碼
    雖然在簡單的 demo 裏面代碼量確實非常小,但同樣的組件模板,這樣的 imperative 操作生成的代碼量會比 vdom 渲染函數要大,多個組件中會有很多重複的代碼。項目裏的組件越多,代碼量的差異就會逐漸縮小。同時,並不是真正的如宣傳的那樣 “沒有 runtime“,而是根據你的代碼按需 import 而已。使用的功能越多,Svelte 要包含的“運行時”代碼也越多。
  2. 在大型應用中的性能還有待觀察
    在大量動態內容和嵌套組件的情況下,Svelte 的更新策略決定了它也需要類似 React 的 shouldComponentUpdate 的機制來防止過度更新。另一方面,其性能優勢比起現在的主流框架並不是質的區別,現在大部分主流框架的性能都可以做到 vanilla js 的 1.2~1.5 倍慢,基於 Virtual DOM 的 Inferno 更是接近原生,證明了 Virtual DOM 這個方向理論上的可能性,所以可以預見以後 web 的性能瓶頸更多是 DOM 本身而不是框架。
  3. 抽象能力偏弱
    Svelte 的編譯策略決定了它跟 Virtual DOM 絕緣(渲染函數由於其動態性,無法像模板那樣可以被可靠地靜態分析),也就享受不到 Virtual DOM 帶來的諸多好處,比如基於 render function 的組件的強大抽象能力,基於 vdom 做測試…

源碼 && 產物剖析

說完了 Svelte 的特點,讓我們進一步去分析下 Svelte 具體做了黑魔法,讓他能夠在不使用 VDOM 的情況下做到兼顧效率和產量去更新數據,下面以一個最常見的Hello World爲例:

image.png
這裏的c代表了create,m代表了mount,d代表了detach,即每個組件自身擁有一個完整的生命週期,包含了元素的創建、插入、更新和銷燬,從而構建出自己的一整套體系。下面是我整理的Svelte編譯後運行時的整體框架圖,可以看到整個runtime的架構還是比較清晰的,事件體系通過state管理,同時頁面有自己的狀態管理和生命週期,且抽取出了常用的工具函數形成工具箱,按需引用,有效減少bundleSize。

運行時架構圖

image.png

單次數據更新

說完了runtime架構,下面講講,Svelte是如何在不需要VDOM的情況下對數據做到高效更新的,這裏以單次數據的更新過程爲例:

  1. Svelte 會在代碼編譯的時候將每一個狀態的改變轉換爲對應DOM節點的操作,從而在組件狀態變化的時候快速高效地對DOM節點進行更新
  2. handleClick 裏對 data 的操作,會被一個 $$invalidate 函數給包裹起來,便於日後判斷
  3. 調用 safe_not_equal 函數來比對新老數據,若需要更新,則標記組件爲一個髒組件,並觸發組件的更新流程,且這裏的更新是在瀏覽器的 microtask 裏進行的,最大化地利用了顯示器更新的原理

這個函數裏的源碼詳見圖片:
image.png

特殊的數據更新策略

image.png
對於一些比較特殊的數據,例如數組裏的數據,那麼不可避免地會碰到需要更新大量數組的問題,其實 Svelte 對於這類情況的處理方法與 React 和 Vue 類似,都是通過添加 key 來做優化,然後整個的更新策略我覺得是比 React 要好的,在更新時會記錄下移動的距離,而不是像 React 一樣按個比對和更新,詳細更新過程見下圖:

image.png

總結

Svelte 爲我們提供了 VDOM 之外另一種可能性,通過靜態編譯減少框架運行時的代碼量,讓編譯打包後的產物在完整實現功能的同時又有極高的性能和很小的體積,未來還將有很大的挖掘空間。

參考資料

  1. 《爲什麼VDOM是一個巨大的開銷》
  2. 《寫出最簡的代碼》
  3. 《如何看待Svelte這個前端框架》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章