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,