perf(frontend): defer mermaid and split editor dependencies
This commit is contained in:
@@ -86,3 +86,7 @@ pnpm build
|
|||||||
Mermaid 大图打开时适配窗口,支持平滑滚轮缩放和鼠标位置补偿;行内中键启用滚轮控制,移动鼠标退出。标签段落样式与正文隔离,避免 foreignObject 内文字裁切。
|
Mermaid 大图打开时适配窗口,支持平滑滚轮缩放和鼠标位置补偿;行内中键启用滚轮控制,移动鼠标退出。标签段落样式与正文隔离,避免 foreignObject 内文字裁切。
|
||||||
|
|
||||||
开发和验证方法见 [后台索引与保存](../docs/development/工作区后台索引与保存开发说明.md)、[Mermaid 预览与缩放](../docs/development/Mermaid预览与缩放开发说明.md)。
|
开发和验证方法见 [后台索引与保存](../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",
|
"build": "vue-tsc -b && vite build",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"type-check": "vue-tsc --noEmit"
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"build:report": "node scripts/build-size.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/commands": "6.11.0",
|
"@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 { computed } from 'vue'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
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({
|
mermaid.initialize({
|
||||||
startOnLoad: false,
|
startOnLoad: false,
|
||||||
theme: 'base',
|
theme: 'base',
|
||||||
@@ -34,6 +41,7 @@ function ensureInitialized(theme: 'light' | 'dark') {
|
|||||||
sequence: { useMaxWidth: true },
|
sequence: { useMaxWidth: true },
|
||||||
gantt: { useMaxWidth: true },
|
gantt: { useMaxWidth: true },
|
||||||
})
|
})
|
||||||
|
return mermaid
|
||||||
}
|
}
|
||||||
let queue: Promise<unknown> = Promise.resolve()
|
let queue: Promise<unknown> = Promise.resolve()
|
||||||
function serialized<T>(work: () => Promise<T>): Promise<T> {
|
function serialized<T>(work: () => Promise<T>): Promise<T> {
|
||||||
@@ -66,9 +74,9 @@ async function renderMermaidNow(
|
|||||||
options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {}
|
options: { theme?: 'light' | 'dark'; mode?: 'interactive' | 'static' } = {}
|
||||||
): Promise<MermaidRenderResult> {
|
): Promise<MermaidRenderResult> {
|
||||||
const theme = options.theme ?? 'light'
|
const theme = options.theme ?? 'light'
|
||||||
ensureInitialized(theme)
|
|
||||||
const id = `mermaid-${Date.now()}-${++renderCounter}`
|
const id = `mermaid-${Date.now()}-${++renderCounter}`
|
||||||
try {
|
try {
|
||||||
|
const mermaid = await ensureInitialized(theme)
|
||||||
const result = await mermaid.render(id, source)
|
const result = await mermaid.render(id, source)
|
||||||
const parser = new DOMParser()
|
const parser = new DOMParser()
|
||||||
const doc = parser.parseFromString(result.svg, 'image/svg+xml')
|
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 }> {
|
export async function validateMermaid(source: string): Promise<{ valid: boolean; error?: MermaidParseError }> {
|
||||||
try {
|
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 }
|
return { valid: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : '未知错误'
|
const message = error instanceof Error ? error.message : '未知错误'
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import path from 'node:path'
|
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({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
@@ -9,6 +21,23 @@ export default defineConfig({
|
|||||||
'@': path.resolve(__dirname, 'src'),
|
'@': 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: {
|
server: {
|
||||||
host: '127.0.0.1',
|
host: '127.0.0.1',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|||||||
Reference in New Issue
Block a user