Skip to content

内存管理示例

演示正确的内存管理模式。

正确的加载/卸载模式

基本模式:加载一次,多次使用

typescript
// 从环境特定入口点导入
import { HtmlLayoutParser, CharLayout } from 'html-layout-parser/web';

// ✅ 正确:加载一次字体,用于多次解析
async function correctPattern() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    // 只加载一次字体
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    const fontId = parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(fontId);

    // 用于多次解析
    const documents = ['<div>文档 1</div>', '<div>文档 2</div>', '<div>文档 3</div>'];

    for (const html of documents) {
      const layouts = parser.parse(html, { viewportWidth: 800 });
      console.log(`解析了 ${layouts.length} 个字符`);
    }

  } finally {
    parser.destroy();
  }
}

// ❌ 错误:为每次解析加载/卸载字体
async function incorrectPattern() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    const documents = ['<div>文档 1</div>', '<div>文档 2</div>'];

    for (const html of documents) {
      // ❌ 错误:为每个文档加载字体
      const fontResponse = await fetch('/fonts/arial.ttf');
      const fontData = new Uint8Array(await fontResponse.arrayBuffer());
      const fontId = parser.loadFont(fontData, 'Arial');
      parser.setDefaultFont(fontId);

      const layouts = parser.parse(html, { viewportWidth: 800 });

      // ❌ 错误:立即卸载
      parser.unloadFont(fontId);
    }
  } finally {
    parser.destroy();
  }
}

内存监控

基础内存监控

typescript
import { HtmlLayoutParser, MemoryMetrics } from 'html-layout-parser/web';

function logMemoryMetrics(parser: HtmlLayoutParser): void {
  const metrics = parser.getMemoryMetrics();
  
  if (metrics) {
    const totalMB = (metrics.totalMemoryUsage / 1024 / 1024).toFixed(2);
    console.log(`总内存: ${totalMB} MB`);
    console.log(`字体数量: ${metrics.fontCount}`);
    
    for (const font of metrics.fonts) {
      const fontMB = (font.memoryUsage / 1024 / 1024).toFixed(2);
      console.log(`  - ${font.name} (ID: ${font.id}): ${fontMB} MB`);
    }
  }
}

async function memoryMonitoringExample() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  try {
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    console.log('=== 加载字体后 ===');
    logMemoryMetrics(parser);

    for (let i = 0; i < 100; i++) {
      parser.parse(`<div>文档 ${i}</div>`, { viewportWidth: 800 });
    }

    console.log('\n=== 解析 100 个文档后 ===');
    logMemoryMetrics(parser);

    if (parser.checkMemoryThreshold()) {
      console.warn('⚠️ 内存使用超过 50MB 阈值!');
    }

  } finally {
    parser.destroy();
  }
}

持续内存监控

typescript
import { HtmlLayoutParser, MemoryMetrics } from 'html-layout-parser/web';

class MemoryMonitor {
  private parser: HtmlLayoutParser;
  private intervalId: NodeJS.Timeout | null = null;
  private warningThresholdMB: number;
  private criticalThresholdMB: number;
  private onWarning?: (metrics: MemoryMetrics) => void;
  private onCritical?: (metrics: MemoryMetrics) => void;

  constructor(
    parser: HtmlLayoutParser,
    options: {
      warningThresholdMB?: number;
      criticalThresholdMB?: number;
      onWarning?: (metrics: MemoryMetrics) => void;
      onCritical?: (metrics: MemoryMetrics) => void;
    } = {}
  ) {
    this.parser = parser;
    this.warningThresholdMB = options.warningThresholdMB || 40;
    this.criticalThresholdMB = options.criticalThresholdMB || 50;
    this.onWarning = options.onWarning;
    this.onCritical = options.onCritical;
  }

  start(intervalMs: number = 5000): void {
    if (this.intervalId) return;
    this.intervalId = setInterval(() => this.check(), intervalMs);
  }

  stop(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }

  check(): MemoryMetrics | null {
    const metrics = this.parser.getMemoryMetrics();
    if (!metrics) return null;

    const usageMB = metrics.totalMemoryUsage / 1024 / 1024;

    if (usageMB >= this.criticalThresholdMB) {
      console.error(`🔴 严重: ${usageMB.toFixed(2)} MB`);
      this.onCritical?.(metrics);
    } else if (usageMB >= this.warningThresholdMB) {
      console.warn(`🟡 警告: ${usageMB.toFixed(2)} MB`);
      this.onWarning?.(metrics);
    }

    return metrics;
  }

  getStatus(): { usageMB: number; status: 'ok' | 'warning' | 'critical' } {
    const metrics = this.parser.getMemoryMetrics();
    const usageMB = metrics ? metrics.totalMemoryUsage / 1024 / 1024 : 0;

    let status: 'ok' | 'warning' | 'critical' = 'ok';
    if (usageMB >= this.criticalThresholdMB) status = 'critical';
    else if (usageMB >= this.warningThresholdMB) status = 'warning';

    return { usageMB, status };
  }
}

资源清理

使用 try/finally 保证清理

typescript
import { HtmlLayoutParser } from 'html-layout-parser/web';

// ✅ 正确:始终使用 try/finally 进行清理
async function guaranteedCleanup() {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();

    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    const layouts = parser.parse('<div>你好</div>', { viewportWidth: 800 });
    return layouts;

  } finally {
    // 这总是会运行,即使发生错误
    parser.destroy();
  }
}

// 包装函数确保清理
async function withParser<T>(
  fn: (parser: HtmlLayoutParser) => Promise<T>
): Promise<T> {
  const parser = new HtmlLayoutParser();
  
  try {
    await parser.init();
    return await fn(parser);
  } finally {
    parser.destroy();
  }
}

// 使用示例
async function cleanupWrapperExample() {
  const result = await withParser(async (parser) => {
    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    parser.loadFont(fontData, 'Arial');
    parser.setDefaultFont(1);

    return parser.parse('<div>你好</div>', { viewportWidth: 800 });
  });

  console.log(`解析了 ${result.length} 个字符`);
}

基于类的应用中的清理

typescript
import { HtmlLayoutParser, CharLayout } from 'html-layout-parser/web';

class DocumentRenderer {
  private parser: HtmlLayoutParser | null = null;
  private initialized = false;
  private destroyed = false;

  async init(): Promise<void> {
    if (this.initialized || this.destroyed) return;

    this.parser = new HtmlLayoutParser();
    await this.parser.init();

    const fontResponse = await fetch('/fonts/arial.ttf');
    const fontData = new Uint8Array(await fontResponse.arrayBuffer());
    this.parser.loadFont(fontData, 'Arial');
    this.parser.setDefaultFont(1);

    this.initialized = true;
  }

  render(html: string, css?: string): CharLayout[] {
    if (!this.parser || this.destroyed) {
      throw new Error('渲染器未初始化或已销毁');
    }

    return this.parser.parse(html, { viewportWidth: 800, css });
  }

  destroy(): void {
    if (this.destroyed) return;

    if (this.parser) {
      this.parser.destroy();
      this.parser = null;
    }

    this.initialized = false;
    this.destroyed = true;
  }
}

// 使用示例
async function classCleanupExample() {
  const renderer = new DocumentRenderer();

  try {
    await renderer.init();
    const layouts = renderer.render('<div>你好</div>');
    console.log(`渲染了 ${layouts.length} 个字符`);
  } finally {
    renderer.destroy();
  }
}

长期运行的应用

单例模式

typescript
import { HtmlLayoutParser, CharLayout, MemoryMetrics } from 'html-layout-parser/web';

class ParserSingleton {
  private static instance: ParserSingleton | null = null;
  private parser: HtmlLayoutParser;
  private initialized = false;
  private loadedFonts: Map<string, number> = new Map();

  private constructor() {
    this.parser = new HtmlLayoutParser();
  }

  static getInstance(): ParserSingleton {
    if (!ParserSingleton.instance) {
      ParserSingleton.instance = new ParserSingleton();
    }
    return ParserSingleton.instance;
  }

  async ensureInitialized(): Promise<void> {
    if (this.initialized) return;
    await this.parser.init();
    this.initialized = true;
  }

  async loadFont(fontData: Uint8Array, fontName: string): Promise<number> {
    await this.ensureInitialized();

    if (this.loadedFonts.has(fontName)) {
      return this.loadedFonts.get(fontName)!;
    }

    const fontId = this.parser.loadFont(fontData, fontName);
    if (fontId > 0) {
      this.loadedFonts.set(fontName, fontId);
    }
    return fontId;
  }

  setDefaultFont(fontName: string): boolean {
    const fontId = this.loadedFonts.get(fontName);
    if (fontId) {
      this.parser.setDefaultFont(fontId);
      return true;
    }
    return false;
  }

  parse(html: string, options: { viewportWidth: number; css?: string }): CharLayout[] {
    if (!this.initialized) throw new Error('解析器未初始化');
    return this.parser.parse(html, options);
  }

  performMaintenance(): void {
    if (this.parser.checkMemoryThreshold()) {
      console.warn('内存阈值已超过');
    }

    const metrics = this.parser.getMemoryMetrics();
    if (metrics) {
      console.log(`维护: ${metrics.fontCount} 个字体, ${(metrics.totalMemoryUsage / 1024 / 1024).toFixed(2)} MB`);
    }
  }

  static destroy(): void {
    if (ParserSingleton.instance) {
      ParserSingleton.instance.parser.destroy();
      ParserSingleton.instance.loadedFonts.clear();
      ParserSingleton.instance.initialized = false;
      ParserSingleton.instance = null;
    }
  }
}

常见错误避免

typescript
import { HtmlLayoutParser } from 'html-layout-parser/web';

// ❌ 错误 1:忘记销毁
async function mistake1() {
  const parser = new HtmlLayoutParser();
  await parser.init();
  const layouts = parser.parse('<div>你好</div>', { viewportWidth: 800 });
  return layouts;
  // 解析器从未销毁 - 内存泄漏!
}

// ❌ 错误 2:多次加载相同字体
async function mistake2() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
  
  // 加载相同字体 3 次 - 浪费内存!
  parser.loadFont(fontData, 'Arial');
  parser.loadFont(fontData, 'Arial');
  parser.loadFont(fontData, 'Arial');

  parser.destroy();
}

// ❌ 错误 3:不处理错误
async function mistake3() {
  const parser = new HtmlLayoutParser();
  await parser.init();

  // 如果 fetch 失败,解析器永远不会被销毁
  const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
  parser.loadFont(fontData, 'Arial');
  
  parser.destroy();
}

// ❌ 错误 4:销毁后使用解析器
async function mistake4() {
  const parser = new HtmlLayoutParser();
  await parser.init();
  
  parser.destroy();
  
  // 这会失败!
  const layouts = parser.parse('<div>你好</div>', { viewportWidth: 800 });
}

// ✅ 正确:所有错误已修复
async function correct() {
  const parser = new HtmlLayoutParser();
  const loadedFonts = new Map<string, number>();

  try {
    await parser.init();

    try {
      const fontData = new Uint8Array(await (await fetch('/fonts/arial.ttf')).arrayBuffer());
      
      // 检查是否已加载
      if (!loadedFonts.has('Arial')) {
        const fontId = parser.loadFont(fontData, 'Arial');
        if (fontId > 0) {
          loadedFonts.set('Arial', fontId);
          parser.setDefaultFont(fontId);
        }
      }
    } catch (error) {
      console.error('加载字体失败:', error);
    }

    const layouts = parser.parse('<div>你好</div>', { viewportWidth: 800 });
    return layouts;

  } finally {
    parser.destroy();
  }
}

Released under the MIT License.