Skip to content

Markdown 性能优化

随着 Markdown 文档变得越来越复杂,性能优化变得至关重要。本指南将帮助你识别性能瓶颈并实施优化策略。

性能基础

为什么性能很重要?

  1. 用户体验:更快的加载速度 = 更好的用户体验
  2. SEO 排名:Google 将页面速度作为排名因素
  3. 转化率:每增加 1 秒加载时间,转化率下降 7%
  4. 跳出率:慢速页面导致 53% 的用户离开

性能指标

核心 Web 指标(Core Web Vitals)

  • LCP(最大内容绘制):< 2.5秒
  • FID(首次输入延迟):< 100毫秒
  • CLS(累积布局偏移):< 0.1

其他重要指标

  • TTFB(首字节时间):< 600毫秒
  • FCP(首次内容绘制):< 1.8秒
  • TTI(可交互时间):< 3.8秒

Markdown 解析优化

选择高效的解析器

性能对比

javascript
// 基准测试
const marked = require('marked');
const markdownIt = require('markdown-it');
const micromark = require('micromark');

console.time('marked');
marked.parse(longMarkdown);
console.timeEnd('marked');  // ~50ms

console.time('markdown-it');
markdownIt().render(longMarkdown);
console.timeEnd('markdown-it');  // ~45ms

console.time('micromark');
micromark(longMarkdown);
console.timeEnd('micromark');  // ~30ms (最快)

推荐选择

  • 小型项目:marked(简单,速度快)
  • 中型项目:markdown-it(可扩展性好)
  • 大型项目:micromark(最快的解析器)
  • SSR 应用:remark(流式处理)

缓存解析结果

javascript
const cache = new Map();

function cachedParse(markdown) {
  // 生成缓存键
  const key = hashString(markdown);
  
  // 检查缓存
  if (cache.has(key)) {
    return cache.get(key);
  }
  
  // 解析并缓存
  const result = marked.parse(markdown);
  cache.set(key, result);
  
  // 限制缓存大小
  if (cache.size > 1000) {
    const firstKey = cache.keys().next().value;
    cache.delete(firstKey);
  }
  
  return result;
}

增量解析

javascript
// 只重新解析修改的部分
class IncrementalParser {
  constructor() {
    this.parsedBlocks = new Map();
  }
  
  parse(markdown) {
    const blocks = this.splitIntoBlocks(markdown);
    const results = [];
    
    for (const block of blocks) {
      const hash = this.hashBlock(block);
      
      if (this.parsedBlocks.has(hash)) {
        results.push(this.parsedBlocks.get(hash));
      } else {
        const parsed = marked.parse(block);
        this.parsedBlocks.set(hash, parsed);
        results.push(parsed);
      }
    }
    
    return results.join('');
  }
  
  splitIntoBlocks(markdown) {
    return markdown.split(/\n\n+/);
  }
  
  hashBlock(block) {
    // 简单哈希函数
    let hash = 0;
    for (let i = 0; i < block.length; i++) {
      hash = ((hash << 5) - hash) + block.charCodeAt(i);
      hash |= 0;
    }
    return hash;
  }
}

Web Worker 异步解析

javascript
// main.js
const worker = new Worker('markdown-worker.js');

worker.postMessage({ markdown: document.getElementById('input').value });

worker.onmessage = (e) => {
  document.getElementById('output').innerHTML = e.data.html;
};

// markdown-worker.js
importScripts('https://cdn.jsdelivr.net/npm/marked/marked.min.js');

self.onmessage = (e) => {
  const html = marked.parse(e.data.markdown);
  self.postMessage({ html });
};

内容优化

图片优化

1. 压缩图片

markdown
<!-- 未优化:5MB PNG -->
![大图](huge-image.png)

<!-- 优化后:200KB WebP -->
![优化图片](optimized-image.webp)

<!-- 使用图片 CDN -->
![CDN图片](https://cdn.example.com/image.jpg?w=800&q=80)

2. 响应式图片

markdown
<picture>
  <source 
    srcset="image-mobile.webp" 
    media="(max-width: 768px)"
    type="image/webp">
  <source 
    srcset="image-desktop.webp" 
    media="(min-width: 769px)"
    type="image/webp">
  <img src="image-desktop.jpg" alt="描述" loading="lazy">
</picture>

3. 懒加载

markdown
<!-- 原生懒加载 -->
![懒加载图片](image.jpg){loading=lazy}

<!-- HTML 方式 -->
<img src="image.jpg" loading="lazy" alt="描述">

<!-- 使用 Intersection Observer -->
<img data-src="image.jpg" class="lazy" alt="描述">
<script>
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        observer.unobserve(img);
      }
    });
  });
  
  document.querySelectorAll('.lazy').forEach(img => observer.observe(img));
</script>

代码块优化

1. 按需加载语法高亮

javascript
// 动态加载 Prism 语言
async function highlightCode(code, language) {
  if (!Prism.languages[language]) {
    await import(`prismjs/components/prism-${language}`);
  }
  return Prism.highlight(code, Prism.languages[language], language);
}

2. 虚拟化长代码块

javascript
// 只渲染可见的代码行
class VirtualCodeBlock {
  constructor(lines, container) {
    this.lines = lines;
    this.container = container;
    this.visibleRange = { start: 0, end: 50 };
    
    this.render();
    this.setupScrollListener();
  }
  
  render() {
    const visible = this.lines.slice(
      this.visibleRange.start,
      this.visibleRange.end
    );
    
    this.container.innerHTML = visible
      .map((line, i) => `<div class="line">${i + 1}: ${line}</div>`)
      .join('');
  }
  
  setupScrollListener() {
    this.container.addEventListener('scroll', () => {
      // 更新可见范围并重新渲染
      this.updateVisibleRange();
      this.render();
    });
  }
}

字体优化

html
<!-- 使用系统字体栈 -->
<style>
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", 
               Roboto, "Helvetica Neue", Arial, sans-serif;
}

/* 预加载关键字体 */
<link rel="preload" href="fonts/custom-font.woff2" as="font" 
      type="font/woff2" crossorigin>

/* 使用 font-display */
@font-face {
  font-family: 'Custom Font';
  src: url('custom-font.woff2') format('woff2');
  font-display: swap;  /* 立即显示备用字体 */
}
</style>

渲染优化

虚拟滚动

javascript
// 只渲染可见的内容
class VirtualScroller {
  constructor(items, container, itemHeight) {
    this.items = items;
    this.container = container;
    this.itemHeight = itemHeight;
    this.scrollTop = 0;
    
    this.init();
  }
  
  init() {
    const totalHeight = this.items.length * this.itemHeight;
    this.container.style.height = `${totalHeight}px`;
    this.container.style.position = 'relative';
    
    this.container.addEventListener('scroll', () => {
      this.scrollTop = this.container.scrollTop;
      this.render();
    });
    
    this.render();
  }
  
  render() {
    const visibleStart = Math.floor(this.scrollTop / this.itemHeight);
    const visibleEnd = visibleStart + Math.ceil(window.innerHeight / this.itemHeight);
    
    const visibleItems = this.items.slice(visibleStart, visibleEnd);
    
    this.container.innerHTML = visibleItems
      .map((item, i) => {
        const index = visibleStart + i;
        const top = index * this.itemHeight;
        return `<div style="position: absolute; top: ${top}px;">${item}</div>`;
      })
      .join('');
  }
}

增量渲染

javascript
// 分批渲染大型文档
function incrementalRender(markdown, container) {
  const chunks = markdown.split(/\n#{1,6}\s/);  // 按标题分割
  let currentIndex = 0;
  
  function renderNextChunk() {
    if (currentIndex >= chunks.length) return;
    
    const chunk = chunks[currentIndex];
    const html = marked.parse(chunk);
    
    const div = document.createElement('div');
    div.innerHTML = html;
    container.appendChild(div);
    
    currentIndex++;
    
    // 使用 requestIdleCallback 在浏览器空闲时渲染
    if ('requestIdleCallback' in window) {
      requestIdleCallback(renderNextChunk);
    } else {
      setTimeout(renderNextChunk, 0);
    }
  }
  
  renderNextChunk();
}

服务端渲染 (SSR)

javascript
// Next.js 示例
export async function getStaticProps() {
  const markdown = await fs.readFile('content.md', 'utf-8');
  const html = marked.parse(markdown);
  
  return {
    props: { html },
    revalidate: 3600  // 每小时重新生成
  };
}

export default function Page({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

网络优化

CDN 加速

markdown
<!-- 使用 CDN 托管资源 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism.css">

<!-- 图片 CDN -->
![图片](https://cdn.example.com/image.jpg)

预加载和预连接

html
<!-- 预连接到关键域名 -->
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://analytics.example.com">

<!-- 预加载关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="marked.min.js" as="script">

<!-- 预取将来可能需要的资源 -->
<link rel="prefetch" href="next-page.html">

HTTP/2 和 HTTP/3

nginx
# Nginx 配置
server {
    listen 443 ssl http2;  # 启用 HTTP/2
    
    # HTTP/3 (QUIC)
    listen 443 http3 reuseport;
    add_header Alt-Svc 'h3=":443"; ma=86400';
}

资源压缩

javascript
// 启用 Gzip/Brotli 压缩
// Express.js 示例
const compression = require('compression');
app.use(compression({
  level: 6,
  threshold: 1024,
  filter: (req, res) => {
    if (req.headers['x-no-compression']) {
      return false;
    }
    return compression.filter(req, res);
  }
}));

构建优化

代码分割

javascript
// Webpack 配置
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        markdown: {
          test: /[\\/]markdown[\\/]/,
          name: 'markdown-parser',
          priority: 5
        }
      }
    }
  }
};

// 动态导入
const marked = await import('marked');

Tree Shaking

javascript
// 只导入需要的功能
import { parse } from 'marked';  // 而不是 import marked from 'marked'

// package.json
{
  "sideEffects": false  // 启用 tree shaking
}

最小化和混淆

javascript
// Terser 配置(Webpack 5 默认)
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,  // 移除 console.log
            passes: 2
          },
          format: {
            comments: false  // 移除注释
          }
        }
      })
    ]
  }
};

监控和分析

性能监控

javascript
// 使用 Performance API
const perfObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration}ms`);
  }
});

perfObserver.observe({ entryTypes: ['measure', 'navigation'] });

// 标记关键时间点
performance.mark('markdown-parse-start');
const html = marked.parse(markdown);
performance.mark('markdown-parse-end');

performance.measure(
  'markdown-parsing',
  'markdown-parse-start',
  'markdown-parse-end'
);

用户体验监控

javascript
// Core Web Vitals
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

getCLS(console.log);
getFID(console.log);
getFCP(console.log);
getLCP(console.log);
getTTFB(console.log);

// 发送到分析服务
function sendToAnalytics(metric) {
  const body = JSON.stringify(metric);
  const url = 'https://analytics.example.com/vitals';
  
  // 使用 sendBeacon 确保数据发送
  if (navigator.sendBeacon) {
    navigator.sendBeacon(url, body);
  } else {
    fetch(url, { method: 'POST', body });
  }
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getLCP(sendToAnalytics);

性能预算

javascript
// webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ],
  performance: {
    maxEntrypointSize: 250000,  // 250KB
    maxAssetSize: 100000,       // 100KB
    hints: 'error'
  }
};

缓存策略

浏览器缓存

html
<!-- Service Worker 缓存 -->
<script>
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}
</script>

<!-- sw.js -->
<script>
const CACHE_NAME = 'markdown-cache-v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/marked.min.js'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => response || fetch(event.request))
  );
});
</script>

内存缓存

javascript
// LRU 缓存实现
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  
  get(key) {
    if (!this.cache.has(key)) return null;
    
    // 移到最前面(最近使用)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }
  
  put(key, value) {
    if (this.cache.has(key)) {
      this.cache.delete(key);
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的项(第一项)
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, value);
  }
}

const markdownCache = new LRUCache(100);

最佳实践清单

内容层面

  • [ ] 优化图片大小和格式(WebP, AVIF)
  • [ ] 使用懒加载加载图片和视频
  • [ ] 压缩和最小化 CSS/JS
  • [ ] 使用系统字体或优化自定义字体
  • [ ] 限制第三方脚本

解析层面

  • [ ] 选择高性能的 Markdown 解析器
  • [ ] 实现解析结果缓存
  • [ ] 考虑使用 Web Worker
  • [ ] 增量解析大型文档

渲染层面

  • [ ] 虚拟滚动长列表
  • [ ] 增量渲染大型内容
  • [ ] 服务端渲染静态内容
  • [ ] 避免布局抖动

网络层面

  • [ ] 使用 CDN 分发资源
  • [ ] 启用 HTTP/2 或 HTTP/3
  • [ ] 配置资源压缩(Gzip/Brotli)
  • [ ] 实现预加载和预连接
  • [ ] 设置合适的缓存策略

构建层面

  • [ ] 代码分割和动态导入
  • [ ] Tree shaking 移除未使用代码
  • [ ] 最小化和混淆代码
  • [ ] 设置性能预算

监控层面

  • [ ] 监控 Core Web Vitals
  • [ ] 跟踪真实用户性能
  • [ ] 定期进行性能审计
  • [ ] 设置性能警报

工具推荐

性能测试

  • Lighthouse:全面的性能审计
  • WebPageTest:详细的性能分析
  • PageSpeed Insights:Google 的性能评分
  • Chrome DevTools:本地性能分析

监控服务

  • Google Analytics:用户行为分析
  • New Relic:应用性能监控
  • Datadog:实时监控
  • Sentry:错误跟踪

构建工具

  • Webpack Bundle Analyzer:分析包大小
  • Lighthouse CI:CI/CD 性能测试
  • size-limit:限制包大小

结论

Markdown 性能优化是一个持续的过程,需要在内容、解析、渲染和网络等多个层面进行优化。通过遵循最佳实践并持续监控,你可以确保为用户提供快速流畅的体验。

其他资源

由 Markdownlang.com 整理创建