配套文档:performance-analysis.html · 生成日期 2026-04-19 · 代码基准
release@3f254fd71
ignoreBuildErrors / ignoreDuringBuilds 放在 P0,避免一边优化一边引入回归。基线:Lighthouse 2026-04-19 实测 · 8 个页面 · 最差页取值
| 指标 | 基线实测 | 最差页 | P0 末目标 | P1 末目标 | 最终目标 | 如何测 |
|---|---|---|---|---|---|---|
| Performance Score | 25–46 | /app/workflow | ≥ 50 | ≥ 80 | ≥ 90 | Lighthouse CLI |
| LCP (移动 Slow 4G) | 44.9 s | /app/workflow | < 15 s | < 5 s | < 2.5 s | Lighthouse × 3 取中位 |
| FCP | 9.7 s | /app/workflow | < 4 s | < 2 s | < 1.8 s | Lighthouse |
| TBT | 2.4 s | /app/workflow | < 1 s | < 0.4 s | < 0.2 s | Lighthouse |
| TTI | 45.2 s | /app/workflow | < 18 s | < 7 s | < 3.8 s | Lighthouse |
| CLS | 0.145 | /app/strategy-studio | < 0.1 | < 0.08 | < 0.05 | strategy-studio 专题修 |
| 主线程工作 | 45.8 s | /app/strategy-studio | < 18 s | < 6 s | < 2 s | Lighthouse main-thread-tasks |
| Unused JS | 1.6 MB | /home | < 1 MB | < 400 KB | < 200 KB | Lighthouse unused-javascript |
| 单页面下载 | 11.0 MB | /app/strategy-studio | < 6 MB | < 3 MB | < 1.5 MB | Lighthouse network-requests |
| 重定向链 (P0.0) | 7.8 s | /app/workflow | 0 s | 0 s | 0 s | Lighthouse redirects audit |
| INP(真实用户) | 未测,估 > 500 ms | – | < 300 ms | < 200 ms | < 100 ms | Cloudflare Web Analytics P75 |
| public/ 总体积 | 138 MB | – | < 95 MB | < 60 MB | < 40 MB | du -sh public/ |
全局硬性门槛(P1 起):首页、/about、/use-case、/how-to-guides/[slug] 必须全部为 SSG;对应路由不能出现 'use client'。
| URL | 类型 | 基线 Score | 基线 LCP | 关注指标 |
|---|---|---|---|---|
/home |
产品首页(匿名) | 28 | 20.8 s | LCP, TBT |
/about |
关于页 | 29 | 21.0 s | LCP(大 SVG + YouTube) |
/use-cases |
use-cases 列表页 | 33 | 17.1 s | LCP, 路由包体 |
/how-to-guides/btc-eth-trading-strategy-complete-battle-plan |
重型模板页 | 46 | 4.0 s | 路由包体(30 份复制) |
/app/chat |
App shell | 28 | 20.6 s | 下载总量 · 10.7 MB |
/app/trade/perps/BTC |
交易页(perps) | 26 | 28.3 s | TradingView 加载、CLS |
/app/strategy-studio |
策略工作室 | 29 | 29.4 s | 主线程 46s · 下载 11 MB |
/app/workflow |
workflow 画布 | 25 | 44.9 s | 最差页,重定向 7.8 s |
详见 performance-analysis.html §🚨 Lighthouse 实测。关键回校:
/zh/,单次重定向浪费 1.3–4.3s(/about 最严重,4.3s)。这是最廉价的改进,必须作为 P0 第一件事。api.minara.ai/quick-search/all-assets 单次返回 2.5 MB JSON(/app/chat 冷载)。minara.ai/zh/icon0.svg 文件体积 1.2 MB — 几乎肯定是 bug(把某个 logo/插画错用作 favicon)。Chrome MCP 实测补充(同一报告 §⚡):
/home 冷加载调了 api.minara.ai/users/pnls/all × 6、cross-chain/activities × 8、arbitrum/batch × 6。facebook.com/tr + privacy_sandbox/pixel/register 各 3 次 = 6 次无谓请求。fonts.googleapis.com/css2 一次加载请求 7 次:印证 7 套字体。_next/static/chunks/1476-*.js)。/cdn-cgi/rum):可直接从 CF 后台拉真实用户 P75 CWV。P0 据此增补了 P0.0(locale 重定向根治) 与 P0.9(favicon / quick-search / YouTube embed 单点爆炸修复) 两节。
背景:Lighthouse 对所有 5 个页面都报告 "Avoid multiple page redirects",省时 1.3–4.3s。最终 URL 全部带 /zh/ 前缀:
/about → /zh/why-minara/about(两跳)/home → /zh/home原因:next-intl 的 middleware 在无 locale 前缀时会做默认 locale 检测与跳转,当前默认指向 zh。Lighthouse 的 Accept-Language 头让它决定 zh;真实非中文用户同样被跳。
改点:
src/middleware.ts(或 src/i18n/navigation.ts):enlocalePrefix: 'as-needed'(默认 locale 不带前缀),让 /home、/about 这类路径本身就是有效的最终 URLlocalePrefix: 'never',进一步简化/about 的双跳:代码里应直接是 /why-minara/about 还是 /about?二选一,把另一条改成 301 永久重定向到规范 URL验收:Lighthouse 重跑:Redirect 诊断完全消失;LCP 至少下降 1.3s(对应 redirect savings)。
目标:立刻回收最大流量、解除最明显的主线程阻塞、建立指标闭环;不改变任何架构。 全阶段应可在 5–7 个工作日内由 1 名工程师完成。
next.config.mjs 接入 @next/bundle-analyzer:ANALYZE=true pnpm build 导出 .next/analyze/*.html,归档到 docs/perf-baseline/2026-04-YY/next build 记录:初始 JS (gzip)、每路由页面 JS (gzip)、首屏 HTML 大小web-vitals 上报到 GA/Sentry:首期只看 LCP/INP/CLS 中位数docs/perf-baseline/2026-04-YY/ 有完整数字;后续每任务完成后重跑并对比next.config.mjs 删除 eslint.ignoreDuringBuilds、typescript.ignoreBuildErrors 两行next build,若报错则在本 PR 内修复(预期零错误,CLAUDE.md 已声明"CI separately"表明历史确认通过)next build 绿;CI 里 next lint + tsc --noEmit 绿改点:src/app/[locale]/layout.tsx
<Script> 加 strategy="afterInteractive"(显式)<Script> 改 strategy="lazyOnload";三个 addPixelId 拆成必要的 1–2 个(咨询 Growth,3 个同时发应为历史遗留)markdown-it-fix 移除 beforeInteractive:直接在使用 markdown-it 的那个模块顶部 if (typeof window !== 'undefined' && !window.isSpace) { window.isSpace = ... };或用 Next 原生 instrumentation-client.ts / next/dynamic 方式;根除 head 阻塞<Script src="/charting-library/..." /> 从 layout 移除,改在 src/router/session/trade/ 与 src/router/session/strategy/ 的布局里按需注入(用 useEffect 动态 append 或 next/script)next/script + strategy="lazyOnload")canvas-datagrid npm 依赖,通过 next/dynamic + ssr:false 导入;同时避免未固定版本的供应链风险<Script> 补全 strategy改点:src/app/[locale]/layout.tsx L152–166 + src/app/[locale]/globals.css
zh / zh-TW 时注入(用 server component 判断 locale 后条件渲染 <link>)next/font/google(自动 preload、避免 CLS;已用 Roboto 同模式)display=swap(已在 query 中),考虑本地自托管子集<link href="fonts.googleapis.com"> 外部 <link> 清零,全部走 next/font;Lighthouse "Ensure text remains visible during webfont load" 通过public/images/guide/minara.gif 27 MB → minara.mp4 + minara.webm (~1.5 MB);引用点用 <video autoPlay muted loop playsInline>;保留 posterffmpeg -i minara.gif -c:v libx264 -movflags +faststart -pix_fmt yuv420p -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" minara.mp4ffmpeg -i minara.gif -c:v libvpx-vp9 -crf 30 -b:v 0 minara.webmcooking/1-4.gif 同样转 MP4/WebMshare/dark/background.png 14 MB、share/light/background.png 7.1 MB → 检查真实展示尺寸,转 WebP 并降分辨率(至 2×DPR 所需像素即可)copilot/hero-bg.png 13 MB → 同上og/image-0/1/2.png → JPEG 85q 或 WebP,目标每张 < 500 KB(Open Graph 规范 1200×630)campaign/korea-trading-competition/hero.png 4.9 MB → WebPscripts/check-public-size.sh,单文件 > 500 KB 时在 CI fail(GIF/MP4 白名单除外)du -sh public/ < 95 MB;Lighthouse 首页 "Properly size images" + "Serve images in next-gen formats" 得分提升pnpm why prismjs / pnpm why highlight.js,确认哪些文件真正引用prismjs 无直接引用(审计结果显示仅 5 个文件用高亮),从 package.json 移除jotai / jotai-immer:全仓 grep -rn "jotai",若使用寥寥,将相关代码迁到 zustand 后移除canvas-datagrid、dom-to-image、@zumer/snapdom、cropperjs、@xyflow/react 若仅在低频入口使用,不删但标注,留到 P1 改 dynamic importpnpm install 后 node_modules 缩小;next build 初始 JS 下降 ≥ 200 KB改点:src/store/discover/index.ts
useRequest 补 cacheKey(如 discover:fear-greed:{locale})manual:true + useEffect 监听 document.visibilityState === 'hidden' 暂停轮询src/trade/shared/trade.service.ts 4 个轮询:确认最小必要频率;登出 / unmount 必须 clearInterval背景:Chrome 实测发现未登录的 /home 上 pnls/all 被调 6 次、cross-chain/activities 8 次。
原因是 src/app/[locale]/layout.tsx 里的 CustodialWalletStoreProvider 与 UserAuthStoreProvider 对所有路由都生效,
且这些 Store 的 useService/useEffect 在无登录态时也会"先查一下"。
改点:
if (!hasCredential) return;/home、/about、/use-cases、/how-to-guides、/faq、/pricing)使用路由分组 src/app/[locale]/(marketing)/layout.tsx,其中 不挂 认证/钱包 Provider。这是 P1.1 的前置工程,独立可落地。ahooks.useRequest 统一默认 cacheKey(按 URL + params hash)以实现自动去重。可以在 src/request/ 封装层加一层中间件;或为每条调用显式加 cacheKey。验收:
/home,devtools Network 面板中:pnls/all / cross-chain/activities / arbitrum/batch 请求数 = 0/use-cases、/about 同上/app/chat:同一接口重复请求数 ≤ 1(允许 React StrictMode 下的双调,其余必须去重)风险:如果某些 UI 组件依赖 store 的 "总是有数据" 假设,可能显示空态。逐一验收 UI 状态。
Lighthouse 抓到的三个可独立修复的点,不涉及架构变更:
minara.ai/zh/icon0.svg 返回 1,210 KB。排查 src/app/icon0.svg / src/app/apple-icon.png 是否引错了文件。常规 SVG favicon ≤ 5 KB,可能是某张 Minara logo 插画被错误用作 icon。/quick-search/all-assets 2.5 MB JSON:确认该接口是否真的要返回所有资产。改为:Content-Encoding)/about 的 YouTube 嵌入换 lite-youtube-embed:当前 YouTube iframe 加载 451 KB player_embed_es6.vflset/zh_CN/base.js。换成 lite-youtube(~3 KB),click 再加载真正 iframe。LCP/TBT 双降。验收:
/quick-search/all-assets 首响应 < 200 KB(或首屏数据 < 200KB)/about 首屏不再有 youtube.com/.../player_embed 加载Lighthouse 显示 CoinGecko widget(213 KB、28–30 ms 主线程)在所有 5 个页面加载;同时 Unused CSS 约 30 KB。
<Script> 从 src/app/[locale]/layout.tsx L148 挪到真正使用它的组件(如 src/components/token-pop-up/ 或 src/router/session/token/),改 next/dynamic 或组件挂载时 next/script strategy="lazyOnload" 注入globals.css 或 Tailwind 未扫描到的文件中遗留):tailwindcss purge scope 正确?highlight.js/styles/github.css 全局引入是否有必要?验收:/home、/about、/use-cases、/how-to-guides/[slug] 均不再加载 gecko-coin-price-chart-widget.js。
目标:真正把架构从"全部 client SSR on-demand"拉回"静态优先 + 按需 client";这个阶段改动大但收益最持久。
范围:/、/home、/about、/use-case、/faq、/pricing(评估)
'use client',放 server componentcapability-animations.tsx 等)page.tsx 导出 export const dynamic = 'force-static' 或 revalidate = 3600<Suspense fallback={<Skeleton />}> 包裹任何含数据获取的 client islandlayout.tsx Provider 下沉:把 UserAuthStoreProvider / CustodialWalletStoreProvider / ShareChatImageStore 挪到 新建的 src/app/[locale]/(session)/layout.tsx 组分组,营销页不再承担认证态初始化成本next build 输出 ● (Static) 标记src/router/how-to-guides/<slug>/index.tsx 内容抽取为 JSON:src/content/how-to-guides/<slug>.json(结构:hero、relatedGrid、faq、…)src/router/how-to-guides/template.tsx(仅 1 份 ~200 行)src/app/[locale]/how-to-guides/[slug]/page.tsx:import { loadGuideContent, listGuideSlugs } from '@/content/how-to-guides';
export async function generateStaticParams() {
return listGuideSlugs().map(slug => ({ slug }));
}
export default async function Page({ params }) {
const data = await loadGuideContent(params.slug);
return <GuideTemplate data={data} />;
}
export const revalidate = 86400;
src/router/how-to-guides/<slug>/ 目录整体删除;保留 sitemap 生成脚本对内容目录的指向更新src/router/how-to-guides/ 代码量从 ~60K LOC 降到 < 500 LOCnext build 输出 ● (Static)next-sitemap 仍产出全部 30 条 URLreact-syntax-highlighter 或(推荐)迁到 Shiki (SSR 渲染出纯 HTML,客户端 0 JS)highlight.js 与 prismjs(含 layout.tsx 中 import 'highlight.js/styles/github.css')next/dynamic + ssr:falsepnpm why prismjs highlight.js 输出 "no such package";消息流 markdown 无视觉差异回归测试通过批量把以下依赖改成 next/dynamic({ ssr: false, loading }):
| 依赖 | 建议承载点 |
|---|---|
| Monaco editor | ✅ 已做 |
@xyflow/react(workflow 画布) |
仅 /app/workflow 动态导入 |
echarts + echarts-for-react |
仅 strategy studio 图表组件 |
canvas-datagrid |
spreadsheet section 内部 dynamic |
cropperjs + react-cropper |
头像上传 modal 打开时 dynamic |
dom-to-image / @zumer/snapdom |
share 截图功能触发时 dynamic |
canvas-confetti |
胜利动画触发时 dynamic |
@lexical/*(编辑器) |
聊天输入框 focus 后再加载(可选) |
| TradingView charting library | P0 已下沉到交易/策略路由;这里确认最终只在 trade/strategy bundle |
import * as X 全部改为命名 import(ast-grep 可批量)components/primitive/index.ts、features/chat-base/index.ts、utils/date|number/index.ts@/components/primitive/button、@/features/chat-base/core/use-chat 等no-restricted-imports 禁止从 barrel 导入(给一个 codemod 脚本帮忙替换)from 'lodash' → from 'lodash-es/capitalize' 之类(或在 optimizePackageImports 命中前提下保留,但统一风格)next build 首页/guide 页 JS chunk 再降 10–20%next.config.mjs 加 experimental.ppr = true(需 Next canary/15.4;15.3 stable 尚有限制——先 POC 评估)babel-plugin-react-compiler,在 babel 配置开启;先在 src/features/chat-base/ 小范围试点reactStrictMode 设回 true(默认 off 是反模式;会暴露一批并发模式相关 bug,值得在这一阶段集中修复)src/router/session/discover/news-page/index.tsx eventsList 的 .map() 改为 virtua 或 react-window(项目已有)motion.* 入场动画仅对视口内的 item 触发(IntersectionObserver + useReducedMotion)src/store/trade-store/index.ts:41 个 selector 保留,但消费端禁止 useXxx() 整取——通过 ESLint 规则或人工 code review 确保只拿需要字段src/store/chat/index.tsx L286 的 s => s 改为导出若干窄 selectornext.config.mjs images.remotePatterns 改白名单(仅允许 space.minara.ai、CoinGecko CDN、Twitter/Google OAuth、Solana/Ethereum 等必要域名)minara-logo-with-text-*.svg 用 SVGO 精简 + 删除重复副本;blockchain.svg 959 KB 若为装饰,降级 PNG/WebP;chain/base.svg 同理<img> → next/image;如果 WrappedImage 组件内部是 <img>,也包一层 next/imagegetTranslations → 一次create-markdown-render.tsx 热路径的 console.warnuseMemo 为一条(组合函数)style={{}} 提取为常量size-limit 或 bundlesize:首页 JS 超过阈值直接 fail(建议 200 KB gzip)@next/bundle-analyzer 结果自动贴到 PR 评论docs/perf-reports/YYYY-MM/| 周 | 阶段 | 主任务 | 预期指标提升 |
|---|---|---|---|
| W1 day 1 | P0.0 + P0.9 + P0.2 | locale 重定向、favicon/quick-search/YouTube 修 bug、CI 类型保护 | LCP < 17 s(-4 s)· score +5 |
| W1 | P0.1 + P0.3–P0.8 | 基线、脚本策略、字体、大图、依赖瘦身、轮询、Provider 下沉 | LCP < 10 s · score ≥ 50 |
| W2 | P1.1 + P1.2 | 营销页 RSC/SSG + how-to-guides 重构 | LCP < 6 s · TTI < 10 s · score ≥ 70 |
| W3 | P1.3 + P1.4 | 高亮合一 + 重型依赖 dynamic | Unused JS < 600 KB · TBT < 600 ms |
| W4 | P1.5 | barrel / import * 重构 | 首屏 JS -40% 对比 W1 |
| W5 | P1.6 + P1.7 + P1.8 | PPR/Compiler + 虚拟化 + store 审计 | INP < 200 ms · score ≥ 85 |
| W6 | P2 | 资源收紧 + 守护 | score ≥ 90 · CI 加 size-limit 告警 |
两人并行可压到 3 周。第一天的 P0.0 + P0.9 合计工作量 < 1 天,收益约 LCP -4 s,强烈建议作为立项 kickoff 的 quick win。
| 风险 | 预防 | 回滚策略 |
|---|---|---|
移除 ignoreBuildErrors 后 CI 挂一大片 |
P0 第一件事,预留半天批量修 | 若无法一次修完,先 revert 保功能,单独 PR 分批修 |
RSC 化漏加 'use client' 引发 "useState 不能在 server" |
单 PR 控制单页面;先预览环境验证 | revert 单页面;其他页面不受影响 |
| SSG how-to-guides 导致 sitemap 生成链断 | 迁移时双跑 next-sitemap + 原脚本 |
保留旧 router/how-to-guides 目录直至 sitemap 验证通过再删 |
| TradingView 下沉到 trade 路由后某些非 trade 页仍在用 | 代码检索 TradingView/charting-library/trading_view 引用 |
Hotfix:恢复 layout 注入,但仅限白名单路由 |
| PPR 在 15.3 稳定性不够 | 先在 canary 15.4 POC | 移除 experimental flag,页面仍以 SSG 运作 |
| Zustand selector 收窄引入 stale closure | 以路由为单位改动 + 人工 code review | 回到整 store 读取(损失性能但功能正确) |
每个任务 PR 必须:
test/<branch>/test-cases.md 的验收清单next build + next lint + tsc阶段完成:
docs/perf-reports/2026-05-XX/ 有对比报告,LCP 改进 ≥ 1 s| 问题 | 文件 |
|---|---|
| 脚本/字体堆叠 | src/app/[locale]/layout.tsx |
| 构建配置 | next.config.mjs |
| 模板复制 | src/router/how-to-guides/<slug>/index.tsx |
| 轮询源头 | src/store/discover/index.ts, src/trade/shared/trade.service.ts, src/services/use-chat-list.ts |
| 状态管理 | src/store/trade-store/index.ts, src/store/chat/index.tsx |
| 流式渲染(保持) | src/features/chat-base/core/xhr-stream-utils.ts, src/features/chat-base/message-view/section/create-streaming-markdown.tsx |
| 巨型资源 | public/images/guide/minara.gif, public/images/share/*, public/images/copilot/hero-bg.png, public/images/og/* |
| Barrel 文件 | src/components/primitive/index.ts, src/features/chat-base/index.ts, src/utils/date/index.ts, src/utils/number/index.ts |