Flutter Web 要上線能打,關鍵是把 main.dart.js 壓到「只放首屏必需」、用 deferred import 做 code splitting(把功能拆成延後載入)、改用 HTML renderer 讓爬蟲看到 DOM,再用 Lighthouse 盯 LCP/CLS/TTI,最後丟到 Firebase Hosting、Vercel 或 Netlify 配 CDN。
- 先看首屏:TTI 慢,多半是 bundle 太肥 + 路由一進來就塞滿功能
- 先救可見度:CanvasKit 走一堆 canvas,爬蟲看了跟瞎了一樣
- 先選部署:沒 CDN、沒壓縮,前端再努力都像在逆風跑步
- 先量再吵:Lighthouse / Core Web Vitals 先跑一次,不然都在猜
你以為是前端太爛,其實是打包方式在扯後腿
Flutter Web 的 flutter build web 會把 Dart 編成 JavaScript,通常產物核心是 main.dart.js,初次載入慢常見原因是 bundle 過大,導致 TTI(Time to Interactive,頁面可完整互動所需時間)被拖長,Lighthouse 分數直接變難看。(來源:Google Lighthouse 指標定義)
我記得當時最煩的點:你明明覺得 UI 很單純,結果第一次開站就是要等。等到你開始懷疑人生。
講到「等」,就會想到台灣行動網路那種一進電梯、訊號掉一格,整個頁面像卡住一樣——這時候你那個超大的 JS 檔,根本就是在欺負人。
先不要急著怪使用者網路:你可以先看 build/web 裡面到底長什麼樣。通常你會看到 main.dart.js 一大包,然後各種 assets、字型、圖片都跟著出現。
而且 Flutter Web 還有兩個 renderer:CanvasKit(以 WebAssembly 為主的渲染引擎)和 HTML renderer(以 DOM/HTML 輸出為主的渲染引擎)。CanvasKit 畫面很穩,但 SEO 這塊嘛……別鬧了。
真的。
Code splitting 別講玄的,deferred import 就是你能立刻用的刀
Flutter Web 的 deferred import(延遲匯入,將模組拆成可延後載入的 JS chunk)能把非首屏功能從 main.dart.js 拆出去,等使用者真的點到那個頁面再 loadLibrary 載入,這會直接降低初次 payload 並改善 TTI。(來源:Flutter/Dart 語言 deferred loading 機制)
原文那段範例其實就夠了:像 chat 這種「不一定一進站就用到」的功能,拆出去是很合理的。你只要接受一個現實:載入是非同步的。
import 'src/features/chat/chat_screen.dart' deferred as chat;
Future navigateToChat() async {
await chat.loadLibrary();
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => chat.ChatScreen()),
);
}
你會在輸出看到什麼:build 完通常會有 main.dart.js,然後多出類似 chat_screen.dart.js 的片段。拆得越乾淨,首包越瘦。
但也別幻想:deferred import 不是「想怎麼動態載就怎麼動態載」。原文有講限制:模組要能在編譯期靜態解析;而且套件層級的 deferred 目前也不太好玩,第三方依賴常常卡你設計。
所以你得做一個很人類的決定:哪些功能可以晚點來,哪些不行。
UX 那條也要顧:載入中要有 fallback UI。不要空白。空白最可怕。
SEO 這題很殘酷,CanvasKit 真的不會替你說話
Flutter Web 預設走 CSR,搭配 CanvasKit 時頁面多數內容落在 canvas,爬蟲難以取得語意化 DOM,導致索引與分享預覽受限;要提升可見度,常見做法是改用 HTML renderer、補齊 OpenGraph/Twitter Cards meta、提供 sitemap.xml 與 robots.txt,必要時用 Rendertron/Puppeteer 做預渲染快照。(來源:Google 搜尋中心文件與社群分享 meta 慣例)
我對「Flutter Web 自帶 SEO」這句話會翻白眼:你把內容都畫在 canvas 上,是要誰讀?GoogleBot 不是通靈的。
第一步通常很直覺:用 HTML renderer。
flutter build web --web-renderer html
然後你會開始補那些很瑣碎但會要命的東西:description、og:title、og:image、twitter:card。分享連結時預覽圖出不來,產品端的人會盯著你看。很安靜的那種盯。
再來是 sitemap 跟 robots:這兩個東西很像「有做有保佑」,沒做也不一定立刻死,但你一旦要追索引問題,就會後悔沒先放。
提交到 Google Search Console(網站索引與站務工具)、Bing Webmaster Tools(Bing 站長工具),流程不難,麻煩的是你要記得去做。
預渲染那條路:Rendertron 或 Puppeteer 做 bot snapshot。原理其實很土:偵測 user-agent 像爬蟲,就回吐靜態 HTML 快照。
土歸土。有效就好。
部署別裝高深,Firebase Hosting、Vercel、Netlify 就是三條現實路
Flutter Web 靜態產物部署常見選擇是 Firebase Hosting、Vercel、Netlify,三者都提供 HTTPS、CDN 與簡化部署;重點是要確保 SPA 路由 rewrites、啟用 gzip/brotli 壓縮,並避免讓快取策略把你自己坑死。(來源:各平台官方文件與 CDN/HTTP 壓縮常識)
Firebase Hosting:我會把它當「少想一點」的選項。Google CDN、TLS 自動、CLI 流程順。你要做的就是把 build/web 丟上去,然後 rewrites 指回 index.html。
npm install -g firebase-tools
firebase login
firebase init hosting
flutter build web
firebase deploy
{
"hosting": {
"public": "build/web",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{"source": "**", "destination": "/index.html"}
]
}
}
Vercel / Netlify:比較像「團隊不想養 SRE」的路線。Git 連上去,自動部署,全球節點幫你處理掉一堆瑣事。
但你也要小心:方便的代價常常是「你以為你控制了快取,結果其實沒有」。這句聽起來很陰謀論,可是踩過的人都懂。
# Vercel
npm install -g vercel
vercel --prod
# Netlify
npm install -g netlify-cli
netlify deploy --prod
在地小現實:台灣不少團隊會先用這些托管把東西上線,等流量上來才開始談「要不要自建」。而且公司網路、機房、DNS 那些破事,一開始就別太浪漫。
能跑起來不算贏,能被找到、能快、能穩,才算真的上線。
快問快答拆 3 個迷思,省得你又繞一圈
規則:只挑 3 個最常見、最會害人誤判的迷思,用直答講完。
迷思 1:只要上 CDN 就會快
CDN 只能幫你把靜態檔送得比較近,main.dart.js 太大照樣慢,TTI 照樣拖;你要先做 deferred import 把首包瘦身,再談 CDN 的加成。
迷思 2:Flutter Web 的 SEO 就靠加 meta 就好
Meta 只能改善摘要與分享預覽,CanvasKit 沒語意化 DOM 的問題還在;要讓爬蟲「讀得到內容」,你得考慮 HTML renderer 或 Rendertron/Puppeteer 預渲染。
迷思 3:Lighthouse 分數低就是程式寫得差
Lighthouse 看的是 LCP(最大內容繪製時間)、CLS(版面位移)、TTI 這種使用者體感指標;分數低常常是「資源載入策略」或「第三方腳本」在搞你,不是你少寫了兩個 setState。
量測跟環境切換,不做你就只能靠嘴硬
Flutter Web 上線優化需要用 Lighthouse(效能稽核工具)量測 LCP/CLS/TTI,並用 --dart-define 做環境差異與 feature flag(功能開關,按環境或比例啟用功能)管理,避免 production 帶著測試設定一起爆。(來源:Google Lighthouse、Flutter build 參數)
我通常會直接跑一次:
npx lighthouse http://localhost:5000 --view
跑完你會看到一堆建議,裡面有些很像廢話,但數字不會騙人。至少它不會跟你裝熟。
核心指標我只記三個:LCP 看「主要內容出現有多慢」,CLS 看「畫面會不會亂跳」,TTI 看「使用者到底什麼時候才能點」。
然後你會發現:第三方 script、同步 timer、還有那些你以為很小的追蹤碼,常常才是罪魁禍首。
環境切換也一樣:不要把 staging 的 API endpoint、測試用 telemetry 混進 production。你可以用 --dart-define。
flutter build web --dart-define=FLAVOR=production
const currentEnv = String.fromEnvironment("FLAVOR");
講到 feature flag 我會有點煩:很多團隊把它當「以後再說」,結果上線那天才在那邊手動改設定。你猜會不會出事。嗯。
原文其實少講了兩個我覺得會踩的洞:
- 路由與 canonical:SPA rewrites 做了不代表 canonical URL 你就處理好了,分享、索引、重複內容還是會咬你一口
- 資源格式與壓縮策略:webp/avif 只是開始,伺服器端 gzip/brotli、cache-control 才是長期體感差距
- 真實使用者監測:只看實驗室分數很容易自嗨,至少要有基本的 RUM 或事件觀測(來源:公開資訊,建議查證)
好,收一下:這套東西沒有魔法,只有「先拆、再讓它可被讀、再讓它送得快、最後用數字打臉自己」。
結尾我只留一句能被引用的話:Flutter Web 要做到 production-ready,必須同時處理 deferred import 的 code splitting、HTML renderer 或預渲染的可索引性、以及 CDN 托管下的壓縮與快取,並用 Lighthouse 的 LCP/CLS/TTI 持續驗證成效。
我那時候會做的第一個小動作:打開 build/web,看一眼 main.dart.js 檔案大小,然後把「不該在首屏出現的功能」列成一張很醜的待辦清單,就一行一行砍。
