前端框架的JIT與AOT,傻傻分不清楚
大家好,我卡頌。
現(xiàn)代前端框架都需要“編譯”這一步驟,用于:
- 將框架中描述的UI轉換為宿主環(huán)境可識別的代碼
- 代碼轉化,比如將ts編譯為js、實現(xiàn)polyfill等
- 執(zhí)行一些編譯時優(yōu)化
- 代碼打包、壓縮、混淆
編譯可以選擇放在兩個時機執(zhí)行:
- 代碼構建時,被稱為AOT(Ahead Of Time,提前編譯或預編譯),宿主環(huán)境獲得的是編譯后的代碼
- 代碼在宿主環(huán)境執(zhí)行時,被稱為JIT(Just In Time,即時編譯),代碼在宿主環(huán)境編譯并執(zhí)行
本文會聊聊兩者的區(qū)別,及前端框架中AOT的應用。
AOT和JIT的區(qū)別
Angular同時提供這兩種編譯方案,下面我們用Angular舉例說明兩者的區(qū)別。
考慮如下Angular代碼:
- import { Component } from "@angular/core";
- @Component({
- selector: "app-root",
- template: "<h3>{{getTitle()}}</h3>"
- })
- export class AppComponent {
- public getTitle() {
- return 'Hello World';
- }
- }
定義AppComponent,最終瀏覽器(作為宿主環(huán)境)渲染的結果為:
現(xiàn)在將模版中使用的getTitle方法修改為未定義的getTitleXXX:
- // 從
- template: "<h3>{{getTitle()}}</h3>"
- // 修改為
- template: "<h3>{{getTitleXXX()}}</h3>"
如果使用AOT,編譯后會立刻報錯:
ERROR occurs in the template of component AppComponent.
如果使用JIT,編譯后不會報錯,代碼在瀏覽器中執(zhí)行時會報錯:
ERROR TypeError: _co.getTitleXXX is not a function
造成以上區(qū)別的原因是:當使用JIT時,構建階段僅僅使用tsc將ts編譯為js并將代碼打包。
打包后的代碼在瀏覽器運行后,執(zhí)行到Decorator(上例中的@Component語句)時,Angular的模版編譯器才開始編譯template字段包含的模版語法,并報錯。
當使用AOT時,tsc、Angular的模版編譯器都會在構建階段進行編譯,所以會立刻發(fā)現(xiàn)template字段包含的錯誤。
除了以上區(qū)別外,JIT與AOT的區(qū)別還包括:
- 使用JIT的應用在首次加載時慢于AOT,因為其需要先編譯代碼,而使用AOT的應用已經(jīng)在構建時完成編譯,可以直接執(zhí)行代碼
- 使用JIT的應用代碼體積普遍大于使用AOT的應用,因為在運行時會多出編譯器代碼
基于以上原因,在Angular中一般在開發(fā)環(huán)境使用JIT,在生產(chǎn)環(huán)境使用AOT。
從前端框架的角度看AOT可以用兩個步驟描述前端框架的工作原理:
- 根據(jù)組件狀態(tài)變化找到變化的UI
- 將UI變化渲染為宿主環(huán)境的真實UI
借助AOT對模版語法編譯時的優(yōu)化,就能減少步驟1的開銷。
這是大部分采用模版語法描述UI的前端框架都會進行的優(yōu)化,比如Vue3、Angular、Svelte。
其本質原因在于模版語法的寫法是固定的,固定意味著「可分析」。
「可分析」意味著在編譯時可以標記模版語法中的靜態(tài)部分(不變的部分)與動態(tài)部分(包含自變量,可變的部分),使步驟1在尋找變化的UI時可以跳過靜態(tài)部分。
甚至Svelte、Solid.js直接利用AOT在編譯時建立了「組件狀態(tài)與UI中動態(tài)部分的關系」,在運行時,組件狀態(tài)變化后,可以直接執(zhí)行步驟2。
AOT與JSX
而采用JSX描述UI的前端框架則很難從AOT中受益。
原因在于JSX是ES的語法糖,作為JS語句只有執(zhí)行后才能知道結果,所以很難被靜態(tài)分析。
為了讓使用JSX描述UI的前端框架在AOT中受益,有兩個思路:
- 使用新的AOT思路
- 約束JSX的靈活性
React嘗試過第一種思路。prepack是meta(原Facebook)推出的一款React編譯器,用來實現(xiàn)AOT優(yōu)化。
他的思路是:在保持運行結果一致的情況下,改變源代碼的運行邏輯,輸出性能更高的代碼。
即:代碼在編譯時將計算結果保留在編譯后代碼中,而不是在運行時才去求值。
比如,如下代碼:
- (function () {
- function hello() { return 'hello'; }
- function world() { return 'world'; }
- global.s = hello() + ' ' + world();
- })();
經(jīng)由prepack編譯后輸出:
- s = "hello world";
遺憾的是,由于復雜度以及人力成本考慮,prepack項目已于三年前暫停了。
Solid.js同樣使用JSX描述視圖,他實現(xiàn)了幾個內(nèi)置組件用于描述UI的邏輯,從而減少JSX的靈活性,使AOT成為可能。比如:
For替代數(shù)組的map方法:
- <For each={state.list} fallback={<div>Loading...</div>}>
- {(item) => <div>{item}</div>}
- </For>
Show替代if條件語句:
- <Show when={state.count > 0} fallback={<div>Loading...</div>}>
- <div>My Content</div>
- </Show>
總結
總結一下,前端框架可以從AOT中收獲很多益處,其中最主要的一條是:
減少“根據(jù)組件狀態(tài)變化找到變化的UI”這一步驟的工作量
要實現(xiàn)AOT的前提是:組件代碼易于分析。