iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Modern Web

Vue 前端工程師視角看 Flutter Web系列 第 21

Day 21|Flutter Web build 流程:CanvasKit 與 renderer 選項

  • 分享至 

  • xImage
  •  

模組五|建置與部署(Day 20–24)

先備:Day 1(畫布不是 DOM)、Day 20(Vite 的基準線與 AOT)。本篇的 code 全是指令與目錄樹,不用背,看懂產物裡多了什麼就夠了。

npm run build 產出的是一包 JS;flutter build web 產出的是一包 JS 加上一顆渲染引擎。這句話是整個模組五的核心,今天把它拆開講清楚。Day 20 已經把 Vite 這一側的基準線立好了——vite build 從頭到尾只處理你自己寫的 code,不會憑空多出別的東西;今天要講的就是另一邊為什麼會多出那顆引擎。

先說我在這題上的立場,免得你以為我要教你調參數:我的 demo(flight-booking-flutter,一個訂機票流程的示範 app,同一份需求我另外用 Vue 也做了一份,整個系列都拿這兩份互相對照)從頭到尾沒碰過任何 renderer 設定。這篇的價值在於解釋為什麼「不碰」在今天是合理的預設,以及哪一天你會被迫非碰不可。

結論先講:

Vue 出貨的是一份樂譜,瀏覽器就是現成的樂團;Flutter Web 出貨的是樂譜加上一整支樂團。

樂團很專業,演奏一致性極高——代價是每個聽眾都得先等樂團搬進場。

Vue 怎麼做

先意識到一件我們從沒想過的事:寫了十年 Vue,我沒有選過 renderer。倒不是 Vue 替我選了,這個選項在 Web 上根本不存在。

Vue 做的事到 render function 為止。SFC 的 template 被編譯成 render function,執行時產生 VNode,diff 完呼叫 document.createElementel.setAttributeel.textContent 這些原生 API——然後就沒有然後了。從 DOM 樹到 layout、到 paint、到 composite,全部是瀏覽器的活。Chrome 的 Blink、Safari 的 WebKit 就是我們的渲染引擎,它們不佔 bundle 一個 byte,因為早就裝在使用者的機器上,而且會隨著系統更新自己變好。

這個「白吃的午餐」還附贈了三樣我們平常視為理所當然的東西:排版引擎(斷行、連字、雙向文字、text-wrap: balance,全部免費)、字型系統(系統字型零下載,font-display 幫你處理載入期的閃爍)、無障礙樹(DOM 天生就是語意結構,螢幕閱讀器直接讀得懂)。這三件事沒有任何一件是「Vue 做得好」,它們是 Web 平台送的。

所以 Vue 的 build 流程只需要處理你自己寫的 code——vite build 的輸入是你的 .vue.ts,輸出是你的 .js.css,中間不會憑空多出一顆引擎。這是明天那個量級差最根本的原因,我先把因果放在這裡,明天只負責量數字。

Flutter Web 怎麼做

Flutter 的立場完全相反:它不信任瀏覽器的排版與繪製,自帶 Skia(Google 的 2D 繪圖引擎,Chrome 自己畫網頁用的也是它)把每個像素畫進 <canvas>(Day 1 講過的「畫布不是 DOM」)。既然要自己畫,build 產物就必須包含引擎本體。歷史上它給過三條路線:

HTML renderer(已死):用 DOM/CSS/Canvas 2D 混合模擬 Flutter 的繪製,bundle 小,但視覺一致性與效能都差。Flutter 3.29 正式移除,官方文件現在只把它當歷史名詞。2021 年 Todd 前輩的系列還在認真比較兩種 renderer 的取捨——四年後這個選擇題本身消失了,這就是我在 Day 1 說「值得重新回答一次」的意思。

CanvasKit(現行預設):Skia 編譯成 WebAssembly(簡稱 WASM,一種瀏覽器能直接執行的低階二進位格式,用來把 C++ 這類語言搬上網頁,跑起來比 JS 快),你的 Dart code 由 dart2js 編成 JS,兩者協作繪製。它就是今天你什麼都不設定會拿到的東西,也是我的 demo 實際在跑的東西。

skwasm(WASM 路線):名字就是 Skia + Wasm 拼起來的。Dart 由 dart2wasm 直接編成 WASM,配上精簡版的 Skia,並支援把渲染工作丟進 worker。啟用它沒有設定檔可選,你換的是編譯目標:

  # 預設路線:dart2js + CanvasKit
flutter build web

  # WASM 路線:一次產出雙產物
flutter build web --wasm

--wasm 會同時編出 dart2wasm 與 dart2js 兩份產物,執行時由 flutter_bootstrap.js 做 feature detection——瀏覽器支援 WasmGC(WebAssembly 的垃圾回收提案,Dart 編成 WASM 的前提)就跑 WASM,不支援就退回 JS。舊教學裡的 --web-renderer flag 已經移除,網路上寫著 --web-renderer html 的文章可以直接跳過。

講完編譯目標,直接看產物最快。build 完的 build/web/ 長這樣(節錄)——檔名不用背,只要看出哪幾個是你寫的、哪幾個是引擎附贈的就夠了:

build/web/
├── index.html
├── flutter.js / flutter_bootstrap.js   (載入器與版本指紋)
├── main.dart.js                        (你的 app,dart2js 產物)
├── canvaskit/
│   ├── canvaskit.js
│   └── canvaskit.wasm                  (渲染引擎本體,大頭在這)
└── assets/                             (字型、圖片、shader)

main.dart.js 是你寫的東西,canvaskit/ 是那支樂團。兩者的體積關係,明天上秤。

誠實揭露:我一個 renderer 設定都沒下過

寫到這裡該交代自己的底細了。我在 flight-booking-flutter 整個 repo 裡 grep 過 web-rendererwasmcanvaskit 三個關鍵字(排除 build/ 產物目錄),零命中。沒有 --wasm、沒有任何 renderer 相關的 flag,Makefile 裡就是一行乾乾淨淨的 flutter build web --release

web/index.html 也一樣素。Flutter 的 index.html 樣板在 <body> 開頭留了一段註解,明白告訴你「你可以客製 flutter_bootstrap.js,用來給 Flutter loader 特殊設定,或在初始化過程中給使用者一點回饋」——那段註解原封不動躺在我的檔案第 36 到 43 行,底下什麼都沒有,直接就是 <script src="flutter_bootstrap.js" async>。官方在檔案裡開好的那扇門,我一次都沒推開過。

index.html:44-60 確實有一段我手寫的 script,但那跟 renderer 無關,是為了繞開一個 macOS Chrome 的觸控板崩潰——那是 Day 23 的主角,今天先放著。)

我把這件事寫出來,是因為它本身就是一個結論:在 2026 年,Flutter Web 的 renderer 對大多數專案已經退化成一個「不需要決策的決策」。 預設就是唯一還活著的通用選項,--wasm 是給有明確效能痛點、而且部署環境設得了 HTTP header 的人準備的。你要是也什麼都沒調,你跟我一樣正常。

差異與坑

坑一:「選 renderer」已經變成「選編譯目標」。 心智模型從「用哪一套畫」變成「Dart 編成什麼格式」,這是近兩年 Flutter Web 最大的路線變動。查資料務必看文章日期,2023 年以前的內容多半整段失效。症狀:你照著搜尋結果第一名的教學下 --web-renderer canvaskit,指令直接吐 Could not find an option named "--web-renderer",而那篇文章看起來寫得很認真、留言區還在稱讚。

坑二:skwasm 的隱藏門檻是 cross-origin isolation。 要吃到多執行緒渲染,host 必須回 Cross-Origin-Opener-PolicyCross-Origin-Embedder-Policy 兩個 header(cross-origin isolation/COOP/COEP:一組 HTTP header,開了才能用 SharedArrayBuffer,也才有多執行緒),而 SharedArrayBuffer(讓多個執行緒共用同一塊記憶體,是 WASM 多執行緒的基礎)正是 worker 之間傳圖的管道。純靜態託管平台不一定讓你設 header,這個坑 Day 23 講實際部署上線時會再撞一次。症狀:--wasm build 部署後,DevTools 顯示 crossOriginIsolated === false,渲染退回單執行緒,動畫掉幀但沒有任何錯誤訊息。

坑三:瀏覽器支援不齊,fallback 是必需品。 WasmGC 在 Chromium 系已穩(Chromium 119 起支援);Safari 如今也支援 WasmGC 了,但官方文件明寫目前有一個 bug 擋住 Flutter 的 Wasm renderer,而 iOS 更硬:所有瀏覽器都被要求用 WebKit,Flutter 編成 Wasm 的產物在 iOS 上完全沒得跑。這代表 --wasm 的部署包裡永遠躺著一份當下用不到的產物——雙產物的體積是常態,不是暫時的過渡狀態。症狀:你興沖沖用 --wasm build 完,發現 build/web/ 比原本更大,而 iPhone 上的使用者拿到的還是那份 dart2js 產物。

坑四:build target 的語意,兩邊差一個層級。 Vite 的 build.target 是「編到哪一年的 JS 語法」,選錯了頂多多幾個 polyfill;Flutter 的 build target 是「編成哪一種執行格式」,選錯了整條載入路徑、整組 header 需求、整套瀏覽器相容矩陣全部換一套。前者是刻度,後者是岔路。症狀:你把 --wasm 當成「加個 flag 讓它快一點」順手加進 CI,結果部署後某些使用者白屏,而你在本機 Chrome 上怎麼測都正常。

順帶記一筆 agent 工作流的觀察:這一層恰好是強型別幫不上忙的地帶。編譯目標、HTTP header、平台能力全都在 Dart 型別系統的射程之外,agent 在這裡跟在 nginx conf 裡一樣是瞎子——它會很有自信地建議你加 --web-renderer canvaskit,因為訓練資料裡那個 flag 還活著。編譯期救得了你的地方,agent 快得驚人;救不了的地方,你得自己驗。

小結與下一篇

一句話:Flutter Web 的 build 不是打包你的 code,是連渲染引擎一起出貨——理解這件事,明天的 bundle size 對決才看得懂。

而我對 renderer 的全部貢獻是零行設定,這件事本身就說明現在的預設有多夠用。Day 22 把 Vue 和 Flutter Web 各自丟上秤,實測 bundle size 與首屏載入,順便把兩個 repo 的原始碼行數也一起量了。

參考資料

如果你卡在語法

深入原理


上一篇
Day 20|Vite build 流程與最佳化策略回顧
系列文
Vue 前端工程師視角看 Flutter Web21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言