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.wasmmain.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 无关紧要
应用型页面(登录后使用)可以用,入口页做个静态介绍页即可
内容型站点(文章、商品详情需被收录)不要用:改用服务端渲染或静态站点生成
需要被分享的落地页至少保证 titledescription、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。

笔记加载中…