feat: 优化 Mermaid 渲染性能
- 12.1 实现批量渲染:使用 requestAnimationFrame 分批渲染,避免阻塞主线程 - 12.2 添加加载动画:显示加载状态和旋转动画,提供视觉反馈 - 12.3 实现延迟渲染:优先渲染视口内图表,延迟渲染视口外图表 - 12.4 优化错误处理:单个图表渲染失败不影响其他图表 需求:18.1, 18.2, 18.3, 18.4, 18.5
This commit is contained in:
175
argontheme.js
175
argontheme.js
@@ -5235,6 +5235,8 @@ void 0;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量渲染所有 Mermaid 图表
|
* 批量渲染所有 Mermaid 图表
|
||||||
|
* 需求 18.1: 使用批量渲染,避免阻塞主线程
|
||||||
|
* 需求 18.4: 延迟渲染视口外的图表,优先渲染可见图表
|
||||||
*/
|
*/
|
||||||
renderAllCharts() {
|
renderAllCharts() {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
@@ -5250,41 +5252,111 @@ void 0;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量渲染
|
this.logDebug(`准备批量渲染 ${blocks.length} 个图表`);
|
||||||
blocks.forEach((block, index) => {
|
|
||||||
this.renderChart(block, index);
|
// 需求 18.4: 将图表分为视口内和视口外两组
|
||||||
|
const visibleBlocks = [];
|
||||||
|
const invisibleBlocks = [];
|
||||||
|
|
||||||
|
blocks.forEach(block => {
|
||||||
|
if (this.isInViewport(block)) {
|
||||||
|
visibleBlocks.push(block);
|
||||||
|
} else {
|
||||||
|
invisibleBlocks.push(block);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logDebug(`完成渲染 ${blocks.length} 个图表`);
|
this.logDebug(`视口内图表: ${visibleBlocks.length}, 视口外图表: ${invisibleBlocks.length}`);
|
||||||
|
|
||||||
|
// 优先渲染视口内的图表
|
||||||
|
let currentIndex = 0;
|
||||||
|
const batchSize = 3; // 每批渲染 3 个图表
|
||||||
|
|
||||||
|
const renderBatch = (blockList, isVisible) => {
|
||||||
|
if (currentIndex >= blockList.length) {
|
||||||
|
// 当前列表渲染完成
|
||||||
|
if (isVisible && invisibleBlocks.length > 0) {
|
||||||
|
// 视口内图表渲染完成,开始渲染视口外图表
|
||||||
|
this.logDebug('视口内图表渲染完成,开始渲染视口外图表');
|
||||||
|
currentIndex = 0;
|
||||||
|
requestAnimationFrame(() => renderBatch(invisibleBlocks, false));
|
||||||
|
} else {
|
||||||
|
this.logDebug(`完成渲染 ${blocks.length} 个图表`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endIndex = Math.min(currentIndex + batchSize, blockList.length);
|
||||||
|
|
||||||
|
// 渲染当前批次
|
||||||
|
for (let i = currentIndex; i < endIndex; i++) {
|
||||||
|
const globalIndex = blocks.indexOf(blockList[i]);
|
||||||
|
this.renderChart(blockList[i], globalIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = endIndex;
|
||||||
|
|
||||||
|
// 继续下一批
|
||||||
|
requestAnimationFrame(() => renderBatch(blockList, isVisible));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始渲染(优先渲染视口内图表)
|
||||||
|
if (visibleBlocks.length > 0) {
|
||||||
|
requestAnimationFrame(() => renderBatch(visibleBlocks, true));
|
||||||
|
} else if (invisibleBlocks.length > 0) {
|
||||||
|
// 如果没有视口内图表,直接渲染视口外图表
|
||||||
|
requestAnimationFrame(() => renderBatch(invisibleBlocks, false));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查元素是否在视口内
|
||||||
|
* @param {HTMLElement} element - 要检查的元素
|
||||||
|
* @returns {boolean} 是否在视口内
|
||||||
|
*/
|
||||||
|
isInViewport(element) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
||||||
|
|
||||||
|
// 元素至少部分可见
|
||||||
|
return (
|
||||||
|
rect.top < windowHeight &&
|
||||||
|
rect.bottom > 0 &&
|
||||||
|
rect.left < windowWidth &&
|
||||||
|
rect.right > 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染单个图表
|
* 渲染单个图表
|
||||||
|
* 需求 18.5: 图表渲染失败不阻塞其他图表的渲染
|
||||||
* @param {HTMLElement} element - 代码块元素
|
* @param {HTMLElement} element - 代码块元素
|
||||||
* @param {number} index - 图表索引
|
* @param {number} index - 图表索引
|
||||||
*/
|
*/
|
||||||
renderChart(element, index) {
|
renderChart(element, index) {
|
||||||
const chartId = `mermaid-chart-${Date.now()}-${index}`;
|
const chartId = `mermaid-chart-${Date.now()}-${index}`;
|
||||||
|
|
||||||
// 检查是否已经是错误容器(避免重复处理错误)
|
// 需求 18.5: 使用 try-catch 包裹整个渲染过程
|
||||||
if (element.classList && element.classList.contains('mermaid-error-container')) {
|
|
||||||
this.logDebug(`元素已经是错误容器,跳过: ${chartId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已经是渲染成功的容器(避免重复渲染)
|
|
||||||
if (element.classList && element.classList.contains('mermaid-container') && element.dataset.mermaidCode) {
|
|
||||||
this.logDebug(`图表已成功渲染,跳过: ${chartId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否已渲染(避免重复渲染)
|
|
||||||
if (this.rendered.has(element)) {
|
|
||||||
this.logDebug(`图表已渲染,跳过: ${chartId}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 检查是否已经是错误容器(避免重复处理错误)
|
||||||
|
if (element.classList && element.classList.contains('mermaid-error-container')) {
|
||||||
|
this.logDebug(`元素已经是错误容器,跳过: ${chartId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经是渲染成功的容器(避免重复渲染)
|
||||||
|
if (element.classList && element.classList.contains('mermaid-container') && element.dataset.mermaidCode) {
|
||||||
|
this.logDebug(`图表已成功渲染,跳过: ${chartId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已渲染(避免重复渲染)
|
||||||
|
if (this.rendered.has(element)) {
|
||||||
|
this.logDebug(`图表已渲染,跳过: ${chartId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 提取代码
|
// 提取代码
|
||||||
const code = this.extractMermaidCode(element);
|
const code = this.extractMermaidCode(element);
|
||||||
|
|
||||||
@@ -5296,7 +5368,20 @@ void 0;
|
|||||||
this.logDebug(`准备渲染图表: ${chartId}, 代码长度: ${code.length}`);
|
this.logDebug(`准备渲染图表: ${chartId}, 代码长度: ${code.length}`);
|
||||||
this.logDebug(`代码内容: ${code.substring(0, 200)}`);
|
this.logDebug(`代码内容: ${code.substring(0, 200)}`);
|
||||||
|
|
||||||
// 创建容器
|
// 需求 18.2: 创建加载动画容器
|
||||||
|
const loadingContainer = document.createElement('div');
|
||||||
|
loadingContainer.className = 'mermaid-loading';
|
||||||
|
loadingContainer.innerHTML = `
|
||||||
|
<div class="mermaid-loading-spinner"></div>
|
||||||
|
<div class="mermaid-loading-text">正在渲染图表...</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 替换原始元素为加载动画
|
||||||
|
if (element.parentNode) {
|
||||||
|
element.parentNode.replaceChild(loadingContainer, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建最终容器
|
||||||
const container = document.createElement('div');
|
const container = document.createElement('div');
|
||||||
container.className = 'mermaid-container';
|
container.className = 'mermaid-container';
|
||||||
container.id = chartId;
|
container.id = chartId;
|
||||||
@@ -5304,13 +5389,13 @@ void 0;
|
|||||||
// 检查 Mermaid API
|
// 检查 Mermaid API
|
||||||
if (typeof window.mermaid === 'undefined') {
|
if (typeof window.mermaid === 'undefined') {
|
||||||
this.logError('Mermaid 库未加载');
|
this.logError('Mermaid 库未加载');
|
||||||
this.handleRenderError(element, new Error('Mermaid 库未加载'), code);
|
this.handleRenderError(loadingContainer, new Error('Mermaid 库未加载'), code);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window.mermaid.render !== 'function') {
|
if (typeof window.mermaid.render !== 'function') {
|
||||||
this.logError('Mermaid.render 方法不存在');
|
this.logError('Mermaid.render 方法不存在');
|
||||||
this.handleRenderError(element, new Error('Mermaid.render 方法不存在'), code);
|
this.handleRenderError(loadingContainer, new Error('Mermaid.render 方法不存在'), code);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5321,7 +5406,7 @@ void 0;
|
|||||||
if (!renderPromise || typeof renderPromise.then !== 'function') {
|
if (!renderPromise || typeof renderPromise.then !== 'function') {
|
||||||
this.logError('Mermaid.render 没有返回 Promise,可能是版本不兼容');
|
this.logError('Mermaid.render 没有返回 Promise,可能是版本不兼容');
|
||||||
// 尝试使用旧版 API
|
// 尝试使用旧版 API
|
||||||
this.renderChartLegacy(element, code, container, chartId);
|
this.renderChartLegacy(loadingContainer, code, container, chartId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5333,27 +5418,35 @@ void 0;
|
|||||||
container.dataset.mermaidCode = code;
|
container.dataset.mermaidCode = code;
|
||||||
container.dataset.currentTheme = this.getMermaidTheme();
|
container.dataset.currentTheme = this.getMermaidTheme();
|
||||||
|
|
||||||
// 替换原始代码块
|
// 替换加载动画为渲染结果
|
||||||
if (element.parentNode) {
|
if (loadingContainer.parentNode) {
|
||||||
element.parentNode.replaceChild(container, element);
|
loadingContainer.parentNode.replaceChild(container, loadingContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记为已渲染
|
// 标记为已渲染
|
||||||
this.rendered.add(container);
|
this.rendered.add(container);
|
||||||
|
|
||||||
// 应用样式增强
|
// 应用样式增强(包含淡入动画)
|
||||||
this.applyStyles(container);
|
this.applyStyles(container);
|
||||||
|
|
||||||
this.logDebug(`图表渲染成功: ${chartId}`);
|
this.logDebug(`图表渲染成功: ${chartId}`);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// 渲染失败
|
// 需求 18.5: 渲染失败时记录错误但不抛出异常
|
||||||
this.logError(`图表渲染失败: ${chartId}`, error);
|
this.logError(`图表渲染失败: ${chartId}`, error);
|
||||||
this.handleRenderError(element, error, code);
|
this.handleRenderError(loadingContainer, error, code);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// 需求 18.5: 捕获所有异常,确保不影响其他图表
|
||||||
this.logError(`渲染异常: ${chartId}`, error);
|
this.logError(`渲染异常: ${chartId}`, error);
|
||||||
this.handleRenderError(element, error, '');
|
|
||||||
|
// 尝试显示错误信息
|
||||||
|
try {
|
||||||
|
this.handleRenderError(element, error, '');
|
||||||
|
} catch (e) {
|
||||||
|
// 如果错误处理也失败,只记录日志
|
||||||
|
this.logError('错误处理失败', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -5507,15 +5600,23 @@ void 0;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用容器样式并添加缩放控制
|
* 应用容器样式并添加缩放控制
|
||||||
|
* 需求 18.3: 使用淡入动画,提升视觉体验
|
||||||
* @param {HTMLElement} container - 图表容器
|
* @param {HTMLElement} container - 图表容器
|
||||||
*/
|
*/
|
||||||
applyStyles(container) {
|
applyStyles(container) {
|
||||||
// 添加淡入动画
|
// 需求 18.3: 添加淡入动画
|
||||||
container.style.opacity = '0';
|
container.style.opacity = '0';
|
||||||
setTimeout(() => {
|
|
||||||
container.style.transition = 'opacity 0.3s ease-in';
|
// 使用 requestAnimationFrame 确保动画流畅
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
container.style.transition = 'opacity 0.4s ease-in';
|
||||||
container.style.opacity = '1';
|
container.style.opacity = '1';
|
||||||
}, 10);
|
|
||||||
|
// 动画完成后清理过渡样式
|
||||||
|
setTimeout(() => {
|
||||||
|
container.style.transition = '';
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
|
||||||
// 确保 SVG 响应式
|
// 确保 SVG 响应式
|
||||||
const svg = container.querySelector('svg');
|
const svg = container.querySelector('svg');
|
||||||
|
|||||||
49
style.css
49
style.css
@@ -921,6 +921,55 @@ article .wp-block-separator {
|
|||||||
animation: mermaidFadeIn 0.3s ease-in-out forwards;
|
animation: mermaidFadeIn 0.3s ease-in-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 需求 18.2: Mermaid 加载动画容器 */
|
||||||
|
.mermaid-loading {
|
||||||
|
background: var(--color-foreground);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 40px 20px;
|
||||||
|
margin: 20px -20px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(94, 114, 228, 0.2);
|
||||||
|
border-top-color: #5e72e4;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: mermaidSpinner 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-loading-text {
|
||||||
|
margin-top: 15px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.darkmode .mermaid-loading {
|
||||||
|
background: var(--color-widgets);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.darkmode .mermaid-loading-text {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.darkmode .mermaid-loading-spinner {
|
||||||
|
border-color: rgba(94, 114, 228, 0.3);
|
||||||
|
border-top-color: #5e72e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载动画旋转 */
|
||||||
|
@keyframes mermaidSpinner {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mermaid-container-inner {
|
.mermaid-container-inner {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
|
|||||||
Reference in New Issue
Block a user