/** * 针对主题使用了 shiki + twoslash, 以及各种各样的对 markdown 的扩展, * 导致了 markdown render 的速度变得越来越慢,如果每次启动都全量编译,那么时间开销会非常夸张。 * 因此,对 markdown render 包装一层 缓存,通过 content hash 对比内容是否有更新, * 没有更新的直接应用缓存从而跳过编译过程,加快启动速度。 * * 此功能计划做成独立的插件,但还不确定是放在 vuepress/ecosystem 还是在 主题插件内, * 也有可能到 vuepress/core 仓库中进行更深度的优化。 * 因此,先在本主题中进行 实验性验证。 * * 使用此功能后,本主题原本的启动耗时,由每次 13s 左右 优化到 二次启动时 1.2s 左右。 * 基本只剩下 vuepress 本身的开销和 加载 shiki 所有语言带来 0.5s 左右的开销。 */ import process from 'node:process' import { fs, path } from 'vuepress/utils' import type { App } from 'vuepress' import type { Markdown, MarkdownEnv } from 'vuepress/markdown' import { hash } from './utils/index.js' export interface CacheData { content: string env: MarkdownEnv } // { [filepath]: CacheDta } export type Cache = Record // { [filepath]: hash } export type Metadata = Record const CACHE_DIR = 'markdown/rendered' const META_FILE = '_metadata.json' export async function extendsMarkdown(md: Markdown, app: App): Promise { if (app.env.isBuild && !fs.existsSync(app.dir.cache(CACHE_DIR))) { return } const basename = app.dir.cache(CACHE_DIR) await fs.ensureDir(basename) const speed = checkIOSpeed(basename) const metaFilepath = `${basename}/${META_FILE}` const metadata = (await readFile(metaFilepath)) || {} let timer: ReturnType | null = null const update = (filepath: string, data: CacheData): void => { writeFile(`${basename}/${filepath}`, data) timer && clearTimeout(timer) timer = setTimeout(async () => writeFile(metaFilepath, metadata), 200) } const rawRender = md.render md.render = (input, env: MarkdownEnv) => { const filepath = env.filePathRelative if (!filepath) { return rawRender(input, env) } const key = hash(input) const filename = normalizeFilename(filepath) if (metadata[filepath] === key) { const cached = readFileSync(`${basename}/${filename}`) if (cached) { Object.assign(env, cached.env) return cached.content } else { metadata[filepath] = '' } } const start = performance.now() const content = rawRender(input, env) /** * High-frequency I/O is also a time-consuming operation, * therefore, for render operations with low overhead, caching is not performed. */ if (performance.now() - start > speed) { metadata[filepath] = key update(filename, { content, env }) } return content } } function normalizeFilename(filename: string): string { return hash(filename).slice(0, 10) } async function readFile(filepath: string): Promise { try { const content = await fs.readFile(filepath, 'utf-8') return JSON.parse(content) as T } catch { return null } } function readFileSync(filepath: string): T | null { try { const content = fs.readFileSync(filepath, 'utf-8') return JSON.parse(content) as T } catch { return null } } async function writeFile(filepath: string, data: T): Promise { return await fs.writeFile(filepath, JSON.stringify(data), 'utf-8') } export function checkIOSpeed(cwd = process.cwd()): number { try { const tmp = path.join(cwd, 'tmp') fs.writeFileSync(tmp, '{}', 'utf-8') const start = performance.now() readFileSync(tmp) const end = performance.now() fs.unlinkSync(tmp) return end - start } catch { return 0.15 } }