Skip to content

HTML Layout Parser vs Browser Range API

When measuring text layout in web applications, developers typically use browser APIs like Range or getBoundingClientRect(). While these work, they have significant limitations that HTML Layout Parser solves.

The Problem with Browser APIs

1. Cross-Browser Inconsistency

Browser Range API produces different results across browsers:

typescript
// Same HTML, different browsers
const html = '<div style="font-size: 16px;">Hello World</div>';

// Chrome result
{ x: 8, y: 8, width: 66.5, height: 19 }

// Safari result  
{ x: 8, y: 8, width: 67.2, height: 19 }

// Firefox result
{ x: 8, y: 8, width: 66.8, height: 19 }

Why? Each browser has its own:

  • Text rendering engine
  • Font hinting algorithms
  • Sub-pixel rendering logic
  • Rounding strategies

2. Platform-Specific Differences

The same browser on different platforms produces different results:

typescript
// Chrome on macOS
{ width: 66.5 }

// Chrome on Windows
{ width: 67.1 }

// Chrome on Linux
{ width: 66.9 }

Why? Operating systems have different:

  • Font rendering systems (CoreText, DirectWrite, FreeType)
  • Anti-aliasing settings
  • DPI scaling

HTML Layout Parser Solution

100% Consistent Results

The parser produces identical results everywhere:

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

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

// Load font
const fontData = await loadFont('Arial.ttf');
parser.loadFont(fontData, 'Arial');

// Parse
const layouts = parser.parse(
  '<div style="font-size: 16px;">Hello World</div>',
  { viewportWidth: 800 }
);

// Result is IDENTICAL on:
// ✅ Chrome, Safari, Firefox
// ✅ Windows, macOS, Linux
// ✅ All browser versions
// ✅ Node.js server-side

How It Works

  1. Single Layout Engine: Uses litehtml (C++) compiled to WASM
  2. Embedded Font Metrics: Uses FreeType for consistent font rendering
  3. Deterministic Algorithm: Same input → same output, always
  4. No Browser Dependencies: Runs in WASM sandbox

Comparison Table

FeatureBrowser Range APIHTML Layout Parser
Cross-browser consistency❌ Different results✅ Identical results
Cross-platform consistency❌ Platform-dependent✅ Platform-independent
Version stability❌ Changes with updates✅ Stable across versions
Server-side support❌ Requires headless browser✅ Native Node.js support
Offline capability❌ Needs DOM✅ Works offline
Performance⚠️ DOM manipulation overhead✅ Fast WASM execution
Predictable testing❌ Flaky tests✅ Deterministic tests

Real-World Impact

Problem: Safari vs Chrome Differences

typescript
// Using Range API
function measureWithRange(text: string): number {
  const div = document.createElement('div');
  div.textContent = text;
  document.body.appendChild(div);
  
  const range = document.createRange();
  range.selectNodeContents(div);
  const width = range.getBoundingClientRect().width;
  
  document.body.removeChild(div);
  return width;
}

// Chrome: 150.5px
// Safari: 151.2px
// Difference: 0.7px (0.46%)

Impact: In a 1000-character document, this compounds to 7px difference - enough to cause:

  • Text overflow
  • Incorrect line breaks
  • Misaligned elements

Solution: Consistent Parsing

typescript
// Using HTML Layout Parser
const layouts = parser.parse(text, { viewportWidth: 800 });
const width = layouts[layouts.length - 1].x + layouts[layouts.length - 1].width;

// Chrome: 150.5px
// Safari: 150.5px
// Difference: 0px (0%)

Use Cases

✅ When to Use HTML Layout Parser

  1. Cross-platform applications

    • Desktop apps (Electron, Tauri)
    • Mobile apps (React Native, Capacitor)
    • Progressive Web Apps
  2. Server-side rendering

    • Generate images from HTML
    • PDF generation
    • Email templates with precise layout
  3. Canvas-based editors

    • Rich text editors
    • Diagram tools with text labels
    • Game UIs
  4. Automated testing

    • Visual regression tests
    • Layout validation
    • Screenshot comparison

⚠️ When Range API Might Be Sufficient

  1. Simple text measurement - Single-line text without complex layout
  2. Browser-only apps - No cross-platform requirements
  3. Approximate positioning - Pixel-perfect accuracy not needed
  4. Native DOM rendering - Content stays in DOM, not Canvas

Migration Example

Before: Range API

typescript
class TextMeasurer {
  measure(html: string): CharPosition[] {
    const container = document.createElement('div');
    container.innerHTML = html;
    document.body.appendChild(container);
    
    const positions: CharPosition[] = [];
    const walker = document.createTreeWalker(
      container,
      NodeFilter.SHOW_TEXT
    );
    
    let node;
    while (node = walker.nextNode()) {
      const text = node.textContent || '';
      for (let i = 0; i < text.length; i++) {
        const range = document.createRange();
        range.setStart(node, i);
        range.setEnd(node, i + 1);
        const rect = range.getBoundingClientRect();
        
        positions.push({
          char: text[i],
          x: rect.left,
          y: rect.top,
          width: rect.width,
          height: rect.height
        });
      }
    }
    
    document.body.removeChild(container);
    return positions;
  }
}

// ❌ Problems:
// - Different results on Chrome vs Safari
// - DOM manipulation overhead
// - Can't run server-side
// - Flaky in tests

After: HTML Layout Parser

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

class TextMeasurer {
  private parser: HtmlLayoutParser;
  
  async init() {
    this.parser = new HtmlLayoutParser();
    await this.parser.init();
    
    const fontData = await loadFont('Arial.ttf');
    this.parser.loadFont(fontData, 'Arial');
  }
  
  measure(html: string): CharPosition[] {
    const layouts = this.parser.parse(html, { 
      viewportWidth: 800 
    });
    
    return layouts.map(char => ({
      char: char.character,
      x: char.x,
      y: char.y,
      width: char.width,
      height: char.height
    }));
  }
}

// ✅ Benefits:
// - Identical results everywhere
// - No DOM manipulation
// - Works in Node.js
// - Deterministic tests

Performance Comparison

typescript
// Benchmark: Measure 1000 characters

// Range API
console.time('Range API');
const rangeResults = measureWithRange(longText);
console.timeEnd('Range API');
// Range API: 45ms (Chrome), 52ms (Safari)

// HTML Layout Parser
console.time('Parser');
const parserResults = parser.parse(longText, { viewportWidth: 800 });
console.timeEnd('Parser');
// Parser: 8ms (everywhere)

Result: Parser is 5-6x faster and consistent across browsers.

Testing Benefits

Before: Flaky Tests

typescript
// Test fails randomly on different browsers/platforms
test('text should fit in container', () => {
  const width = measureWithRange(text);
  expect(width).toBeLessThan(800);
  // ❌ Fails on Safari but passes on Chrome
});

After: Reliable Tests

typescript
test('text should fit in container', () => {
  const layouts = parser.parse(text, { viewportWidth: 800 });
  const width = getTextWidth(layouts);
  expect(width).toBeLessThan(800);
  // ✅ Always passes, everywhere
});

Conclusion

HTML Layout Parser solves the fundamental problem of cross-platform text measurement inconsistency. Compared to Range API, it uses a single WASM-based layout engine with embedded font metrics to provide:

  • 100% consistent results across all browsers and platforms
  • Faster performance than DOM-based measurement
  • Server-side support without headless browsers
  • Deterministic testing with no flaky tests
  • Future-proof - no dependency on browser updates

For applications that need precise, consistent text layout - especially Canvas-based rendering - HTML Layout Parser is the superior choice.

See Also

Released under the MIT License.