公开页提速方案(传输压缩 + 公开页全面去 Flutter 化)

2026-07-07 设计 · 触发问题:jiu.51yanmei.com/app/product/<public_id>?code=P1331 首开约 30 秒 · 目标机 ali(3Mbps 固定带宽)· 用户拍板:所有公开页不使用 Flutter

一、问题与实测

公开商品页(顾客扫标签二维码 / 微信分享打开)走的是 Flutter Web 深链:无论从 /product/<id> 短链还是 /app/product/<id> 进来,后端 ProductPage 都返回注入了 OG 标签的 Flutter 应用壳 index.html,浏览器随后要下完整个管理端应用才渲染出一个商品卡。

环节大小实测耗时问题
HTML(含 OG 注入)2.1 KB0.05s正常
公开商品 API830 B0.05s正常
main.dart.js5.3 MB11.8s未压缩 无 gzip、无缓存头
canvaskit.wasm7.2 MB16.4s未压缩

三因叠加:① Flutter Web 首包 ~12.5MB;② nginx 全程无压缩(响应无 content-encoding);③ ECS 出网带宽固定 3Mbps(PayByBandwidth)→ 12.5MB ÷ 3Mbps ≈ 33 秒。重复访问可靠 etag 304,但每个第一次扫码的顾客都要全量拉。

二、方案总览(两条腿,独立生效、互相叠加)

方案解决什么改动面预期效果
A′ · 传输层压缩 + 缓存(瘦身版)员工 Web 版首开、API JSON 响应仅 nginx conf ~10 行Web 版首开 12.5MB → ~4.4MB,约 33s → ~12s;API 列表响应 −70%
B · 公开页全面 SSR(去 Flutter)顾客扫码 / 分享动线与 App 彻底解耦backend 直出商品页 + 店铺列表页两张模板公开页首开 ~20KB,< 0.5s,全程零 Flutter

公开面盘点(B 的完整范围):App 中匿名可访问的页面只有两张——商品公开页(/product/:public_id)与店铺公开商品列表页(Flutter 路由 /shop/:shop_code,现仅能从商品页点门店名进入,无独立浏览器短链:直开 /shop/S001 落营销站 404)。两张全部 SSR 化并互相跳转后,顾客动线不再触碰任何 Flutter 资源。登录/注册属 App 本体入口(员工用)不在范围;官网营销站本就是静态站。

A 仍然值得做——员工浏览器开 Web 版、以及公开页上「在 App 中打开」的兜底路径都吃到红利。

三、方案 A′:nginx 运行时 gzip + 缓存策略(瘦身版)

2026-07-07 用户评审后瘦身:B(公开页 SSR)落地后,「顾客要下 12.5MB」的场景消失,原方案 A1「CI 预压缩 + gzip_static」重武器不再必要,整体砍掉——client 流水线零改动。保留的 A′ 只有 nginx 里 ~10 行运行时配置,随 server 发版同车带出,覆盖剩余收益:员工浏览器开 Web 版首开 33s→12s(低频、一次性)、API JSON 响应压缩(库存列表类响应小 ~70%)、公开页「在 App 中打开」兜底路径。

A′1 · nginx 运行时 gzip

# server 块(deploy/nginx-jiu-ali.conf):低频大文件现压(2vCPU 压 5MB 约 0.1s,可接受)
gzip on;
gzip_comp_level 5;
gzip_min_length 1k;
gzip_vary on;
gzip_proxied any;
gzip_types application/javascript application/wasm application/json
           text/css text/plain image/svg+xml application/octet-stream;

图片不在列:JPEG/PNG 本身是压缩格式,gzip 无收益。图片链路现状已健康——上传时后端统一缩到最长边 1200px + JPEG q85(线上实测中位数 ~91KB/张),/images/ 已配 expires 30d + immutable,不动。

A′2 · 缓存策略:协商缓存(304),不做长缓存

Flutter Web 产物文件名无内容 hash(main.dart.js 固定名),长缓存会在发版后出现新旧混用(main.dart.js 与 canvaskit 版本错配会白屏)。因此:

路径策略说明
/app/index.htmlno-store(现状不变)入口永远最新
/app/ 其余全部Cache-Control: no-cache(协商)复访全部 etag 304,零字节;Flutter 自带 service worker 二次访问另有本地缓存

四、方案 B:公开页后端轻量直出(商品页 + 店铺列表页)

B1 · 现状与改造点

backend/internal/handler/public.goProductPage:读 Flutter index.html → 注入 OG 标签 → 返回应用壳。改造为:直接渲染一张自包含的轻量商品 HTML 页(Go html/template + go:embed 内嵌模板),不再依赖 Flutter 产物。

设计
模板位置backend/internal/handler/templates/product_page.html(go:embed,二进制自包含,部署不变)
页面内容主图(多图横滑,纯 CSS scroll-snap)· 品牌/名称(系列)· 规格 · 商品编号(?code= 参数回显,与现 App 公开页口径一致,实施时核对)· 产地/保质期/储存条件/介绍(基础数据字典字段,取自现有公开查询)· 门店名 + 联系电话 · 页脚「岩美酒库 · 正品溯源」
数据来源复用 GetProduct/api/v1/public/products/:id)已有的查询/字段组装逻辑,抽出共用函数——不新增查询口径,公开字段集与 API 完全一致(成本/进价等敏感字段本就不在其中)
OG 兼容OG meta(title/description/image/url)直接写进模板 head,复用现有 buildProductOG/buildOGTitle;微信/飞书分享卡片行为不变,且不再需要 UA 分流
样式内联 CSS,全页 < 20KB(gzip 后 ~6KB);配色沿用设计系统主题 A 色值(品牌蓝 #2563AC 系),移动优先单列卡片
图片加载全部异步拉取(2026-07-07 用户拍板):HTML 先到先渲染,图片不阻塞——首图 decoding="async" 优先加载,其余图 loading="lazy" 滚动到位才拉;店铺列表页同规则
兜底商品不存在 → 渲染同风格「商品不存在或已下架」页(HTTP 200,现状对爬虫友好的行为保持)
互跳门店名可点 → /shop/<shop_code>(B4 店铺 SSR 页);页脚小链接「在 App 中查看」→ /app/product/<id>(员工/深链兜底,普通顾客不需要)

B4 · 店铺公开商品列表页 SSR(新增短链)

现状:Flutter 内部路由 /shop/:shop_code(从商品公开页点门店名进入),功能 = 该店公开商品列表 + 关键字搜索 + 分页,数据源 ListShopProducts/api/v1/public/shops/:shop_code/products)。SSR 化设计:

设计
新路由GET /shop/:shop_code(backend,router.go 公开区)——店铺公开页从此有独立浏览器短链,可印店铺码/分享
模板templates/shop_page.html(同 go:embed / 同风格):店铺名+logo · 搜索框(GET 表单 ?q=,服务端过滤)· 商品卡列表(图/品牌型号版本/编号,图片 loading="lazy" 原生懒加载——20 卡×~90KB 主图只按滚动加载)· 上一页/下一页链接(?page=N,沿用 API 的 page_size=20 上限)· 每张商品卡链接 /product/<public_id>
数据来源复用 ListShopProducts 的查询组装(与 API 同一函数抽共用),公开字段集一致,零新增口径
OGhead 输出店铺级 OG(店名 + logo + 「共 N 件商品」),微信分享店铺链接出卡片
限流/反爬路由挂与 shops API 同一套闸:shopListIP 分钟限流 + dqShopList 日配额(300/IP/日);nginx 新增 location ~ ^/shop/,进 jiu_shoplist 2r/s 专属 zone + $jiu_bad_ua_lib + limit_conn 10(与 shops API location 同参数,且同样须放在通用 location 之前)
兜底店铺不存在 / 未开公开页 → 同风格「店铺不存在」页;无商品 → 空态文案
App 内导航Flutter /shop/:shop_code 内部路由保留不动(员工 App 内预览用,不发 HTTP)

B2 · 入口矩阵(改造后各入口去向)

入口现状改造后
标签二维码 / App「复制公开链接」= /product/<id>Flutter 壳(~30s)轻量页 <0.5s 历史已印标签直接受益,无需换码
浏览器直开 / 刷新 /app/product/<id>nginx rewrite → 同一 ProductPage同上轻量页(nginx 该 location 不动)
App 内浏览公开页(Flutter 路由 /product/:public_id/shop/:shop_code客户端内部导航,不发 HTTP不受影响
浏览器直开 /shop/<shop_code>404(落营销站,无此入口)店铺 SSR 页 新增短链,可印店铺码
商品公开页点门店名App 内跳 Flutter 店铺页SSR 商品页 → SSR 店铺页,全程零 Flutter
微信/飞书爬虫抓分享卡片OG 注入壳OG 在轻量页 head,行为不变(店铺页新增店铺级 OG)
公开 API /api/v1/public/products/:id/api/v1/public/shops/:code/productsApp 数据源零改动

B3 · 反爬与限流

路由与闸全部沿用现状:publicReadIP 分钟限流 + dqProduct 日配额(1000/IP/日)+ nginx $jiu_bad_ua_lib/limit_conn 不动。轻量页比现在还省服务器资源(不再读盘 index.html,模板编译一次常驻)。

五、发版与回滚

批次内容tag
唯一一批方案 B(商品页 + 店铺页 SSR)+ 方案 A′(nginx gzip/缓存头,含新增 /shop location)——全部 server 流水线一次带出,client 零改动server-v1.1.8

验收清单(上线后逐条打勾)

  1. curl -H 'Accept-Encoding: gzip' --compressed -so /dev/null -w '%{size_download}' https://jiu.51yanmei.com/app/main.dart.js → 响应带 content-encoding: gzip,下载量 ~1.3MB(原 5.3MB)
  2. /product/<id> 首开 < 0.5s(total time),页面含商品名/图/门店信息
  3. 微信发送 /product/<id> 链接 → 分享卡片(标题/图)正常
  4. 响应体不含 cost / purchase_price 等敏感字段名
  5. App 内「公开页预览」导航正常;/app/product/<id> 刷新落到轻量页
  6. /shop/<code> 直开出店铺列表页,搜索(?q=)与翻页(?page=)可用;商品卡点进 /product/<id>;超日配额出 429
  7. 手机弱网(蜂窝)实测扫标签二维码体感

六、明确不做(本期)