diff --git a/docs/README.md b/docs/README.md index 39e8853..fe8912b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,8 @@ ## development:开发说明 +- [前端构建分块优化开发说明](development/前端构建分块优化开发说明.md) + - [工作区后台索引与保存开发说明](development/工作区后台索引与保存开发说明.md) - [Mermaid 预览与缩放开发说明](development/Mermaid预览与缩放开发说明.md) - [扩展安装持久化与社区包开发说明](development/扩展安装持久化与社区包开发说明.md) diff --git a/docs/development/前端构建分块优化开发说明.md b/docs/development/前端构建分块优化开发说明.md new file mode 100644 index 0000000..139ac7e --- /dev/null +++ b/docs/development/前端构建分块优化开发说明.md @@ -0,0 +1,43 @@ +# 前端构建分块优化开发说明 + +> 更新日期:2026-09-06。基于 main 的 64af068 进行构建对比;以下结果为本地生产构建,不是网络耗时或性能评分。 + +## 当前方案 + +- Mermaid 由静态导入改成第一次渲染或校验图表时动态导入。复用加载 Promise,加载失败清除缓存以允许重试,主题初始化与渲染继续串行执行。 +- CodeMirror 基础模块、ProseMirror 和 Milkdown 分组缓存。仅显式选中的模块进入手动分组,不吸收所有传递依赖。 +- CodeMirror 语言解析器、全部 Shiki 语法和 Mermaid 图型继续按需加载。不能将所有语言打入 editor vendor,否则会反而增加编辑器的首次加载量。 +- KaTeX 按实际安装版本分组,避免合并项目版本和依赖内版本。不强制升级第三方依赖的数学解析器。 +- 启用 Vite manifest,并提供 build:report 脚本用于持续比较入口静态依赖。 + +## 构建观察 + +数值按十进制 kB 计算。静态闭包包括入口 JS 与递归 imports,去重后求和;不包含 CSS、字体、运行时动态导入或浏览器缓存。gzip 为每个 JS 文件压缩后求和,不代表服务器必然启用该压缩。 + +| 检查项 | 优化前 | 优化后 | +| --- | --- | --- | +| 聊天页静态 JS 闭包 | 约 1563.5 kB | 约 894 kB | +| 聊天页闭包 gzip | 约 451.6 kB | 约 292 kB | +| VisualMarkdownEditor 单包 | 约 1145.8 kB | 约 20 kB,核心依赖转入独立块 | +| 首屏静态 JS 闭包 | 约 409.7 kB | 约 409 kB,基本不变 | + +组件单包缩小不等于整个编辑器只需要 20 kB。编辑器仍需要加载基础框架、核心依赖和实际用到的语法。此次主要减少普通 Markdown 页对 Mermaid 的提前加载,并改善模块缓存边界;未测量真实启动耗时,不能声称首屏提速比例。 + +## 验证方法 + +在 frontend 目录执行: + +```powershell +pnpm build +pnpm build:report +pnpm test +pnpm exec vite preview --host 127.0.0.1 --port 4173 +``` + +build:report 读取 dist/.vite/manifest.json,输出入口闭包大小与最大的 15 个 JS 块。对比时保存相同构建环境的两份输出;更新依赖后需重新测量。 + +本轮前端 345 项测试通过,类型检查及生产构建通过。已在生产预览打开工作区和真实 Mermaid 示例笔记,确认编辑器与 SVG 加载,未发现控制台 error。Shiki 全语言覆盖仍由既有语言测试检查。 + +## 保留的大块与边界 + +C++、Emacs Lisp 等语法、Oniguruma WASM、部分 Mermaid 图型及 Mermaid 核心仍可能超过 500 kB。这些资源保留按需加载,不裁减语言支持,也不提高警告阈值来隐藏问题。首次打开复杂图表或对应语言仍有加载成本;后续可基于真实请求和设备数据评估 Worker、资源预热及库版本升级。 diff --git a/frontend/README.md b/frontend/README.md index 8b1f23b..4a84848 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -86,3 +86,7 @@ pnpm build Mermaid 大图打开时适配窗口,支持平滑滚轮缩放和鼠标位置补偿;行内中键启用滚轮控制,移动鼠标退出。标签段落样式与正文隔离,避免 foreignObject 内文字裁切。 开发和验证方法见 [后台索引与保存](../docs/development/工作区后台索引与保存开发说明.md)、[Mermaid 预览与缩放](../docs/development/Mermaid预览与缩放开发说明.md)。 + +## 构建体积检查 + +执行 `pnpm build` 后运行 `pnpm build:report`,查看入口静态 JS 依赖与大块清单。分组策略、统计口径及保留的大资源见 [前端构建分块优化开发说明](../docs/development/前端构建分块优化开发说明.md)。 diff --git a/frontend/package.json b/frontend/package.json index 83b878c..97a32fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ "build": "vue-tsc -b && vite build", "test": "vitest run", "preview": "vite preview", - "type-check": "vue-tsc --noEmit" + "type-check": "vue-tsc --noEmit", + "build:report": "node scripts/build-size.mjs" }, "dependencies": { "@codemirror/commands": "6.11.0", diff --git a/frontend/scripts/build-size.mjs b/frontend/scripts/build-size.mjs new file mode 100644 index 0000000..c7ec4e0 --- /dev/null +++ b/frontend/scripts/build-size.mjs @@ -0,0 +1,19 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { gzipSync } from 'node:zlib' +const root = fileURLToPath(new URL('../dist/', import.meta.url)) +const manifest = JSON.parse(readFileSync(resolve(root, '.vite/manifest.json'), 'utf8')) +const files = new Map(Object.values(manifest).filter(x => x.file.endsWith('.js')).map(x => [x.file, x])) +const sizes = file => { const bytes = readFileSync(resolve(root, file)); return { bytes: bytes.length, gzip: gzipSync(bytes).length } } +const closure = key => { + const seen = new Set() + function visit(k) { if (seen.has(k)) return; seen.add(k); for (const i of manifest[k]?.imports ?? []) visit(i) } + visit(key) + return [...new Set([...seen].map(k => manifest[k]?.file).filter(f => f?.endsWith('.js')))] +} +const entries = Object.entries(manifest).filter(([key, value]) => value.isEntry || /(?:WorkspaceView|VisualMarkdownEditor|VaultEntry|ChatView)\.vue$/.test(key)).map(([key]) => { + const files = closure(key) + return { entry: key, files, bytes: files.reduce((n, f) => n + sizes(f).bytes, 0), gzip: files.reduce((n, f) => n + sizes(f).gzip, 0) } +}) +console.log(JSON.stringify({ entries, largest: [...files.keys()].map(file => ({ file, ...sizes(file) })).sort((a,b) => b.bytes-a.bytes).slice(0,15), chunks: files.size }, null, 2)) diff --git a/frontend/src/services/mermaidService.ts b/frontend/src/services/mermaidService.ts index 57eb899..81dcc32 100644 --- a/frontend/src/services/mermaidService.ts +++ b/frontend/src/services/mermaidService.ts @@ -1,4 +1,10 @@ -import mermaid from 'mermaid' +let mermaidPromise: Promise | undefined +function loadMermaid() { + return mermaidPromise ??= import('mermaid').then(module => module.default).catch(error => { + mermaidPromise = undefined + throw error + }) +} import { computed } from 'vue' import { useThemeStore } from '@/stores/theme' @@ -23,7 +29,8 @@ export function mermaidThemeVariables(dark: boolean) { } } -function ensureInitialized(theme: 'light' | 'dark') { +async function ensureInitialized(theme: 'light' | 'dark') { + const mermaid = await loadMermaid() mermaid.initialize({ startOnLoad: false, theme: 'base', @@ -34,6 +41,7 @@ function ensureInitialized(theme: 'light' | 'dark') { sequence: { useMaxWidth: true }, gantt: { useMaxWidth: true }, }) + return mermaid } let queue: Promise = Promise.resolve() function serialized(work: () => Promise): Promise { @@ -66,9 +74,9 @@ async function renderMermaidNow( options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {} ): Promise { const theme = options.theme ?? 'light' - ensureInitialized(theme) const id = `mermaid-${Date.now()}-${++renderCounter}` try { + const mermaid = await ensureInitialized(theme) const result = await mermaid.render(id, source) const parser = new DOMParser() const doc = parser.parseFromString(result.svg, 'image/svg+xml') @@ -131,7 +139,7 @@ export function useMermaidTheme() { export async function validateMermaid(source: string): Promise<{ valid: boolean; error?: MermaidParseError }> { try { - await serialized(async () => { ensureInitialized('light'); await mermaid.parse(source) }) + await serialized(async () => { const mermaid = await ensureInitialized('light'); await mermaid.parse(source) }) return { valid: true } } catch (error) { const message = error instanceof Error ? error.message : '未知错误' diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ee63575..0c13b75 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,6 +1,18 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'node:path' +import { readFileSync } from 'node:fs' + +const mathChunks = new Map() +function mathChunk(module: string) { + const marker = '/node_modules/katex/' + const root = module.slice(0, module.lastIndexOf(marker) + marker.length) + if (!mathChunks.has(root)) { + const { version } = JSON.parse(readFileSync(`${root}package.json`, 'utf8')) + mathChunks.set(root, `math-katex-${version.replace(/[^0-9a-z]/gi, '-')}`) + } + return mathChunks.get(root)! +} export default defineConfig({ plugins: [vue()], @@ -9,6 +21,23 @@ export default defineConfig({ '@': path.resolve(__dirname, 'src'), }, }, + build: { + manifest: true, + rollupOptions: { + output: { + // Keep lazy languages/diagrams independent; do not collect every vendor into one bundle. + onlyExplicitManualChunks: true, + manualChunks(id) { + const module = id.replace(/\\/g, '/') + if (!module.includes('/node_modules/')) return + if (/\/@codemirror\/(?:state|view|language|commands|search|autocomplete|lint)\//.test(module) || /\/@lezer\/(?:common|highlight|lr)\//.test(module) || module.includes('/node_modules/codemirror/')) return 'editor-codemirror' + if (/\/node_modules\/prosemirror-[^/]+\//.test(module)) return 'editor-prosemirror' + if (module.includes('/node_modules/@milkdown/')) return 'editor-milkdown' + if (module.includes('/node_modules/katex/')) return mathChunk(module) + }, + }, + }, + }, server: { host: '127.0.0.1', port: 5173,