|
| 1 | +# Spring Lament Blog 开发问题记录 |
| 2 | + |
| 3 | +## 1. 文章详情页水合不匹配 |
| 4 | + |
| 5 | +### 问题描述 |
| 6 | + |
| 7 | +Next.js SSR 水合错误:服务端渲染的 HTML 与客户端渲染不匹配,导致控制台出现 hydration 错误。 |
| 8 | + |
| 9 | +### 错误信息 |
| 10 | + |
| 11 | +``` |
| 12 | +Hydration failed because the server rendered HTML didn't match the client |
| 13 | +Error: Text content does not match server-rendered HTML. |
| 14 | +``` |
| 15 | + |
| 16 | +### 问题原因 |
| 17 | + |
| 18 | +- 标题 ID 生成使用了 `Date.now()` 和 `Math.random()` |
| 19 | +- 服务端和客户端生成的随机值不同 |
| 20 | +- 导致相同内容生成不同的 DOM 结构 |
| 21 | + |
| 22 | +### 解决方案 |
| 23 | + |
| 24 | +1. **实现稳定的哈希算法**: |
| 25 | + |
| 26 | + ```javascript |
| 27 | + const generateStableUniqueId = (text: string, index: number) => { |
| 28 | + const baseId = text.toLowerCase().replace(/[^\w\u4e00-\u9fa5\s-]/g, "").replace(/\s+/g, "-"); |
| 29 | + const hash = text.split('').reduce((a, b) => { |
| 30 | + a = ((a << 5) - a) + b.charCodeAt(0); |
| 31 | + return a & a; |
| 32 | + }, 0); |
| 33 | + return `${baseId}-${Math.abs(hash).toString(36)}-${index}`; |
| 34 | + }; |
| 35 | + ``` |
| 36 | + |
| 37 | +2. **预生成所有标题 ID**:在 useMemo 中一次性解析所有标题并生成稳定的 ID 映射 |
| 38 | + |
| 39 | +3. **统一 Mermaid ID 生成**:使用内容哈希而非随机数 |
| 40 | + |
| 41 | +### 验证结果 |
| 42 | + |
| 43 | +- ✅ SSR 水合错误完全消除 |
| 44 | +- ✅ 服务端客户端 ID 生成一致 |
| 45 | +- ✅ 目录跳转功能正常 |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +## 2. 导入导出文件太大 |
| 50 | + |
| 51 | +### 问题描述 |
| 52 | + |
| 53 | +应用打包后文件体积过大,影响加载性能。 |
| 54 | + |
| 55 | +### 待分析 |
| 56 | + |
| 57 | +- Bundle 分析 |
| 58 | +- 代码分割优化 |
| 59 | +- 依赖项瘦身 |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +## 3. Mermaid重绘导致的页面闪烁 |
| 64 | + |
| 65 | +### 问题描述 |
| 66 | + |
| 67 | +在博客文章页面滚动时,Mermaid 图表出现明显的重渲染闪烁现象,严重影响用户体验。 |
| 68 | + |
| 69 | +### 复现步骤 |
| 70 | + |
| 71 | +1. 访问包含 Mermaid 图表的文章页面 |
| 72 | +2. 向下滚动页面 |
| 73 | +3. 观察到 Mermaid 图表频繁闪烁/重新渲染 |
| 74 | + |
| 75 | +### 问题根因分析 |
| 76 | + |
| 77 | +1. **组件频繁重渲染**:滚动时 `activeHeading` 状态频繁更新 → 整个 MarkdownRenderer 组件重新渲染 |
| 78 | +2. **Mermaid 重新初始化**:每次组件重渲染时,Mermaid 图表都会重新渲染 |
| 79 | +3. **资源浪费**:主题检测的 MutationObserver 被重复创建 |
| 80 | + |
| 81 | +### 解决方案 |
| 82 | + |
| 83 | +#### 3.1 Mermaid组件优化 (`src/components/markdown/mermaid.tsx`) |
| 84 | + |
| 85 | +**添加全局缓存机制**: |
| 86 | + |
| 87 | +```typescript |
| 88 | +// 全局缓存已渲染的图表 |
| 89 | +const mermaidCache = new Map<string, { svg: string; theme: string }>(); |
| 90 | +``` |
| 91 | + |
| 92 | +**优化主题检测**: |
| 93 | + |
| 94 | +```typescript |
| 95 | +// 只在组件挂载时创建一次 MutationObserver |
| 96 | +useEffect(() => { |
| 97 | + setIsDark(currentTheme === "dark"); |
| 98 | + |
| 99 | + const checkTheme = () => { |
| 100 | + const newIsDark = document.documentElement.classList.contains("dark"); |
| 101 | + setIsDark(newIsDark); |
| 102 | + }; |
| 103 | + |
| 104 | + const observer = new MutationObserver(checkTheme); |
| 105 | + observer.observe(document.documentElement, { |
| 106 | + attributes: true, |
| 107 | + attributeFilter: ["class"], |
| 108 | + }); |
| 109 | + |
| 110 | + return () => observer.disconnect(); |
| 111 | +}, []); // 只在挂载时执行一次 |
| 112 | +``` |
| 113 | + |
| 114 | +**渲染防抖机制**: |
| 115 | + |
| 116 | +```typescript |
| 117 | +const renderAttempted = useRef(false); |
| 118 | + |
| 119 | +const renderChart = async () => { |
| 120 | + if (!chart || renderAttempted.current) return; |
| 121 | + |
| 122 | + const theme = isDark ? "dark" : "default"; |
| 123 | + const cacheKey = `${chart}-${theme}`; |
| 124 | + |
| 125 | + // 检查缓存 |
| 126 | + const cached = mermaidCache.get(cacheKey); |
| 127 | + if (cached) { |
| 128 | + setSvg(cached.svg); |
| 129 | + setError(""); |
| 130 | + return; |
| 131 | + } |
| 132 | + |
| 133 | + renderAttempted.current = true; |
| 134 | + // ... 渲染逻辑 |
| 135 | + // 存入缓存 |
| 136 | + mermaidCache.set(cacheKey, { svg: renderedSvg, theme }); |
| 137 | +}; |
| 138 | +``` |
| 139 | + |
| 140 | +#### 3.2 MarkdownRenderer组件优化 (`src/components/markdown/markdown-renderer.tsx`) |
| 141 | + |
| 142 | +**使用 useMemo 防止不必要的重渲染**: |
| 143 | + |
| 144 | +```typescript |
| 145 | +{useMemo( |
| 146 | + () => ( |
| 147 | + <ReactMarkdown |
| 148 | + remarkPlugins={[remarkGfm]} |
| 149 | + rehypePlugins={[rehypeRaw]} |
| 150 | + components={{...}} |
| 151 | + > |
| 152 | + {content || ""} |
| 153 | + </ReactMarkdown> |
| 154 | + ), |
| 155 | + [content, headingIdMap] // 只依赖 content 和 headingIdMap |
| 156 | +)} |
| 157 | +``` |
| 158 | + |
| 159 | +### 性能提升效果 |
| 160 | + |
| 161 | +**优化前**: |
| 162 | + |
| 163 | +- ❌ 滚动时 Mermaid 图表频繁闪烁 |
| 164 | +- ❌ 每次滚动都重新渲染整个 Markdown 内容 |
| 165 | +- ❌ 重复创建 MutationObserver 和事件监听器 |
| 166 | + |
| 167 | +**优化后**: |
| 168 | + |
| 169 | +- ✅ 滚动时 Mermaid 图表保持稳定 |
| 170 | +- ✅ 缓存机制大幅提升渲染性能 |
| 171 | +- ✅ 资源复用,减少内存占用 |
| 172 | +- ✅ 用户体验显著改善 |
| 173 | + |
| 174 | +### 关键经验总结 |
| 175 | + |
| 176 | +1. **识别并分离频繁变化的状态**:将滚动相关状态与内容渲染分离 |
| 177 | +2. **为昂贵的渲染操作实现缓存**:避免重复的异步渲染 |
| 178 | +3. **合理管理副作用的生命周期**:确保资源正确清理 |
| 179 | +4. **使用 React 优化手段**:useMemo、useCallback 等 |
| 180 | + |
| 181 | +--- |
| 182 | + |
| 183 | +## 开发经验总结 |
| 184 | + |
| 185 | +### 性能优化原则 |
| 186 | + |
| 187 | +1. 优先识别性能瓶颈,避免过度优化 |
| 188 | +2. 合理使用缓存机制 |
| 189 | +3. 注意 React 渲染优化 |
| 190 | +4. 监控内存使用情况 |
| 191 | + |
| 192 | +### 调试技巧 |
| 193 | + |
| 194 | +1. 使用 React DevTools 分析组件渲染 |
| 195 | +2. 利用浏览器性能面板定位问题 |
| 196 | +3. 添加适当的日志和断点 |
| 197 | +4. 重视用户反馈和实际体验 |
0 commit comments