From 4c9785ec0e0c46e4c8299abd487f3ffa9976ac0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:34:35 +0000 Subject: [PATCH 1/2] Initial plan From 2c24ac4726223edd805043b9a6c1aec5da8651e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Dec 2025 08:43:00 +0000 Subject: [PATCH 2/2] feat: optimize file system operations and component performance - Add caching for post slugs and content in mdx.ts to avoid repeated file reads - Optimize getTopicPosts to filter slugs before loading posts - Optimize getTopicSlugs to use withFileTypes for better performance - Reduce Three.js calculations by pre-computing values and using squared distance - Cache mermaid instance to avoid re-initialization - Improve PagefindSearch debounce and simplify type checking - Remove unused variable in ThreeBackground Co-authored-by: minorcell <120795714+minorcell@users.noreply.github.com> --- content/blog/2025/17_dockerfirst.md | 1 - src/app/topics/[topic]/page.tsx | 3 +- src/components/MermaidBlock.tsx | 50 ++++++++++++++++------- src/components/PagefindSearch.tsx | 16 +++----- src/components/ThreeBackground.tsx | 34 +++++++++------- src/lib/mdx.ts | 61 ++++++++++++++++++----------- src/types/pagefind.d.ts | 5 ++- 7 files changed, 105 insertions(+), 65 deletions(-) diff --git a/content/blog/2025/17_dockerfirst.md b/content/blog/2025/17_dockerfirst.md index c893f82..d14e7c3 100644 --- a/content/blog/2025/17_dockerfirst.md +++ b/content/blog/2025/17_dockerfirst.md @@ -18,7 +18,6 @@ author: mCell ![085.webp](https://stack-mcell.tos-cn-shanghai.volces.com/085.webp) - ## **为什么我们需要 Docker?** 在开发中,你是否遇到过这些问题? diff --git a/src/app/topics/[topic]/page.tsx b/src/app/topics/[topic]/page.tsx index a532f25..9159ac0 100644 --- a/src/app/topics/[topic]/page.tsx +++ b/src/app/topics/[topic]/page.tsx @@ -11,7 +11,8 @@ const topicMeta: Record< bun: { title: 'Bun 指南', meta: 'Runtime', - description: '这是一组面向 Node/JavaScript 开发者的 Bun 实战笔记:目标不是“百科全书”,而是让你能更快把 Bun 用在 CLI、脚本和小型服务里。', + description: + '这是一组面向 Node/JavaScript 开发者的 Bun 实战笔记:目标不是“百科全书”,而是让你能更快把 Bun 用在 CLI、脚本和小型服务里。', }, } diff --git a/src/components/MermaidBlock.tsx b/src/components/MermaidBlock.tsx index c657053..8b8517a 100644 --- a/src/components/MermaidBlock.tsx +++ b/src/components/MermaidBlock.tsx @@ -1,12 +1,16 @@ 'use client' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState, useRef } from 'react' import { Copy, Maximize2, X } from 'lucide-react' type MermaidBlockProps = { code: string } +// Cache mermaid instance to avoid re-importing +let mermaidInstance: typeof import('mermaid').default | null = null +let mermaidInitialized = false + export function MermaidBlock({ code }: MermaidBlockProps) { const [svg, setSvg] = useState(null) const [error, setError] = useState(null) @@ -16,26 +20,40 @@ export function MermaidBlock({ code }: MermaidBlockProps) { ) const [copied, setCopied] = useState(false) const [zoomed, setZoomed] = useState(false) + const isRenderingRef = useRef(false) useEffect(() => { let mounted = true ;(async () => { + // Prevent duplicate renders + if (isRenderingRef.current) return + isRenderingRef.current = true + try { - const mermaid = (await import('mermaid')).default - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'loose', - theme: 'dark', - fontFamily: 'Source Sans 3, Inter, -apple-system, sans-serif', - themeVariables: { - background: '#0b0d11', - primaryColor: '#1a73e8', - primaryTextColor: '#e5e7eb', - lineColor: '#94a3b8', - }, - }) - const { svg } = await mermaid.render(renderId, code) + // Load and initialize mermaid only once + if (!mermaidInstance) { + const mermaid = (await import('mermaid')).default + mermaidInstance = mermaid + } + + if (!mermaidInitialized) { + mermaidInstance.initialize({ + startOnLoad: false, + securityLevel: 'loose', + theme: 'dark', + fontFamily: 'Source Sans 3, Inter, -apple-system, sans-serif', + themeVariables: { + background: '#0b0d11', + primaryColor: '#1a73e8', + primaryTextColor: '#e5e7eb', + lineColor: '#94a3b8', + }, + }) + mermaidInitialized = true + } + + const { svg } = await mermaidInstance.render(renderId, code) if (mounted) { setSvg(svg) setError(null) @@ -43,6 +61,8 @@ export function MermaidBlock({ code }: MermaidBlockProps) { } catch (err) { console.error('Mermaid render failed', err) if (mounted) setError('Mermaid 图渲染失败') + } finally { + isRenderingRef.current = false } })() diff --git a/src/components/PagefindSearch.tsx b/src/components/PagefindSearch.tsx index 34d46ff..37d5fd1 100644 --- a/src/components/PagefindSearch.tsx +++ b/src/components/PagefindSearch.tsx @@ -157,19 +157,15 @@ export function PagefindSearch({ try { const search = await pagefind.search(query) + // Limit to 20 results and process them efficiently + const topResults = search.results.slice(0, 20) const detailed = await Promise.all( - search.results.slice(0, 20).map(async (result: PagefindHit, idx) => { + topResults.map(async (result: PagefindHit, idx) => { const data = await result.data() return { url: data.url, - title: - (data.meta && typeof data.meta.title === 'string' - ? data.meta.title - : data.url) ?? `结果 ${idx + 1}`, - excerpt: - typeof data.excerpt === 'string' - ? data.excerpt - : data.content?.slice(0, 200), + title: data.meta?.title || data.url || `结果 ${idx + 1}`, + excerpt: data.excerpt || data.content?.slice(0, 200), } }), ) @@ -180,7 +176,7 @@ export function PagefindSearch({ } finally { setIsSearching(false) } - }, 180) + }, 200) // Slightly longer debounce for better performance return () => clearTimeout(handle) }, [query, isActive, ensurePagefind]) diff --git a/src/components/ThreeBackground.tsx b/src/components/ThreeBackground.tsx index b171d97..609ea63 100644 --- a/src/components/ThreeBackground.tsx +++ b/src/components/ThreeBackground.tsx @@ -82,8 +82,7 @@ function ParticleNet() { const time = clock.getElapsedTime() if (pointsRef.current) { - // 1. 移除整体倾斜交互,保持网格平稳 - // 平滑恢复到初始角度 (0,0,0) + // Smooth rotation recovery to initial angle (0,0,0) pointsRef.current.rotation.x = THREE.MathUtils.lerp( pointsRef.current.rotation.x, 0, @@ -98,13 +97,18 @@ function ParticleNet() { const positions = pointsRef.current.geometry.attributes.position .array as Float32Array - // Use global mouse ref - // 将归一化的鼠标坐标 (-1 到 1) 转换为世界坐标 + // Use global mouse ref and convert to world coordinates const mouseX = (mouseRef.current.x * viewport.width) / 2 const mouseY = (mouseRef.current.y * viewport.height) / 2 const base = baseCoordsRef.current + // Pre-calculate values used in loop + const timeFactor1 = time * 0.8 + const timeFactor2 = time * 0.6 + const interactionRadius = 4.0 + const interactionRadiusSq = interactionRadius * interactionRadius + for (let idx = 0; idx < totalPoints; idx++) { const positionIndex = idx * 3 const baseIndex = idx * 2 @@ -112,21 +116,21 @@ function ParticleNet() { const x = base[baseIndex] const y = base[baseIndex + 1] - // 基础波浪运动 + // Base wave motion let z = - Math.sin(x * 0.6 + time * 0.8) * Math.cos(y * 0.6 + time * 0.6) * 0.8 + Math.sin(x * 0.6 + timeFactor1) * + Math.cos(y * 0.6 + timeFactor2) * + 0.8 - // 鼠标交互:计算粒子到鼠标的距离 + // Mouse interaction: calculate distance to mouse const dx = x - mouseX const dy = y - mouseY - const dist = Math.sqrt(dx * dx + dy * dy) - - // 交互范围和强度 - // 范围半径约 3 单位,中心强度 2.5 - const interactionRadius = 4.0 - if (dist < interactionRadius) { - // 使用高斯衰减函数制造平滑隆起 - const strength = 2.5 * Math.exp((-dist * dist) / (2 * 1.5)) // sigma^2 = 1.5 + const distSq = dx * dx + dy * dy + + // Only apply interaction if within radius (using squared distance to avoid sqrt) + if (distSq < interactionRadiusSq) { + // Gaussian decay function for smooth bump + const strength = 2.5 * Math.exp(-distSq / 3.0) // sigma^2 = 1.5 z += strength } diff --git a/src/lib/mdx.ts b/src/lib/mdx.ts index cc3e305..135b5b2 100644 --- a/src/lib/mdx.ts +++ b/src/lib/mdx.ts @@ -23,9 +23,22 @@ export interface Post { slug: string } +// Cache for post slugs to avoid repeated file system traversal +const slugCache = new Map() +// Cache for parsed posts to avoid re-reading and re-parsing files +const postCache = new Map() + export function getPostSlugs(type: PostType) { + // Return cached result if available + if (slugCache.has(type)) { + return slugCache.get(type)! + } + const dir = path.join(contentDir, type) - if (!fs.existsSync(dir)) return [] + if (!fs.existsSync(dir)) { + slugCache.set(type, []) + return [] + } const files: string[] = [] @@ -45,24 +58,16 @@ export function getPostSlugs(type: PostType) { } traverse(dir) + slugCache.set(type, files) return files } export function getPostBySlug(type: PostType, slug: string): Post { - // Slug might contain slashes if it was nested, but here we usually flatten or handle it. - // However, the current implementation of [slug] page assumes a single segment slug. - // If we want to support nested routes like /blog/2025/foo, we need [...slug]. - // For now, let's assume we want to flatten them or just find the file by name? - // Or better, let's update [slug] to [...slug] if we want to keep the structure. - // BUT, the user's existing structure is `blog/2025/foo.md`. - // If I return `2025/foo.md` as slug, the URL will be `/blog/2025%2Ffoo`. - // That's ugly. - // If I want `/blog/foo`, I need to handle collisions. - // Let's assume we want to support the path as is. - // So I should change `[slug]` to `[...slug]`. - - // For now, I will implement a simple recursive search that returns the RELATIVE path as the slug. - // And I will update the page to use `[...slug]`. + // Check cache first + const cacheKey = `${type}:${slug}` + if (postCache.has(cacheKey)) { + return postCache.get(cacheKey)! + } const dir = path.join(contentDir, type) const realSlug = slug.replace(/\.mdx?$/, '') @@ -100,7 +105,7 @@ export function getPostBySlug(type: PostType, slug: string): Post { } } - return { + const post: Post = { slug: realSlug, metadata: { ...data, @@ -113,6 +118,10 @@ export function getPostBySlug(type: PostType, slug: string): Post { }, content, } + + // Cache the post + postCache.set(cacheKey, post) + return post } export function getAllPosts(type: PostType): Post[] { @@ -126,14 +135,22 @@ export function getAllPosts(type: PostType): Post[] { export function getTopicSlugs(): string[] { const dir = path.join(contentDir, 'topics') if (!fs.existsSync(dir)) return [] - return fs - .readdirSync(dir) - .filter((item) => fs.statSync(path.join(dir, item)).isDirectory()) + + // Cache directory check with readdirSync for better performance + const items = fs.readdirSync(dir, { withFileTypes: true }) + return items.filter((item) => item.isDirectory()).map((item) => item.name) } export function getTopicPosts(topicSlug: string): Post[] { - const all = getAllPosts('topics') - const filtered = all.filter((post) => post.slug.startsWith(`${topicSlug}/`)) + // Get only slugs for this topic, avoiding loading all posts + const slugs = getPostSlugs('topics') + const topicPrefix = `${topicSlug}/` + + // Filter slugs first before loading posts + const filteredSlugs = slugs.filter((slug) => slug.startsWith(topicPrefix)) + + // Load only the relevant posts + const posts = filteredSlugs.map((slug) => getPostBySlug('topics', slug)) const getLeadingNumber = (slug: string) => { const last = slug.split('/').pop() ?? slug @@ -141,7 +158,7 @@ export function getTopicPosts(topicSlug: string): Post[] { return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER } - return filtered.sort((a, b) => { + return posts.sort((a, b) => { const numA = getLeadingNumber(a.slug) const numB = getLeadingNumber(b.slug) if (numA !== numB) return numA - numB diff --git a/src/types/pagefind.d.ts b/src/types/pagefind.d.ts index dce3b9d..3f85332 100644 --- a/src/types/pagefind.d.ts +++ b/src/types/pagefind.d.ts @@ -12,6 +12,9 @@ declare module '/pagefind/pagefind.js' { } export function init(): Promise - export function options(opts?: { basePath?: string; baseUrl?: string }): Promise + export function options(opts?: { + basePath?: string + baseUrl?: string + }): Promise export function search(query: string): Promise<{ results: PagefindHit[] }> }