perf(frontend): defer mermaid and split editor dependencies
This commit is contained in:
@@ -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)。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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))
|
||||
@@ -1,4 +1,10 @@
|
||||
import mermaid from 'mermaid'
|
||||
let mermaidPromise: Promise<typeof import('mermaid')['default']> | 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<unknown> = Promise.resolve()
|
||||
function serialized<T>(work: () => Promise<T>): Promise<T> {
|
||||
@@ -66,9 +74,9 @@ async function renderMermaidNow(
|
||||
options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {}
|
||||
): Promise<MermaidRenderResult> {
|
||||
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 : '未知错误'
|
||||
|
||||
@@ -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<string, string>()
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user