Flutter Web 构建与部署
Flutter Web 能用同一份 Dart 代码产出网页,但它的定位是“把应用搬到浏览器”,不是“写一个网站”。理解这一点,很多坑就不会踩:首屏体积比常规前端大、爬虫抓不到内容、刷新子路由 404。本章按“选渲染方式 → 构建 → 看懂产物 → 部署 → 首屏优化 → 认清 SEO 边界”的顺序讲。
渲染方式的演进
| 方式 | 状态 | 特点 |
|---|---|---|
| HTML renderer | 已弃用并在新版本移除 | 用 DOM 渲染,包小但样式与自绘不一致 |
| CanvasKit | 当前默认(--wasm 之外) | 渲染与移动端一致,首次需下载 canvaskit 资源 |
skwasm(--wasm) | 推荐的新选项 | 基于 WebAssembly,性能更好,但要求浏览器支持 WasmGC |
结论:新项目不要再依赖 --web-renderer,该参数在新版本已被移除;需要 WebAssembly 版本时直接加 --wasm,否则用默认(CanvasKit)即可。
构建命令
flutter build web --release # 默认产物,进入 build/web
flutter build web --release --wasm # 使用 skwasm(需要 WasmGC 支持的浏览器)
flutter build web --release --web-renderer canvaskit # 旧写法:该参数已在新版本移除,老项目升级时需改掉
flutter build web --release --base-href /app/ # 部署在子路径时必须指定,否则资源 404
flutter build web --release --source-maps # 保留 source map,便于定位线上 Dart 异常
flutter build web --release --pwa-strategy=offline-first # 控制 Service Worker 策略
flutter run -d chrome --web-port=8080 # 本地调试
--base-href 对应的是 index.html 里资源前缀,凡是部署在 https://site.com/app/ 这类子路径下的场景都必须设置,且末尾要带斜杠。
产物结构
| 文件/目录 | 说明 | 优化点 |
|---|---|---|
index.html | 页面入口,内嵌加载逻辑 | 可加自定义加载动画 |
flutter_bootstrap.js | 新版引导脚本,负责加载引擎 | 可在此配置 base、字体与资源 |
main.dart.js | 编译后的业务代码,通常最大 | 用 gzip/brotli 压缩 |
canvaskit/ | CanvasKit wasm 与 js | 用 CDN 或长缓存 |
assets/ | 字体、图片等资源 | 字体子集化,图片转 WebP |
version.json | 版本与 Service Worker 信息 | 用于灰度与强制更新 |
一个中等复杂度应用的 main.dart.js 常见在 24 MB(未压缩),gzip 后约 800 KB1.5 MB,这是 Flutter Web 首屏偏慢的主因。
Nginx 部署配置
server {
listen 80;
root /var/www/app; # flutter build web 的产物目录
# 单页应用:任何未命中的路径都回落到 index.html,解决刷新 404
location / {
try_files $uri $uri/ /index.html;
}
# 压缩:wasm 用 br 或 gzip,文本资源统一开 gzip
gzip on;
gzip_types text/plain text/css application/javascript application/wasm application/json;
gzip_min_length 1024;
# 带哈希的产物可以长缓存,index.html 与 version.json 必须不缓存
location ~* \.(js|wasm|otf|ttf|png|jpg|webp)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
location = /index.html { add_header Cache-Control "no-cache"; }
location = /version.json { add_header Cache-Control "no-cache"; }
}
首屏优化
- 用
flutter_bootstrap.js里的_flutter.loader.load()配置加载提示:先把静态 HTML 加载动画展示出来,等 Flutter 首帧再切换,避免长时间白屏。 - 预加载关键资源:在
index.html里给canvaskit.wasm、main.dart.js加<link rel="preload">,可省掉一轮 RTT。 - 字体是隐形大头:中文字体动辄数 MB,Web 端优先用系统字体,必须用时做子集化。
- 开启 brotli 比 gzip 再省 15%~20%;CDN 与静态托管平台(如 Netlify、Vercel、OSS + CDN)通常已内置。
- 首屏依赖的接口要做缓存与骨架屏,不要等数据齐了再渲染。
SEO 的现实限制
Flutter Web 默认在 Canvas 上绘制文字,搜索引擎看到的是一个近乎空的 HTML 与 canvas 标签,这对内容型站点是致命问题,因此:
| 场景 | 建议 |
|---|---|
| 后台管理系统、内部工具 | 放心用 Flutter Web,SEO 无关紧要 |
| 应用型页面(登录后使用) | 可以用,入口页做个静态介绍页即可 |
| 内容型站点(文章、商品详情需被收录) | 不要用:改用服务端渲染或静态站点生成 |
| 需要被分享的落地页 | 至少保证 title、description、Open Graph 标签正确 |
Flutter 官方有语义树(Semantics)支持,但对爬虫的帮助有限;折中方案是主站用静态页承载内容与 SEO,交互复杂的功能页用 Flutter Web。
路由:hash 与 path
| 方式 | URL 示例 | 优点 | 缺点 |
|---|---|---|---|
| hash | /app/#/detail/1 | 部署零配置,刷新不会 404 | 链接不美观,部分统计工具识别差 |
| path | /app/detail/1 | 链接标准,利于分享与统计 | 服务器必须配 try_files 回落,否则刷新 404 |
go_router 通过 usePathUrlStrategy() 切换成 path 模式;用 hash 模式时前端路由与后端无关,属于“省事但不优雅”的选择。
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
子路径部署忘记 --base-href | 页面白屏、控制台 404 | 构建时指定与部署路径一致的前缀 |
| 刷新子路由 404 | 直接访问深链接失败 | Nginx try_files 回落到 index.html |
继续使用 --web-renderer | 新版本报参数不存在 | 改用 --wasm 或默认渲染 |
缓存 index.html | 发版后用户仍看到旧版本 | 只对带哈希资源长缓存 |
| 把 Flutter Web 当内容站 | 搜索引擎收录为零 | 内容站用 SSR/静态页 |
| 忽略字体体积 | 首屏加载数 MB 字体 | 系统字体优先 + 子集化 |
小结:Flutter Web 的构建只有 flutter build web --release 一条主线,子路径加 --base-href、要 Wasm 加 --wasm(旧的 --web-renderer 已被取代);部署要点是 try_files 回落、gzip/brotli 压缩与“哈希资源长缓存、index.html 不缓存”;首屏用 flutter_bootstrap.js 做加载提示并预加载关键资源,内容型站点不要指望 Canvas 渲染被收录,该用 SSR 就用 SSR。