尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

DeerFlow 前端性能实战:Next.js App Router 静态渲染恢复与路由级 Bundle 预算

DeerFlow 前端性能实战:Next.js App Router 静态渲染恢复与路由级 Bundle 预算 DeerFlow 前端性能实战Next.js App Router 静态渲染恢复与路由级 Bundle 预算【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow本文基于 DeerFlow 仓库中的实施计划 前端 Bundle 与静态渲染计划系统讲解如何通过根布局静态化、路由级 i18n 词典隔离、交互面板懒加载、CodeMirror 语言按需加载与 Shiki 单树高亮五步改造恢复 DeerFlow 前端公开路由的静态渲染能力并用可执行的字节级预算脚本把优化成果固化下来。读完本文你将掌握 Next.js App Router 中“为什么 cookies 会拖垮静态生成”“RSC 序列化边界如何影响国际化方案”以及“如何用自动化测量脚本为每条路由锁死 JS/CSS 体积上限”的完整方法论并能在本仓库中逐条找到对应的源码与测试佐证。一、计划总览目标、架构与技术约束计划文档的开篇定义了这次性能改造的边界这也是后文所有改造的验收基准Goal目标恢复公开路由的静态渲染并且把 locale 词典、设置页、编辑器、artifact 面板和 Shiki 从“不使用它们的路由”中剥离出去Architecture架构根布局变为与 locale 无关的纯静态布局公开的路由感知locale-aware路由各自解析自己的一份路由独占词典交互式 auth/workspace provider 同时持有两份词典因为 formatter 函数无法跨越 RSC 序列化边界且语言切换必须保持“零延迟”功能型宿主组件在用户交互边界而非路由边界才加载代码语法高亮对每个代码块只生成一棵 HTML 树。Tech StackNext.js App Router、React 19、TypeScript、Rstest、Streamdown、Shiki、CodeMirror。Global Constraints全局约束保持可见行为与深链deep link不变所有动态 import 必须有稳定的 loading/error 状态不得把认证检查移入公开路由所有影响用户可感知架构的改动须同步更新对应的AGENTS.md。从仓库现状看这五项约束在代码中都有对应落地物根布局已静态化、i18n 词典已按路由拆分、设置页与面板已next/dynamic化、编辑器/高亮已按需加载并存在一个专门的预算校验脚本后文第五节详述。二、任务一根布局静态化与路由样式收敛计划要求把根布局缩减为“全局 reset 主题 metadata html lang{DEFAULT_LOCALE}”并新增一个源码边界测试断言根布局不导入cookies、detectLocaleServer、KaTeX CSS、Streamdown CSS 或 workspace providers。当前仓库的 根布局 正是该形态export default function RootLayout({ children, }: Readonly{ children: React.ReactNode }) { return ( html lang{DEFAULT_LOCALE} suppressContentEditableWarning suppressHydrationWarning body ThemeProvider attributeclass enableSystem disableTransitionOnChange {children} /ThemeProvider /body /html ); }关键点在于布局没有任何对next/headers的依赖lang属性直接使用DEFAULT_LOCALE常量而非运行时检测结果。这意味着/、/en/docs等公开路由可以在构建期完成 HTML 产出响应不再携带仅因根布局读 cookies 而产生的private, no-store头。而“需要动态能力”的部分被下放到子布局。例如 workspace 布局 显式声明export const dynamic force-dynamic在内部调用detectLocaleServer()解析 locale并在此处挂载I18nProvider与富文本样式katex/dist/katex.min.css、streamdown/styles.cssauth 布局/layout.tsx) 同理。这样“样式/词典只属于真正渲染它们的路由”成为一条可测试的边界。边界由 layout-boundaries.test.ts 以源码扫描方式固化it(keeps request locale and rich-content styles out of the root layout, () { const rootLayout source(src/app/layout.tsx); expect(rootLayout).not.toContain(detectLocaleServer); expect(rootLayout).not.toContain(I18nProvider); expect(rootLayout).not.toContain(katex/dist/katex.min.css); expect(rootLayout).not.toContain(streamdown/styles.css); expect(rootLayout).toContain(DEFAULT_LOCALE); });测试还断言 workspace 布局拥有streamdown/styles.css、docs/blog 布局拥有katex/dist/katex.min.css且(auth)与 workspace 布局只向子树传递可序列化的 locale 状态。这类“源码边界测试”的价值在于它不需要启动浏览器就能防止未来有人把cookies()或重型 CSS 悄悄塞回根布局——这是静态渲染能力最常见的回退路径。三、任务二按路由边界拆分 locale 词典计划的核心判断是公开静态导入两份完整词典会让所有路由包括登录页都为不存在的语言切换能力买单且 formatter 函数无法通过 RSC 序列化边界。因此拆分为“服务端按路由加载单份词典 交互边界持有双份词典”的结构。服务端穷举 loader 映射。translations.ts 用一个穷举的RecordLocale, () PromiseTranslations映射替代静态导入const translationLoaders: RecordLocale, () PromiseTranslations { en-US: async () (await import(./locales/en-US)).enUS, zh-CN: async () (await import(./locales/zh-CN)).zhCN, }; export async function loadTranslations(locale: Locale): PromiseTranslations { return await translationLoaders[locale](); }RecordLocale, ...的穷举约束保证了新增 locale 时若忘记登记 loader 会在类型检查阶段失败。server.ts 提供三个服务端入口detectLocaleServer()从localecookie 读取并做decodeURIComponent容错后交给normalizeLocale归一化setLocale()写回 cookiemaxAge一年、sameSite: laxgetI18n()支持显式 locale 覆盖。客户端交互式 provider 持有双份词典。client-translations.ts 中的注释直接点明了设计动机“Translation dictionaries contain formatter functions, so they must be selected inside a Client Component rather than serialized through an RSC boundary.” 即词典对象里含有函数日期、复数等 formatter不能作为 props 从服务端布局传入客户端组件。因此 context.tsx 的I18nProvider只接收可序列化的initialLocale内部以useState持有当前词典setLocale时同步切换locale与t实现计划要求的“语言切换零延迟、无 loading 闪烁”并通过useEffect将document.documentElement.lang与状态保持同步。该模块由 context.dom.test.tsxDOM 环境验证立即切换与lang属性同步与 translations.test.ts验证loadTranslations(en-US)/loadTranslations(zh-CN)与不支持的 locale 被解析器拒绝共同覆盖。公开路由如首页 header则只接收显式的默认 locale不继承交互式双词典 provider——这正是“公开路由不继承两 locale provider”这一断言的来源。四、任务三设置页与 workspace 面板的交互边界懒加载计划要求保持唯一的SettingsDialogHost与键盘/深链打开行为不变但设置对话框、九个设置页以及 artifact 详情、浏览器实时视图等“关闭状态下”的面板其模块求值必须推迟到触发器打开之后。当前仓库中设置宿主位于 settings-dialog-host.tsx计划中写作components/settings/实现时归入了components/workspace/settings/lazy-panels.test.ts 用源码断言把三条边界钉死it(does not import the settings dialog until its store is open, () { const host read(src/components/workspace/settings/settings-dialog-host.tsx); expect(host).toContain(dynamic(); expect(host).toContain(if (!open)); expect(host).not.toContain(import { SettingsDialog } from ./settings-dialog); }); it(loads each settings page from its active section, () { const dialog read(src/components/workspace/settings/settings-dialog.tsx); expect(dialog.match(/dynamic\(/g)).toHaveLength(10); // 禁止从 settings/ 目录静态导入任何 XxxSettingsPage }); it(keeps right-panel implementations behind dynamic imports, () { const chatBox read(src/components/workspace/chats/chat-box.tsx); expect(chatBox).toContain(import dynamic from next/dynamic); // 禁止静态导入 ArtifactFileDetail/ArtifactFileList/BrowserViewPanel/SidecarPanel expect(chatBox.match(/dynamic\(/g)?.length).toBeGreaterThanOrEqual(4); });对应计划中的“九个设置页 对话框本体”共 10 个dynamic(调用点以及 chat-box 中至少 4 个右侧面板的动态边界。ssr: false仅用于纯浏览器模块其余动态边界保留 SSR 以保证首次打开不出现布局抖动每个动态边界都配套了可见的 loading shell满足全局约束第 2 条。该改造的验证目标是chats 路由的首次资产清单中不再包含设置/编辑器/浏览器面板的 chunk——这一目标由第五节的资产测量脚本闭环验证。五、任务四CodeMirror 语言按需加载与 Shiki 单树高亮5.1 编辑器语言适配器穷举 动态 import计划提出新增“语言到 import 的穷举加载器”仓库中的实现为 code-editor-extensions.ts计划文件名code-editor-languages.ts落地时并入 extensions 文件。它先做一次语言归一化把别名折叠到 7 个 canonical 语言export function normalizeCodeEditorLanguage( language: string | null | undefined, ): CodeEditorLanguage { switch (language?.toLowerCase()) { case css: case scss: case sass: case less: return css; case html: case xml: return html; case javascript: case typescript: case jsx: case tsx: return javascript; case json: case jsonc: case json5: return json; case markdown: case mdx: return markdown; case python: case py: return python; default: return text; } }随后loadLanguageExtension对每个 canonical 语言动态import对应的codemirror/lang-*包未知语言落到text空扩展数组纯文本编辑loadTheme按主题动态加载uiw/codemirror-theme-monokai暗色或uiw/codemirror-theme-basic亮色两者均把背景设为透明以贴合容器样式。Promise.all并行加载语言与主题。宿主组件 code-editor.tsx 负责“稳定的加载状态”扩展未就绪或线程处于isLoading时渲染一个只读Textarea占位就绪后才挂载CodeMirrorCtrl/CmdS触发onSave行号/折叠等通过settings透传。由于整个扩展加载走动态 import落地页与 chats 路由的初始资产列表中不会出现任何 CodeMirror chunk。5.2 Shiki一个代码块只生成一棵树计划明确要求高亮惰性加载在 code block 边界之后只调用一次codeToHtml生成一份 HTML并用“Shiki CSS 变量或单份双主题 token 树”实现主题切换而非两次调用两棵树。仓库实现为 shiki-highlight.ts落地扩展名为.ts。核心调用codeToHtml(code, { lang: language, themes: { light: one-light, dark: one-dark-pro }, defaultColor: light, transformers: showLineNumbers ? [lineNumberTransformer] : [], });双主题one-light/one-dark-pro在单次codeToHtml中产出同一棵 DOM 树运行时通过 CSS 变量切换主题避免“暗/亮各高亮一次”的冗余。模块还内置了高亮缓存HIGHLIGHT_CACHE_LIMIT 64条、单条代码超过HIGHLIGHT_CACHE_MAX_CODE_LENGTH 100_000字符则绕过缓存直接渲染缓存 key 包含HIGHLIGHT_CONFIGURATION_VERSION、代码、语言与行号开关并以 LRU 方式淘汰——对长会话中反复渲染同一代码块的场景这是显著的成本削减点。shiki-highlight.ts 被 code-block.tsx 以动态 import 的方式引用useEffect中先显示原始pre兜底高亮结果到达后替换renderId引用防止异步竞态导致的陈旧状态回写useEffect(() { const currentRenderId renderId.current; setHtml(); void highlightCode(code, language, showLineNumbers) .then((highlighted) { if (currentRenderId renderId.current) setHtml(highlighted); }) .catch(() { // Shiki 加载失败或模型输出不支持的语言时原始代码兜底保持可见 }); return () { renderId.current 1; }; }, [code, language, showLineNumbers]);这条“原始代码兜底 稳定 loading 态”的实现正是全局约束第 2 条动态 import 必须有稳定 loading/error 状态在高亮路径上的具体体现。六、任务五性能门禁——路由资产测量与预算锁定计划最后一步是把“优化成果”变成“不可回退的契约”。仓库中对应的自动化门禁由 measure-route-assets.mjs 与 performance-budgets.json 组成package.json中的perf:check脚本即node scripts/measure-route-assets.mjs --check。测量流程与脚本源码逐段对应双模式构建/login用普通模式构建因为它是force-dynamic的认证路由其余路由用NEXT_PUBLIC_STATIC_WEBSITE_ONLYtrue构建验证静态产出能力启动next start于随机本地端口等待就绪30 秒超时对每条路由抓取 HTML用正则提取/_next/static/下的全部src/href资产extractAssetPaths再对每个文件stat累加真实字节数--check模式下将测量值与预算文件对比任何一项超阈值即整体失败并打印超出字节数evaluateBudgets。被测量的六条路由ROUTES覆盖了用户旅程的关键节点const ROUTES [ /, /login, /workspace/chats, /workspace/chats/${DEMO_THREAD_ID}, // 演示线程 7cfa5f8f-... /en/docs, /blog/posts, ];当前锁定的预算字节如下即计划中“one-time post-optimization calibration”的产物——每条上限都低于对应路由优化前的实测基线且之后不得上调路由CSS 上限JS 上限预算语义/login170,000850,000认证路由最轻的交互面/175,0001,050,000落地页不得继承 workspace 重量级 chunk/workspace/chats190,0001,750,000会话列表不含单线程重资产/workspace/chats/7cfa5f8f-...190,0004,100,000单线程页允许编辑器/流式渲染资产/en/docs270,0004,200,000nextra 文档含 KaTeX/blog/posts270,0004,200,000富文本博客含 KaTeX完整验收命令序列计划 Task 5cd frontend pnpm check pnpm test # eslint tsc rstest NEXT_PUBLIC_STATIC_WEBSITE_ONLYtrue pnpm build pnpm perf:check # 构建 启动 逐路由测字节 对比预算perf:check的测量结果会写入.next/performance-results.json并输出按路由汇总的JS x B | CSS x B | HTML x B摘要配合“检查五个路由的资产清单、确认每个重 chunk 有且只有一个归属路由/交互”的人工核对步骤形成“自动化断言 清单归属”双重门禁。七、全局约束与回退验证方法计划对每个任务都内置了“回退证明”步骤例如还原根布局的 locale 检测 → 证明测试变红 → 恢复 → 重跑这是一种“证明测试真的在守护”的反向验证手段确保边界测试不是永绿的空壳。四条全局约束的最终落地位置可以对照检查全局约束落地证据保持可见行为与深链设置页键盘/深链打开行为不变SettingsDialogHost唯一入口深链路由/workspace/chats/{threadId}仍在测量清单中动态 import 有稳定 loading/error 态CodeEditor 的Textarea占位、code-block 的原始pre兜底、面板的可见 loading shell认证检查不进公开路由auth/workspace 布局各自保留getServerSideUser分支与force-dynamic公开布局无认证调用架构变更同步 AGENTS.md仓库在 frontend/AGENTS.md 等位置维护架构说明约束要求同类改动同步更新八、如何在仓库中复核本文内容计划原文含五个任务的完整步骤与提交信息约定如perf(frontend): restore static public layout boundaries2026-07-31-frontend-performance-bundle-static.md静态根布局与路由布局layout.tsx、(auth)/layout.tsx/layout.tsx)、workspace/layout.tsxi18n 分层server.ts、translations.ts、context.tsx、client-translations.ts懒加载边界与测试settings-dialog-host.tsx、lazy-panels.test.ts、layout-boundaries.test.ts编辑器与高亮code-editor.tsx、code-editor-extensions.ts、code-block.tsx、shiki-highlight.ts性能门禁measure-route-assets.mjs、performance-budgets.json这套方案的可借鉴之处在于它没有停留在“压缩打包”层面而是把性能约束转化为三条可长期执行的工程契约——源码边界测试防止重型依赖回流入共享布局、路由资产预算防止体积回退、以及每个动态边界的稳定加载态防止懒加载损伤可用性。三者共同保证公开路由的静态渲染能力不因后续迭代而被悄悄侵蚀。【免费下载链接】deer-flowAn open-source long-horizon SuperAgent harness that researches, codes, and creates. With the help of sandboxes, memories, tools, skill, subagents and message gateway, it handles different levels of tasks that could take minutes to hours.项目地址: https://gitcode.com/GitHub_Trending/de/deer-flow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表