From 215ea2e1584874ba4ef9d17d6e80f2be8cba2324 Mon Sep 17 00:00:00 2001 From: pengzhanbo Date: Tue, 25 Jun 2024 01:36:09 +0800 Subject: [PATCH] feat: add cache plugin --- docs/.vuepress/config.ts | 2 + docs/package.json | 3 +- plugins/tools/plugin-cache/CHANGELOG.md | 4 + plugins/tools/plugin-cache/package.json | 42 ++++++++++ .../plugin-cache/src/node/cachePlugin.ts | 19 +++++ .../plugin-cache/src/node/highlightCache.ts | 38 +++++++++ plugins/tools/plugin-cache/src/node/index.ts | 1 + plugins/tools/plugin-cache/src/node/lru.ts | 37 +++++++++ .../plugin-cache/src/node/renderCache.ts | 81 +++++++++++++++++++ plugins/tools/plugin-cache/src/node/utils.ts | 19 +++++ .../tools/plugin-cache/tsconfig.build.json | 8 ++ pnpm-lock.yaml | 9 +++ tsconfig.build.json | 1 + 13 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 plugins/tools/plugin-cache/CHANGELOG.md create mode 100644 plugins/tools/plugin-cache/package.json create mode 100644 plugins/tools/plugin-cache/src/node/cachePlugin.ts create mode 100644 plugins/tools/plugin-cache/src/node/highlightCache.ts create mode 100644 plugins/tools/plugin-cache/src/node/index.ts create mode 100644 plugins/tools/plugin-cache/src/node/lru.ts create mode 100644 plugins/tools/plugin-cache/src/node/renderCache.ts create mode 100644 plugins/tools/plugin-cache/src/node/utils.ts create mode 100644 plugins/tools/plugin-cache/tsconfig.build.json diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 7669e9a94a..c52845c319 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -3,6 +3,7 @@ import { footnote } from '@mdit/plugin-footnote' import { viteBundler } from '@vuepress/bundler-vite' import { webpackBundler } from '@vuepress/bundler-webpack' import { getRealPath } from '@vuepress/helper' +import { cachePlugin } from '@vuepress/plugin-cache' import { catalogPlugin } from '@vuepress/plugin-catalog' import { commentPlugin } from '@vuepress/plugin-comment' import { docsearchPlugin } from '@vuepress/plugin-docsearch' @@ -114,5 +115,6 @@ export default defineUserConfig({ notationWordHighlight: true, }) : [], + cachePlugin(), ], }) as UserConfig diff --git a/docs/package.json b/docs/package.json index b71369cda5..0270df18ae 100644 --- a/docs/package.json +++ b/docs/package.json @@ -4,7 +4,7 @@ "scripts": { "docs:build": "vuepress build . --clean-cache --clean-temp", "docs:build-webpack": "DOCS_BUNDLER=webpack pnpm docs:build", - "docs:dev": "vuepress dev . --clean-cache --clean-temp", + "docs:dev": "vuepress dev .", "docs:dev-webpack": "DOCS_BUNDLER=webpack pnpm docs:dev", "docs:serve": "http-server -a localhost .vuepress/dist" }, @@ -29,6 +29,7 @@ "@vuepress/plugin-register-components": "workspace:*", "@vuepress/plugin-search": "workspace:*", "@vuepress/plugin-shiki": "workspace:*", + "@vuepress/plugin-cache": "workspace:*", "@vuepress/theme-default": "workspace:*", "mathjax-full": "3.2.2", "sass-loader": "^14.2.1", diff --git a/plugins/tools/plugin-cache/CHANGELOG.md b/plugins/tools/plugin-cache/CHANGELOG.md new file mode 100644 index 0000000000..e4d87c4d45 --- /dev/null +++ b/plugins/tools/plugin-cache/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/plugins/tools/plugin-cache/package.json b/plugins/tools/plugin-cache/package.json new file mode 100644 index 0000000000..8d0882c8fa --- /dev/null +++ b/plugins/tools/plugin-cache/package.json @@ -0,0 +1,42 @@ +{ + "name": "@vuepress/plugin-cache", + "version": "2.0.0-rc.37", + "description": "VuePress plugin - cache", + "keywords": [ + "vuepress-plugin", + "vuepress", + "plugin", + "cache" + ], + "homepage": "https://ecosystem.vuejs.press/plugins/tools/cache.html", + "bugs": { + "url": "https://github.com/vuepress/ecosystem/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuepress/ecosystem.git", + "directory": "plugins/tools/plugin-cache" + }, + "license": "MIT", + "author": "pengzhanbo", + "type": "module", + "exports": { + ".": "./lib/node/index.js", + "./package.json": "./package.json" + }, + "main": "./lib/node/index.js", + "types": "./lib/node/index.d.ts", + "files": [ + "lib" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf --glob ./lib ./*.tsbuildinfo" + }, + "peerDependencies": { + "vuepress": "2.0.0-rc.14" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugins/tools/plugin-cache/src/node/cachePlugin.ts b/plugins/tools/plugin-cache/src/node/cachePlugin.ts new file mode 100644 index 0000000000..b84ac7cd7e --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/cachePlugin.ts @@ -0,0 +1,19 @@ +import type { Plugin } from 'vuepress/core' +import { highlightCache } from './highlightCache.js' +import { renderCache } from './renderCache.js' + +/** + * Cache markdown rendering, optimize compilation speed. + * + * This plugin is recommended to be placed after all other plugins to ensure maximum utilization of the cache. + */ +export const cachePlugin = (): Plugin => { + return { + name: '@vuepress/plugin-cache', + + async extendsMarkdown(md, app) { + highlightCache(md, app) + await renderCache(md, app) + }, + } +} diff --git a/plugins/tools/plugin-cache/src/node/highlightCache.ts b/plugins/tools/plugin-cache/src/node/highlightCache.ts new file mode 100644 index 0000000000..badecdd6cd --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/highlightCache.ts @@ -0,0 +1,38 @@ +/** + * Code highlight is a relatively time-consuming operation, + * especially in `shiki` where enabling numerous transformers can significantly impact performance. + * This effect is particularly noticeable with tools like `twoslash`, + * which require type compilation and may lead to individual code blocks taking over 500ms to process. + * + * When there are multiple code blocks, focusing only on modifying parts of the code blocks while + * still compiling all code blocks entirely can lead to unnecessary overhead. + * Therefore, using the LRU cache algorithm can help by storing unchanged code blocks' + * highlighted results and only processing the parts that have been modified. + */ +import type { App } from 'vuepress' +import type { Markdown } from 'vuepress/markdown' +import { LRUCache } from './lru.js' +import { hash } from './utils.js' + +const cache = new LRUCache(64) + +export const highlightCache = (md: Markdown, app: App): void => { + /** + * Cache is only needed in development mode to enhance the development experience. + */ + if (!app.env.isDev) return + + const highlight = md.options.highlight + + md.options.highlight = (...args) => { + const key = hash(args.join('')) + const cached = cache.get(key) + + if (cached) return cached + + const content = highlight?.(...args) ?? '' + cache.set(key, content) + + return content + } +} diff --git a/plugins/tools/plugin-cache/src/node/index.ts b/plugins/tools/plugin-cache/src/node/index.ts new file mode 100644 index 0000000000..ed3c65ff45 --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/index.ts @@ -0,0 +1 @@ +export * from './cachePlugin.js' diff --git a/plugins/tools/plugin-cache/src/node/lru.ts b/plugins/tools/plugin-cache/src/node/lru.ts new file mode 100644 index 0000000000..0e85ca2bb7 --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/lru.ts @@ -0,0 +1,37 @@ +// adapted from https://stackoverflow.com/a/46432113/11613622 + +export class LRUCache { + private max: number + private cache: Map + + constructor(max = 10) { + this.max = max + this.cache = new Map() + } + + get(key: K): V | undefined { + const item = this.cache.get(key) + if (item !== undefined) { + // refresh key + this.cache.delete(key) + this.cache.set(key, item) + } + return item + } + + set(key: K, val: V): void { + // refresh key + if (this.cache.has(key)) this.cache.delete(key) + // evict oldest + else if (this.cache.size === this.max) this.cache.delete(this.first()!) + this.cache.set(key, val) + } + + first(): K | undefined { + return this.cache.keys().next().value + } + + clear(): void { + this.cache.clear() + } +} diff --git a/plugins/tools/plugin-cache/src/node/renderCache.ts b/plugins/tools/plugin-cache/src/node/renderCache.ts new file mode 100644 index 0000000000..9eefd40187 --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/renderCache.ts @@ -0,0 +1,81 @@ +/** + * When various features are added to markdown, the compilation speed of a single markdown file + * will become slower, especially when there are many pages in the project, + * causing the startup of the vuepress development service to become very slow and time-consuming. + * This plugin will cache the `markdown render` result during the initial compilation process. + * During subsequent compilations, if the content has not been modified, + * compilation will be skipped directly, thus speeding up the second startup of vuepress. + */ +import type { App } from 'vuepress' +import type { Markdown, MarkdownEnv } from 'vuepress/markdown' +import { fs } from 'vuepress/utils' +import { hash, readFile, writeFile } from './utils.js' + +export interface CacheDta { + 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' +const CACHE_FILE = '_cache.json' + +export const renderCache = async (md: Markdown, app: App): Promise => { + if (app.env.isBuild && !fs.existsSync(app.dir.cache(CACHE_DIR))) { + return + } + + const basename = app.dir.cache(CACHE_DIR) + const metaFilepath = `${basename}/${META_FILE}` + const cacheFilepath = `${basename}/${CACHE_FILE}` + + await fs.ensureDir(basename) + + const [metadata, cache] = await Promise.all([ + readFile(metaFilepath), + readFile(cacheFilepath), + ]) + + let timer: ReturnType | null = null + const update = async (): Promise => { + timer && clearTimeout(timer) + timer = setTimeout( + async () => + await Promise.all([ + writeFile(metaFilepath, metadata), + writeFile(cacheFilepath, cache), + ]), + 200, + ) + } + + const rawRender = md.render + md.render = (input, env: MarkdownEnv) => { + const filepath = env.filePathRelative + + if (!filepath) { + return rawRender(input, env) + } + + const key = hash(input) + if (metadata[filepath] === key && cache[filepath]) { + const cached = cache[filepath] + Object.assign(env, cached.env) + return cached.content + } + + const content = rawRender(input, env) + metadata[filepath] = key + cache[filepath] = { content, env } + + update() + + return content + } +} diff --git a/plugins/tools/plugin-cache/src/node/utils.ts b/plugins/tools/plugin-cache/src/node/utils.ts new file mode 100644 index 0000000000..3e0189ced3 --- /dev/null +++ b/plugins/tools/plugin-cache/src/node/utils.ts @@ -0,0 +1,19 @@ +import { createHash } from 'node:crypto' +import { fs } from 'vuepress/utils' + +export const hash = (data: string): string => + createHash('md5').update(data).digest('hex') + +export const readFile = async (filepath: string): Promise => { + try { + const content = await fs.readFile(filepath, 'utf-8') + return JSON.parse(content) as T + } catch { + return {} as T + } +} + +export const writeFile = async ( + filepath: string, + data: T, +): Promise => await fs.writeFile(filepath, JSON.stringify(data), 'utf-8') diff --git a/plugins/tools/plugin-cache/tsconfig.build.json b/plugins/tools/plugin-cache/tsconfig.build.json new file mode 100644 index 0000000000..1e7fd0d655 --- /dev/null +++ b/plugins/tools/plugin-cache/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib" + }, + "include": ["./src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 099aad032d..10a7d26c2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ importers: '@vuepress/plugin-back-to-top': specifier: workspace:* version: link:../plugins/features/plugin-back-to-top + '@vuepress/plugin-cache': + specifier: workspace:* + version: link:../plugins/tools/plugin-cache '@vuepress/plugin-catalog': specifier: workspace:* version: link:../plugins/features/plugin-catalog @@ -795,6 +798,12 @@ importers: specifier: workspace:* version: link:../../development/plugin-git + plugins/tools/plugin-cache: + dependencies: + vuepress: + specifier: 2.0.0-rc.14 + version: 2.0.0-rc.14(@vuepress/bundler-vite@2.0.0-rc.14(@types/node@20.14.7)(jiti@1.21.6)(lightningcss@1.25.1)(sass@1.77.6)(terser@5.31.1)(tsx@4.15.7)(typescript@5.5.2)(yaml@2.4.5))(@vuepress/bundler-webpack@2.0.0-rc.14(typescript@5.5.2))(typescript@5.5.2)(vue@3.4.29(typescript@5.5.2)) + plugins/tools/plugin-google-tag-manager: dependencies: vuepress: diff --git a/tsconfig.build.json b/tsconfig.build.json index de4e19a4c2..444cf66fa6 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -84,6 +84,7 @@ { "path": "./plugins/tools/plugin-register-components/tsconfig.build.json" }, + { "path": "./plugins/tools/plugin-cache/tsconfig.build.json" }, // themes { "path": "./themes/theme-default/tsconfig.build.json" },