Back to Blog

Google Docs Frontend System Architecture: Real-Time Collaboration at Scale

1. Product Overview & Scale

Google Docs pioneered browser-based document editing and remains the gold standard for real-time collaboration. The frontend must handle simultaneous editing by hundreds of users while maintaining document fidelity, sub-100ms synchronization, and pixel-perfect rendering.

Scale Metrics:

  • 1+ billion monthly active users across Google Workspace
  • Millions of concurrent editing sessions
  • Documents ranging from 1 page to 1,000+ pages
  • 100+ simultaneous editors per document (enterprise)
  • Sub-100ms edit propagation latency target
  • 99.9% sync consistency requirement
  • Offline-first with seamless sync
  • 40+ export formats supported

The technical challenge is unprecedented: multiple people typing in the same sentence simultaneously must result in a coherent, identical document for everyone—in real-time.

┌─────────────────────────────────────────────────────────────────────────────┐
│                    GOOGLE DOCS COLLABORATION COMPLEXITY                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  SCENARIO: 3 users editing the same paragraph simultaneously               │
│                                                                             │
│  Time 0ms:  Document state: "The quick brown fox"                          │
│             │                                                               │
│  User A     │    User B           User C                                   │
│  (Seattle)  │    (London)         (Tokyo)                                  │
│      │      │        │                │                                    │
│      ▼      │        ▼                ▼                                    │
│  Types      │    Types            Types                                    │
│  "very "    │    "lazy "          "jumps"                                  │
│  at pos 4   │    at pos 16        at end                                   │
│             │                                                               │
│  Time 50ms: Operations arrive at server in different orders                │
│             Server must transform operations to maintain consistency       │
│             │                                                               │
│  Time 100ms: All clients converge to same state:                           │
│              "The very quick brown lazy fox jumps"                         │
│                                                                             │
│  CHALLENGE: Network latency varies (10ms to 500ms+)                        │
│             Operations can arrive out of order                             │
│             Must handle conflicts without data loss                        │
│             Must feel instant to each user                                 │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

2. Core Frontend Challenges

The Real-Time Collaboration Problem

Unlike most web applications, Google Docs must solve a distributed systems problem in the browser: multiple writers, single document, eventual consistency.

┌─────────────────────────────────────────────────────────────────────────────┐
│                      COLLABORATION CHALLENGES                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. CONFLICT RESOLUTION                                                    │
│     Two users type at the same position simultaneously                     │
│     → Operational Transformation (OT) resolves conflicts                   │
│                                                                             │
│  2. LATENCY HIDING                                                         │
│     User types, sees result immediately                                    │
│     Server confirmation comes 100-500ms later                              │
│     → Optimistic updates with rollback capability                          │
│                                                                             │
│  3. CURSOR SYNCHRONIZATION                                                 │
│     Each user's cursor position must be visible to others                  │
│     Cursors move as document content changes                               │
│     → Position transformation relative to edits                            │
│                                                                             │
│  4. OFFLINE SUPPORT                                                        │
│     User edits while disconnected                                          │
│     Reconnects with potentially conflicting changes                        │
│     → Operation queue + conflict resolution on reconnect                   │
│                                                                             │
│  5. LARGE DOCUMENT PERFORMANCE                                             │
│     1000+ page documents must remain responsive                            │
│     → Virtualization + incremental rendering                               │
│                                                                             │
│  6. RICH CONTENT                                                           │
│     Text, images, tables, drawings, charts, equations                      │
│     Each has different editing semantics                                   │
│     → Unified operation model across content types                         │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

The Six Core Challenges

ChallengeImpactComplexity
Operational TransformationCore of collaboration - transforms concurrent editsExtreme - mathematical correctness required
Canvas RenderingPixel-perfect document layout, custom text renderingHigh - reimplementing browser typography
Offline SyncEdit without network, merge on reconnectHigh - conflict resolution at scale
Large Documents1000+ pages must be editableHigh - virtualization, lazy loading
Undo/RedoPer-user history in collaborative contextHigh - tracking ownership of operations
AccessibilityScreen readers with custom renderingCritical - must expose semantic structure

3. High-Level Frontend Architecture

Google Docs uses a sophisticated architecture that diverges significantly from typical web applications:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    GOOGLE DOCS ARCHITECTURE                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      COLLABORATION SERVER                            │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐  │   │
│  │  │  Operation  │  │  Transform  │  │  Document   │  │  Presence  │  │   │
│  │  │  Queue      │  │  Engine     │  │  State      │  │  Service   │  │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                      │                                      │
│                          WebSocket / HTTP2                                  │
│                                      │                                      │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                        CLIENT APPLICATION                            │   │
│  │                                                                       │   │
│  │  ┌─────────────────────────────────────────────────────────────────┐ │   │
│  │  │                    COLLABORATION LAYER                           │ │   │
│  │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │ │   │
│  │  │  │ Local OT │  │ Op Queue │  │ Conflict │  │ Cursor Manager   │ │ │   │
│  │  │  │ Engine   │  │          │  │ Resolver │  │                  │ │ │   │
│  │  │  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │ │   │
│  │  └─────────────────────────────────────────────────────────────────┘ │   │
│  │                                                                       │   │
│  │  ┌─────────────────────────────────────────────────────────────────┐ │   │
│  │  │                     DOCUMENT MODEL                               │ │   │
│  │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │ │   │
│  │  │  │  Tree    │  │ Segments │  │ Styling  │  │ Comments/        │ │ │   │
│  │  │  │ Structure│  │ (Runs)   │  │ Resolver │  │ Suggestions      │ │ │   │
│  │  │  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │ │   │
│  │  └─────────────────────────────────────────────────────────────────┘ │   │
│  │                                                                       │   │
│  │  ┌─────────────────────────────────────────────────────────────────┐ │   │
│  │  │                    RENDERING ENGINE                              │ │   │
│  │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │ │   │
│  │  │  │  Layout  │  │  Canvas  │  │  Text    │  │ Viewport         │ │ │   │
│  │  │  │  Engine  │  │  Painter │  │ Shaper   │  │ Manager          │ │ │   │
│  │  │  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │ │   │
│  │  └─────────────────────────────────────────────────────────────────┘ │   │
│  │                                                                       │   │
│  │  ┌─────────────────────────────────────────────────────────────────┐ │   │
│  │  │                      INPUT HANDLING                              │ │   │
│  │  │  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │ │   │
│  │  │  │ Keyboard │  │  IME     │  │ Selection│  │ Clipboard        │ │ │   │
│  │  │  │ Handler  │  │ Handler  │  │ Manager  │  │ Handler          │ │ │   │
│  │  │  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │ │   │
│  │  └─────────────────────────────────────────────────────────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Canvas-Based Rendering

In 2021, Google Docs switched from DOM-based rendering to canvas-based rendering. This was a monumental architectural change:

// Why Canvas over DOM?

// DOM Rendering (Old Approach)
// - Each character/run is a DOM element
// - Browser handles text layout
// - Limited control over rendering
// - Performance degrades with document size
// - Accessibility built-in

// Canvas Rendering (New Approach)
// - Full control over pixel placement
// - Consistent rendering across browsers
// - Better performance for large documents
// - Must implement text shaping manually
// - Must implement accessibility layer manually

interface CanvasRenderingAdvantages {
  performance: {
    // DOM: O(n) elements for n characters
    // Canvas: O(1) canvas element, render visible portion only
    largeDocuments: 'Significantly faster',
    scrolling: '60fps even on 1000+ pages',
    selection: 'Custom optimized hit testing',
  };

  consistency: {
    // DOM: Browser differences in text rendering
    // Canvas: Pixel-identical across all browsers
    crossBrowser: 'Identical layout everywhere',
    printing: 'WYSIWYG guaranteed',
  };

  features: {
    // DOM: Limited by CSS/HTML capabilities
    // Canvas: Full control
    advancedTypography: 'Custom kerning, ligatures',
    complexLayouts: 'Columns, text wrapping around images',
  };
}

4. Operational Transformation Deep Dive

Operational Transformation (OT) is the mathematical foundation enabling real-time collaboration:

┌─────────────────────────────────────────────────────────────────────────────┐
│                    OPERATIONAL TRANSFORMATION                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  CORE CONCEPT: Transform operations so they can be applied in any order    │
│                and produce the same final document state                   │
│                                                                             │
│  EXAMPLE:                                                                  │
│  ─────────                                                                 │
│  Initial document: "ABCD"                                                  │
│                                                                             │
│  User 1: Insert 'X' at position 1 → Op1: insert(1, 'X')                   │
│  User 2: Insert 'Y' at position 3 → Op2: insert(3, 'Y')                   │
│                                                                             │
│  If applied as-is to different clients:                                    │
│  Client 1: "ABCD" → "AXBCD" → "AXBYCD"  (Op1 then Op2)                    │
│  Client 2: "ABCD" → "ABYCD" → "AXBYCD"  (Op2 then Op1, but Op1 needs      │
│                                          transformation!)                  │
│                                                                             │
│  TRANSFORMATION:                                                           │
│  When Op1 is applied first, Op2's position must shift:                     │
│  Op2' = insert(4, 'Y')  // Position increased by 1                         │
│                                                                             │
│  When Op2 is applied first, Op1 stays the same:                            │
│  Op1' = insert(1, 'X')  // Position before Op2's insert                    │
│                                                                             │
│  Both orderings now produce: "AXBYCD" ✓                                    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

OT Implementation

// Operation types for text documents
type Operation =
  | { type: 'insert'; position: number; content: string; attributes?: TextAttributes }
  | { type: 'delete'; position: number; length: number }
  | { type: 'retain'; count: number; attributes?: TextAttributes }
  | { type: 'format'; position: number; length: number; attributes: TextAttributes };

// Transform function: transform op2 against op1
// Returns op2' that can be applied after op1 to achieve same result
function transform(op1: Operation, op2: Operation): Operation {
  // Insert vs Insert
  if (op1.type === 'insert' && op2.type === 'insert') {
    if (op1.position <= op2.position) {
      // op1 inserts before or at op2's position
      // Shift op2's position by length of op1's content
      return {
        ...op2,
        position: op2.position + op1.content.length,
      };
    } else {
      // op1 inserts after op2's position - no change needed
      return op2;
    }
  }

  // Insert vs Delete
  if (op1.type === 'insert' && op2.type === 'delete') {
    if (op1.position <= op2.position) {
      // Insert before delete - shift delete position
      return {
        ...op2,
        position: op2.position + op1.content.length,
      };
    } else if (op1.position >= op2.position + op2.length) {
      // Insert after delete range - no change
      return op2;
    } else {
      // Insert within delete range - split delete
      return {
        type: 'delete',
        position: op2.position,
        length: op2.length + op1.content.length,
      };
    }
  }

  // Delete vs Insert
  if (op1.type === 'delete' && op2.type === 'insert') {
    if (op1.position + op1.length <= op2.position) {
      // Delete before insert - shift insert position back
      return {
        ...op2,
        position: op2.position - op1.length,
      };
    } else if (op1.position >= op2.position) {
      // Delete after insert - no change
      return op2;
    } else {
      // Delete overlaps insert position
      return {
        ...op2,
        position: op1.position,
      };
    }
  }

  // Delete vs Delete
  if (op1.type === 'delete' && op2.type === 'delete') {
    if (op1.position + op1.length <= op2.position) {
      // op1 deletes before op2 - shift position
      return {
        ...op2,
        position: op2.position - op1.length,
      };
    } else if (op1.position >= op2.position + op2.length) {
      // op1 deletes after op2 - no change
      return op2;
    } else {
      // Overlapping deletes - complex case
      return transformOverlappingDeletes(op1, op2);
    }
  }

  return op2;
}

// Handle overlapping delete operations
function transformOverlappingDeletes(
  op1: DeleteOperation,
  op2: DeleteOperation
): DeleteOperation {
  const start1 = op1.position;
  const end1 = op1.position + op1.length;
  const start2 = op2.position;
  const end2 = op2.position + op2.length;

  // Calculate the portion of op2 that wasn't deleted by op1
  if (start2 >= start1 && end2 <= end1) {
    // op2 is completely within op1 - nothing left to delete
    return { type: 'delete', position: start1, length: 0 };
  }

  if (start2 < start1 && end2 > end1) {
    // op2 spans op1 - reduce length by op1's length
    return {
      type: 'delete',
      position: start2,
      length: op2.length - op1.length,
    };
  }

  if (start2 < start1) {
    // op2 starts before op1 - truncate end
    return {
      type: 'delete',
      position: start2,
      length: start1 - start2,
    };
  }

  // op2 starts within op1 - shift to op1's start and reduce length
  return {
    type: 'delete',
    position: start1,
    length: end2 - end1,
  };
}

Client-Side OT Engine

// Client OT engine managing local and remote operations

interface OTState {
  // Acknowledged document state (server confirmed)
  serverState: DocumentState;
  serverRevision: number;

  // Local operations not yet acknowledged
  pendingOperations: Operation[];

  // Operations sent but not acknowledged
  inflightOperations: Operation[];

  // Current document state (server + pending + inflight)
  localState: DocumentState;
}

class OTEngine {
  private state: OTState;
  private socket: WebSocket;
  private operationBuffer: Operation[] = [];
  private sendTimeout: number | null = null;

  constructor(initialState: DocumentState, socket: WebSocket) {
    this.state = {
      serverState: initialState,
      serverRevision: 0,
      pendingOperations: [],
      inflightOperations: [],
      localState: initialState,
    };
    this.socket = socket;
    this.setupSocketHandlers();
  }

  // Apply local operation (user edit)
  applyLocal(operation: Operation): void {
    // Apply to local state immediately (optimistic)
    this.state.localState = applyOperation(this.state.localState, operation);

    // Queue for sending
    this.state.pendingOperations.push(operation);

    // Debounce sending to batch rapid edits
    this.scheduleSend();

    // Emit local change event
    this.emit('localChange', this.state.localState);
  }

  // Handle operation from server (remote edit)
  handleRemote(serverOp: Operation, revision: number): void {
    // Transform against inflight operations
    let transformedOp = serverOp;
    for (const inflightOp of this.state.inflightOperations) {
      transformedOp = transform(inflightOp, transformedOp);
    }

    // Transform against pending operations
    for (const pendingOp of this.state.pendingOperations) {
      transformedOp = transform(pendingOp, transformedOp);
    }

    // Apply transformed operation to local state
    this.state.localState = applyOperation(this.state.localState, transformedOp);
    this.state.serverRevision = revision;

    // Emit remote change event
    this.emit('remoteChange', transformedOp, this.state.localState);
  }

  // Handle acknowledgment from server
  handleAck(revision: number): void {
    // Move inflight to acknowledged
    const ackedOps = this.state.inflightOperations;
    this.state.inflightOperations = [];

    // Apply to server state
    for (const op of ackedOps) {
      this.state.serverState = applyOperation(this.state.serverState, op);
    }

    this.state.serverRevision = revision;

    // Send pending operations if any
    this.sendPending();
  }

  private scheduleSend(): void {
    if (this.sendTimeout !== null) return;

    // Batch operations for 50ms
    this.sendTimeout = window.setTimeout(() => {
      this.sendTimeout = null;
      this.sendPending();
    }, 50);
  }

  private sendPending(): void {
    if (this.state.pendingOperations.length === 0) return;
    if (this.state.inflightOperations.length > 0) return; // Wait for ack

    // Compose pending operations into single operation
    const composed = composeOperations(this.state.pendingOperations);

    // Move to inflight
    this.state.inflightOperations = this.state.pendingOperations;
    this.state.pendingOperations = [];

    // Send to server
    this.socket.send(JSON.stringify({
      type: 'operation',
      operation: composed,
      revision: this.state.serverRevision,
    }));
  }

  private setupSocketHandlers(): void {
    this.socket.onmessage = (event) => {
      const message = JSON.parse(event.data);

      switch (message.type) {
        case 'operation':
          this.handleRemote(message.operation, message.revision);
          break;
        case 'ack':
          this.handleAck(message.revision);
          break;
        case 'presence':
          this.emit('presence', message.users);
          break;
      }
    };
  }
}

// Compose multiple operations into one
function composeOperations(operations: Operation[]): Operation {
  if (operations.length === 0) {
    return { type: 'retain', count: 0 };
  }

  let composed = operations[0];
  for (let i = 1; i < operations.length; i++) {
    composed = compose(composed, operations[i]);
  }

  return composed;
}

5. Document Model Architecture

Google Docs uses a sophisticated document model that supports rich formatting:

// Document tree structure

interface Document {
  id: string;
  title: string;
  body: Body;
  headers: Record<string, Header>;
  footers: Record<string, Footer>;
  footnotes: Record<string, Footnote>;
  styles: DocumentStyles;
  namedRanges: Record<string, NamedRange>;
  suggestions: Record<string, Suggestion>;
  revisionHistory: RevisionHistory;
}

interface Body {
  content: StructuralElement[];
}

type StructuralElement =
  | Paragraph
  | Table
  | TableOfContents
  | SectionBreak;

interface Paragraph {
  type: 'paragraph';
  elements: ParagraphElement[];
  paragraphStyle: ParagraphStyle;
  bullet?: Bullet;
}

type ParagraphElement =
  | TextRun
  | InlineObjectElement
  | AutoText
  | PageBreak
  | ColumnBreak
  | FootnoteReference
  | HorizontalRule
  | Equation
  | Person
  | RichLink;

interface TextRun {
  type: 'textRun';
  content: string;
  textStyle: TextStyle;
  suggestedTextStyleChanges?: Record<string, TextStyle>;
  suggestedInsertionIds?: string[];
  suggestedDeletionIds?: string[];
}

interface TextStyle {
  bold?: boolean;
  italic?: boolean;
  underline?: boolean;
  strikethrough?: boolean;
  smallCaps?: boolean;
  fontSize?: Dimension;
  fontFamily?: string;
  foregroundColor?: Color;
  backgroundColor?: Color;
  link?: Link;
  baselineOffset?: 'NONE' | 'SUPERSCRIPT' | 'SUBSCRIPT';
  weightedFontFamily?: WeightedFontFamily;
}

interface ParagraphStyle {
  headingId?: string;
  namedStyleType?: NamedStyleType;
  alignment?: Alignment;
  lineSpacing?: number;
  direction?: 'LEFT_TO_RIGHT' | 'RIGHT_TO_LEFT';
  spacingMode?: 'NEVER_COLLAPSE' | 'COLLAPSE_LISTS';
  spaceAbove?: Dimension;
  spaceBelow?: Dimension;
  borderBetween?: ParagraphBorder;
  borderTop?: ParagraphBorder;
  borderBottom?: ParagraphBorder;
  borderLeft?: ParagraphBorder;
  borderRight?: ParagraphBorder;
  indentFirstLine?: Dimension;
  indentStart?: Dimension;
  indentEnd?: Dimension;
  tabStops?: TabStop[];
  keepLinesTogether?: boolean;
  keepWithNext?: boolean;
  avoidWidowAndOrphan?: boolean;
  shading?: Shading;
}

// Document model manager
class DocumentModel {
  private document: Document;
  private listeners: Set<(doc: Document) => void> = new Set();

  constructor(initialDoc: Document) {
    this.document = initialDoc;
  }

  // Apply operation to document
  applyOperation(operation: Operation): void {
    switch (operation.type) {
      case 'insert':
        this.insertContent(operation.position, operation.content, operation.attributes);
        break;
      case 'delete':
        this.deleteContent(operation.position, operation.length);
        break;
      case 'format':
        this.formatContent(operation.position, operation.length, operation.attributes);
        break;
      case 'retain':
        // No-op for document model, but may carry attributes
        if (operation.attributes) {
          this.formatContent(0, operation.count, operation.attributes);
        }
        break;
    }

    this.notifyListeners();
  }

  // Get content at position with resolved styles
  getContentAt(position: number): ResolvedContent {
    const { element, offset } = this.findElementAtPosition(position);

    if (element.type === 'textRun') {
      return {
        type: 'text',
        character: element.content[offset],
        style: this.resolveTextStyle(element),
        paragraphStyle: this.getParagraphStyleForElement(element),
      };
    }

    return { type: element.type, element };
  }

  // Find element at document position
  private findElementAtPosition(position: number): {
    element: ParagraphElement;
    offset: number;
    paragraph: Paragraph;
  } {
    let currentPosition = 0;

    for (const structural of this.document.body.content) {
      if (structural.type === 'paragraph') {
        for (const element of structural.elements) {
          const elementLength = this.getElementLength(element);

          if (currentPosition + elementLength > position) {
            return {
              element,
              offset: position - currentPosition,
              paragraph: structural,
            };
          }

          currentPosition += elementLength;
        }
      }
    }

    throw new Error(`Position ${position} out of bounds`);
  }

  private getElementLength(element: ParagraphElement): number {
    switch (element.type) {
      case 'textRun':
        return element.content.length;
      case 'pageBreak':
      case 'columnBreak':
      case 'footnoteReference':
      case 'horizontalRule':
        return 1;
      case 'inlineObjectElement':
        return 1;
      default:
        return 0;
    }
  }

  private resolveTextStyle(textRun: TextRun): ResolvedTextStyle {
    // Start with document default style
    let resolved = { ...this.document.styles.defaultTextStyle };

    // Apply named style if paragraph has one
    // Apply direct formatting
    resolved = { ...resolved, ...textRun.textStyle };

    return resolved;
  }

  private notifyListeners(): void {
    for (const listener of this.listeners) {
      listener(this.document);
    }
  }
}

6. Canvas Rendering Engine

The rendering engine is responsible for converting the document model into pixels:

// Layout and rendering pipeline

interface LayoutEngine {
  // Input: Document model
  // Output: Layout tree with positioned elements
  layout(document: Document, viewport: Viewport): LayoutTree;
}

interface RenderEngine {
  // Input: Layout tree
  // Output: Pixels on canvas
  render(layoutTree: LayoutTree, ctx: CanvasRenderingContext2D): void;
}

// Layout tree structure
interface LayoutTree {
  pages: PageLayout[];
  totalHeight: number;
  totalWidth: number;
}

interface PageLayout {
  pageNumber: number;
  bounds: Rectangle;
  columns: ColumnLayout[];
  header?: HeaderLayout;
  footer?: FooterLayout;
  marginBoxes: MarginBox[];
}

interface ColumnLayout {
  bounds: Rectangle;
  lines: LineLayout[];
}

interface LineLayout {
  bounds: Rectangle;
  baseline: number;
  runs: RunLayout[];
  lineHeight: number;
  alignment: Alignment;
}

interface RunLayout {
  bounds: Rectangle;
  content: string;
  style: ResolvedTextStyle;
  glyphs: GlyphLayout[];
  // For hit testing and selection
  characterOffsets: number[];
}

interface GlyphLayout {
  glyph: number; // Glyph ID in font
  x: number;
  y: number;
  width: number;
  height: number;
}

// Text shaping and layout
class TextLayoutEngine {
  private fontCache: Map<string, Font> = new Map();
  private measurementCanvas: OffscreenCanvas;
  private measurementCtx: OffscreenCanvasRenderingContext2D;

  constructor() {
    this.measurementCanvas = new OffscreenCanvas(1, 1);
    this.measurementCtx = this.measurementCanvas.getContext('2d')!;
  }

  // Layout a paragraph into lines
  layoutParagraph(
    paragraph: Paragraph,
    availableWidth: number,
    startY: number
  ): ParagraphLayout {
    const lines: LineLayout[] = [];
    let currentLine: RunLayout[] = [];
    let currentLineWidth = 0;
    let y = startY;

    for (const element of paragraph.elements) {
      if (element.type === 'textRun') {
        const runs = this.layoutTextRun(element, availableWidth - currentLineWidth);

        for (const run of runs) {
          if (currentLineWidth + run.bounds.width > availableWidth && currentLine.length > 0) {
            // Line break needed
            lines.push(this.finalizeLine(currentLine, y, availableWidth, paragraph.paragraphStyle));
            y += this.getLineHeight(currentLine, paragraph.paragraphStyle);
            currentLine = [];
            currentLineWidth = 0;
          }

          currentLine.push(run);
          currentLineWidth += run.bounds.width;
        }
      } else if (element.type === 'inlineObjectElement') {
        const objectRun = this.layoutInlineObject(element);
        currentLine.push(objectRun);
        currentLineWidth += objectRun.bounds.width;
      }
    }

    // Finalize last line
    if (currentLine.length > 0) {
      lines.push(this.finalizeLine(currentLine, y, availableWidth, paragraph.paragraphStyle));
    }

    return {
      bounds: {
        x: 0,
        y: startY,
        width: availableWidth,
        height: y - startY + this.getLineHeight(currentLine, paragraph.paragraphStyle),
      },
      lines,
    };
  }

  // Layout a text run, potentially breaking into multiple runs
  private layoutTextRun(textRun: TextRun, availableWidth: number): RunLayout[] {
    const runs: RunLayout[] = [];
    const font = this.getFont(textRun.textStyle);

    // Shape text (convert to glyphs)
    const shapedText = this.shapeText(textRun.content, font, textRun.textStyle);

    let currentRun: GlyphLayout[] = [];
    let currentWidth = 0;
    let wordStart = 0;

    for (let i = 0; i < shapedText.glyphs.length; i++) {
      const glyph = shapedText.glyphs[i];

      // Check for word boundary
      if (textRun.content[i] === ' ' || i === shapedText.glyphs.length - 1) {
        // End of word
        if (currentWidth + glyph.width > availableWidth && currentRun.length > 0) {
          // Need to break before this word
          runs.push(this.createRunFromGlyphs(
            currentRun,
            textRun.content.substring(wordStart, i),
            textRun.textStyle
          ));
          currentRun = [];
          currentWidth = 0;
          wordStart = i;
        }
      }

      currentRun.push(glyph);
      currentWidth += glyph.width;
    }

    // Add remaining glyphs
    if (currentRun.length > 0) {
      runs.push(this.createRunFromGlyphs(
        currentRun,
        textRun.content.substring(wordStart),
        textRun.textStyle
      ));
    }

    return runs;
  }

  // Shape text using HarfBuzz-like algorithm
  private shapeText(text: string, font: Font, style: TextStyle): ShapedText {
    const glyphs: GlyphLayout[] = [];
    let x = 0;

    for (let i = 0; i < text.length; i++) {
      const char = text[i];
      const glyphId = font.charToGlyph(char);
      const metrics = font.getGlyphMetrics(glyphId);

      // Apply kerning
      let kerning = 0;
      if (i > 0) {
        kerning = font.getKerning(text.charCodeAt(i - 1), text.charCodeAt(i));
      }

      glyphs.push({
        glyph: glyphId,
        x: x + kerning,
        y: 0, // Baseline relative
        width: metrics.advanceWidth,
        height: metrics.height,
      });

      x += metrics.advanceWidth + kerning;
    }

    return { glyphs, width: x };
  }

  private finalizeLine(
    runs: RunLayout[],
    y: number,
    availableWidth: number,
    style: ParagraphStyle
  ): LineLayout {
    const lineWidth = runs.reduce((sum, run) => sum + run.bounds.width, 0);
    const lineHeight = this.getLineHeight(runs, style);
    const baseline = this.getBaseline(runs, style);

    // Apply alignment
    let xOffset = 0;
    switch (style.alignment) {
      case 'CENTER':
        xOffset = (availableWidth - lineWidth) / 2;
        break;
      case 'END':
        xOffset = availableWidth - lineWidth;
        break;
      case 'JUSTIFIED':
        // Distribute space between words
        this.justifyLine(runs, availableWidth - lineWidth);
        break;
    }

    // Position runs
    let x = xOffset;
    for (const run of runs) {
      run.bounds.x = x;
      run.bounds.y = y;
      x += run.bounds.width;
    }

    return {
      bounds: { x: 0, y, width: availableWidth, height: lineHeight },
      baseline,
      runs,
      lineHeight,
      alignment: style.alignment || 'START',
    };
  }
}

// Canvas rendering
class CanvasRenderer {
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  private dpr: number;

  constructor(canvas: HTMLCanvasElement) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d')!;
    this.dpr = window.devicePixelRatio || 1;

    // Set up high-DPI rendering
    this.setupHighDPI();
  }

  private setupHighDPI(): void {
    const rect = this.canvas.getBoundingClientRect();
    this.canvas.width = rect.width * this.dpr;
    this.canvas.height = rect.height * this.dpr;
    this.ctx.scale(this.dpr, this.dpr);
  }

  // Render visible portion of document
  render(layoutTree: LayoutTree, scrollY: number, viewportHeight: number): void {
    // Clear canvas
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    // Find visible pages
    const visiblePages = this.getVisiblePages(layoutTree, scrollY, viewportHeight);

    for (const page of visiblePages) {
      this.renderPage(page, scrollY);
    }
  }

  private renderPage(page: PageLayout, scrollY: number): void {
    const offsetY = page.bounds.y - scrollY;

    // Draw page background
    this.ctx.fillStyle = '#ffffff';
    this.ctx.fillRect(page.bounds.x, offsetY, page.bounds.width, page.bounds.height);

    // Draw page shadow
    this.ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
    this.ctx.shadowBlur = 10;
    this.ctx.shadowOffsetY = 2;
    this.ctx.fillRect(page.bounds.x, offsetY, page.bounds.width, page.bounds.height);
    this.ctx.shadowColor = 'transparent';

    // Render columns
    for (const column of page.columns) {
      this.renderColumn(column, offsetY);
    }

    // Render header/footer
    if (page.header) {
      this.renderHeader(page.header, offsetY);
    }
    if (page.footer) {
      this.renderFooter(page.footer, offsetY + page.bounds.height);
    }
  }

  private renderColumn(column: ColumnLayout, pageOffsetY: number): void {
    for (const line of column.lines) {
      this.renderLine(line, pageOffsetY);
    }
  }

  private renderLine(line: LineLayout, pageOffsetY: number): void {
    for (const run of line.runs) {
      this.renderRun(run, line.baseline, pageOffsetY);
    }
  }

  private renderRun(run: RunLayout, baseline: number, pageOffsetY: number): void {
    const { style, content, bounds, glyphs } = run;

    // Set font
    this.ctx.font = this.buildFontString(style);
    this.ctx.fillStyle = style.foregroundColor?.rgbColor
      ? `rgb(${style.foregroundColor.rgbColor.red * 255}, ${style.foregroundColor.rgbColor.green * 255}, ${style.foregroundColor.rgbColor.blue * 255})`
      : '#000000';

    // Draw background if present
    if (style.backgroundColor) {
      this.ctx.fillStyle = this.colorToCSS(style.backgroundColor);
      this.ctx.fillRect(
        bounds.x,
        bounds.y + pageOffsetY,
        bounds.width,
        bounds.height
      );
      this.ctx.fillStyle = style.foregroundColor
        ? this.colorToCSS(style.foregroundColor)
        : '#000000';
    }

    // Draw text
    const y = bounds.y + pageOffsetY + baseline;

    // Use glyph positions for precise rendering
    for (let i = 0; i < glyphs.length; i++) {
      const glyph = glyphs[i];
      const char = content[i];
      this.ctx.fillText(char, bounds.x + glyph.x, y);
    }

    // Draw underline
    if (style.underline) {
      this.ctx.beginPath();
      this.ctx.moveTo(bounds.x, y + 2);
      this.ctx.lineTo(bounds.x + bounds.width, y + 2);
      this.ctx.stroke();
    }

    // Draw strikethrough
    if (style.strikethrough) {
      const strikeY = y - (bounds.height / 3);
      this.ctx.beginPath();
      this.ctx.moveTo(bounds.x, strikeY);
      this.ctx.lineTo(bounds.x + bounds.width, strikeY);
      this.ctx.stroke();
    }
  }

  private buildFontString(style: TextStyle): string {
    const parts: string[] = [];

    if (style.italic) parts.push('italic');
    if (style.bold) parts.push('bold');

    parts.push(`${style.fontSize?.magnitude || 11}pt`);
    parts.push(style.fontFamily || 'Arial');

    return parts.join(' ');
  }

  private colorToCSS(color: Color): string {
    if (color.rgbColor) {
      const { red = 0, green = 0, blue = 0 } = color.rgbColor;
      return `rgb(${red * 255}, ${green * 255}, ${blue * 255})`;
    }
    return '#000000';
  }

  private getVisiblePages(
    layoutTree: LayoutTree,
    scrollY: number,
    viewportHeight: number
  ): PageLayout[] {
    return layoutTree.pages.filter(page => {
      const pageTop = page.bounds.y;
      const pageBottom = page.bounds.y + page.bounds.height;
      const viewportTop = scrollY;
      const viewportBottom = scrollY + viewportHeight;

      return pageBottom > viewportTop && pageTop < viewportBottom;
    });
  }
}

7. Selection & Cursor Management

Managing selections across collaborative editors is complex:

// Selection model for collaborative editing

interface Selection {
  anchor: Position;  // Where selection started
  focus: Position;   // Where selection ended (cursor position)
  isCollapsed: boolean;
  direction: 'forward' | 'backward' | 'none';
}

interface Position {
  // Document position
  offset: number;
  // For sub-element positioning (e.g., within a table cell)
  path: number[];
}

interface RemoteSelection {
  userId: string;
  userName: string;
  color: string;
  selection: Selection;
}

class SelectionManager {
  private localSelection: Selection | null = null;
  private remoteSelections: Map<string, RemoteSelection> = new Map();
  private layoutEngine: LayoutEngine;
  private otEngine: OTEngine;

  // Convert screen coordinates to document position
  screenToDocumentPosition(x: number, y: number): Position {
    const layoutTree = this.layoutEngine.getCurrentLayout();

    // Find page at y coordinate
    const page = this.findPageAtY(layoutTree, y);
    if (!page) return { offset: 0, path: [] };

    // Find line at y coordinate
    const line = this.findLineAtY(page, y);
    if (!line) return { offset: 0, path: [] };

    // Find character position at x coordinate
    const position = this.findPositionInLine(line, x);

    return position;
  }

  // Convert document position to screen coordinates
  documentPositionToScreen(position: Position): { x: number; y: number } {
    const layoutTree = this.layoutEngine.getCurrentLayout();

    // Find the run containing this position
    const { run, offsetInRun } = this.findRunAtPosition(layoutTree, position.offset);

    if (!run) return { x: 0, y: 0 };

    // Calculate x position within run
    let x = run.bounds.x;
    for (let i = 0; i < offsetInRun; i++) {
      x += run.characterOffsets[i];
    }

    return { x, y: run.bounds.y };
  }

  // Update local selection
  setSelection(selection: Selection): void {
    this.localSelection = selection;

    // Broadcast to other users
    this.otEngine.broadcastPresence({
      type: 'selection',
      selection,
    });

    this.emit('selectionChange', selection);
  }

  // Handle remote selection update
  handleRemoteSelection(userId: string, selection: Selection): void {
    const userInfo = this.getUserInfo(userId);

    this.remoteSelections.set(userId, {
      userId,
      userName: userInfo.name,
      color: userInfo.color,
      selection,
    });

    this.emit('remoteSelectionChange', this.remoteSelections);
  }

  // Transform selection when document changes
  transformSelection(selection: Selection, operation: Operation): Selection {
    return {
      anchor: this.transformPosition(selection.anchor, operation),
      focus: this.transformPosition(selection.focus, operation),
      isCollapsed: selection.isCollapsed,
      direction: selection.direction,
    };
  }

  private transformPosition(position: Position, operation: Operation): Position {
    let newOffset = position.offset;

    switch (operation.type) {
      case 'insert':
        if (operation.position <= position.offset) {
          newOffset += operation.content.length;
        }
        break;
      case 'delete':
        if (operation.position + operation.length <= position.offset) {
          newOffset -= operation.length;
        } else if (operation.position < position.offset) {
          newOffset = operation.position;
        }
        break;
    }

    return { ...position, offset: Math.max(0, newOffset) };
  }

  private findLineAtY(page: PageLayout, y: number): LineLayout | null {
    for (const column of page.columns) {
      for (const line of column.lines) {
        if (y >= line.bounds.y && y < line.bounds.y + line.bounds.height) {
          return line;
        }
      }
    }
    return null;
  }

  private findPositionInLine(line: LineLayout, x: number): Position {
    let offset = 0;

    for (const run of line.runs) {
      if (x < run.bounds.x) {
        return { offset, path: [] };
      }

      if (x < run.bounds.x + run.bounds.width) {
        // Position is within this run
        let runX = run.bounds.x;

        for (let i = 0; i < run.characterOffsets.length; i++) {
          const charWidth = run.characterOffsets[i];
          const charMidpoint = runX + charWidth / 2;

          if (x < charMidpoint) {
            return { offset: offset + i, path: [] };
          }

          runX += charWidth;
        }

        return { offset: offset + run.content.length, path: [] };
      }

      offset += run.content.length;
    }

    return { offset, path: [] };
  }
}

// Render remote cursors
class CursorRenderer {
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  private selectionManager: SelectionManager;

  render(remoteSelections: Map<string, RemoteSelection>): void {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    for (const [userId, remote] of remoteSelections) {
      this.renderRemoteCursor(remote);
    }
  }

  private renderRemoteCursor(remote: RemoteSelection): void {
    const { selection, color, userName } = remote;

    if (selection.isCollapsed) {
      // Draw cursor line
      const pos = this.selectionManager.documentPositionToScreen(selection.focus);
      this.drawCursor(pos.x, pos.y, 20, color);

      // Draw name label
      this.drawNameLabel(pos.x, pos.y - 24, userName, color);
    } else {
      // Draw selection highlight
      this.drawSelection(selection, color);

      // Draw cursor at focus
      const pos = this.selectionManager.documentPositionToScreen(selection.focus);
      this.drawCursor(pos.x, pos.y, 20, color);
      this.drawNameLabel(pos.x, pos.y - 24, userName, color);
    }
  }

  private drawCursor(x: number, y: number, height: number, color: string): void {
    this.ctx.fillStyle = color;
    this.ctx.fillRect(x - 1, y, 2, height);
  }

  private drawNameLabel(x: number, y: number, name: string, color: string): void {
    this.ctx.font = '12px Arial';
    const metrics = this.ctx.measureText(name);
    const padding = 4;

    // Background
    this.ctx.fillStyle = color;
    this.ctx.fillRect(
      x,
      y,
      metrics.width + padding * 2,
      16
    );

    // Text
    this.ctx.fillStyle = '#ffffff';
    this.ctx.fillText(name, x + padding, y + 12);
  }

  private drawSelection(selection: Selection, color: string): void {
    // Get all line ranges that the selection covers
    const lineRanges = this.getSelectionLineRanges(selection);

    this.ctx.fillStyle = color + '40'; // 25% opacity

    for (const range of lineRanges) {
      this.ctx.fillRect(range.x, range.y, range.width, range.height);
    }
  }
}

8. Input Handling

Handling input across different browsers, languages, and input methods:

// Comprehensive input handling for document editing

class InputHandler {
  private documentModel: DocumentModel;
  private selectionManager: SelectionManager;
  private hiddenTextArea: HTMLTextAreaElement;
  private compositionState: CompositionState | null = null;

  constructor(
    documentModel: DocumentModel,
    selectionManager: SelectionManager
  ) {
    this.documentModel = documentModel;
    this.selectionManager = selectionManager;

    // Create hidden textarea for IME and clipboard
    this.hiddenTextArea = this.createHiddenTextArea();
    this.setupEventListeners();
  }

  private createHiddenTextArea(): HTMLTextAreaElement {
    const textarea = document.createElement('textarea');
    textarea.style.cssText = `
      position: absolute;
      left: -9999px;
      top: 0;
      width: 1px;
      height: 1px;
      opacity: 0;
      pointer-events: none;
    `;
    document.body.appendChild(textarea);
    return textarea;
  }

  private setupEventListeners(): void {
    // Keyboard events
    document.addEventListener('keydown', this.handleKeyDown.bind(this));

    // IME composition events
    this.hiddenTextArea.addEventListener('compositionstart', this.handleCompositionStart.bind(this));
    this.hiddenTextArea.addEventListener('compositionupdate', this.handleCompositionUpdate.bind(this));
    this.hiddenTextArea.addEventListener('compositionend', this.handleCompositionEnd.bind(this));

    // Input events (for IME final input)
    this.hiddenTextArea.addEventListener('input', this.handleInput.bind(this));

    // Clipboard events
    document.addEventListener('copy', this.handleCopy.bind(this));
    document.addEventListener('cut', this.handleCut.bind(this));
    document.addEventListener('paste', this.handlePaste.bind(this));

    // Focus management
    document.addEventListener('click', this.handleClick.bind(this));
  }

  private handleKeyDown(event: KeyboardEvent): void {
    // Handle special keys
    if (event.key === 'Backspace') {
      event.preventDefault();
      this.handleBackspace();
      return;
    }

    if (event.key === 'Delete') {
      event.preventDefault();
      this.handleDelete();
      return;
    }

    if (event.key === 'Enter') {
      event.preventDefault();
      this.handleEnter(event.shiftKey);
      return;
    }

    if (event.key === 'Tab') {
      event.preventDefault();
      this.handleTab(event.shiftKey);
      return;
    }

    // Handle keyboard shortcuts
    if (event.metaKey || event.ctrlKey) {
      if (this.handleShortcut(event)) {
        event.preventDefault();
        return;
      }
    }

    // Arrow keys for navigation
    if (event.key.startsWith('Arrow')) {
      event.preventDefault();
      this.handleArrowKey(event);
      return;
    }

    // Regular character input - let it go through IME/hidden textarea
    if (event.key.length === 1 && !event.metaKey && !event.ctrlKey) {
      // Focus hidden textarea to capture input
      this.hiddenTextArea.focus();
    }
  }

  private handleShortcut(event: KeyboardEvent): boolean {
    const key = event.key.toLowerCase();

    switch (key) {
      case 'b':
        this.toggleFormat('bold');
        return true;
      case 'i':
        this.toggleFormat('italic');
        return true;
      case 'u':
        this.toggleFormat('underline');
        return true;
      case 'z':
        if (event.shiftKey) {
          this.documentModel.redo();
        } else {
          this.documentModel.undo();
        }
        return true;
      case 'y':
        this.documentModel.redo();
        return true;
      case 'a':
        this.selectAll();
        return true;
      default:
        return false;
    }
  }

  // IME handling for Asian languages, etc.
  private handleCompositionStart(event: CompositionEvent): void {
    const selection = this.selectionManager.getSelection();
    if (!selection) return;

    this.compositionState = {
      startPosition: selection.anchor.offset,
      currentText: '',
    };
  }

  private handleCompositionUpdate(event: CompositionEvent): void {
    if (!this.compositionState) return;

    // Show composition text inline (with underline)
    this.showCompositionPreview(event.data);
  }

  private handleCompositionEnd(event: CompositionEvent): void {
    if (!this.compositionState) return;

    // Remove preview and insert final text
    this.clearCompositionPreview();
    this.insertText(event.data);

    this.compositionState = null;
    this.hiddenTextArea.value = '';
  }

  private handleInput(event: Event): void {
    // Handle non-composition input
    if (this.compositionState) return;

    const target = event.target as HTMLTextAreaElement;
    const text = target.value;

    if (text) {
      this.insertText(text);
      target.value = '';
    }
  }

  private handleBackspace(): void {
    const selection = this.selectionManager.getSelection();
    if (!selection) return;

    if (selection.isCollapsed) {
      // Delete character before cursor
      if (selection.anchor.offset > 0) {
        this.documentModel.applyOperation({
          type: 'delete',
          position: selection.anchor.offset - 1,
          length: 1,
        });

        this.selectionManager.setSelection({
          anchor: { offset: selection.anchor.offset - 1, path: [] },
          focus: { offset: selection.anchor.offset - 1, path: [] },
          isCollapsed: true,
          direction: 'none',
        });
      }
    } else {
      // Delete selection
      this.deleteSelection(selection);
    }
  }

  private handleDelete(): void {
    const selection = this.selectionManager.getSelection();
    if (!selection) return;

    if (selection.isCollapsed) {
      // Delete character after cursor
      this.documentModel.applyOperation({
        type: 'delete',
        position: selection.anchor.offset,
        length: 1,
      });
    } else {
      this.deleteSelection(selection);
    }
  }

  private handleEnter(shiftKey: boolean): void {
    if (shiftKey) {
      // Soft line break
      this.insertText('\n');
    } else {
      // New paragraph
      this.insertParagraphBreak();
    }
  }

  private insertText(text: string): void {
    const selection = this.selectionManager.getSelection();
    if (!selection) return;

    // Delete selection first if not collapsed
    if (!selection.isCollapsed) {
      this.deleteSelection(selection);
    }

    // Insert text
    this.documentModel.applyOperation({
      type: 'insert',
      position: selection.anchor.offset,
      content: text,
    });

    // Move cursor after inserted text
    this.selectionManager.setSelection({
      anchor: { offset: selection.anchor.offset + text.length, path: [] },
      focus: { offset: selection.anchor.offset + text.length, path: [] },
      isCollapsed: true,
      direction: 'none',
    });
  }

  private deleteSelection(selection: Selection): void {
    const start = Math.min(selection.anchor.offset, selection.focus.offset);
    const end = Math.max(selection.anchor.offset, selection.focus.offset);

    this.documentModel.applyOperation({
      type: 'delete',
      position: start,
      length: end - start,
    });

    this.selectionManager.setSelection({
      anchor: { offset: start, path: [] },
      focus: { offset: start, path: [] },
      isCollapsed: true,
      direction: 'none',
    });
  }

  private toggleFormat(format: 'bold' | 'italic' | 'underline'): void {
    const selection = this.selectionManager.getSelection();
    if (!selection || selection.isCollapsed) return;

    const start = Math.min(selection.anchor.offset, selection.focus.offset);
    const end = Math.max(selection.anchor.offset, selection.focus.offset);

    // Check current format state
    const currentState = this.documentModel.getFormatAt(start, format);

    this.documentModel.applyOperation({
      type: 'format',
      position: start,
      length: end - start,
      attributes: { [format]: !currentState },
    });
  }

  // Clipboard handling
  private handleCopy(event: ClipboardEvent): void {
    const selection = this.selectionManager.getSelection();
    if (!selection || selection.isCollapsed) return;

    event.preventDefault();

    const start = Math.min(selection.anchor.offset, selection.focus.offset);
    const end = Math.max(selection.anchor.offset, selection.focus.offset);

    // Get content with formatting
    const content = this.documentModel.getContentRange(start, end);

    // Set both plain text and rich text formats
    event.clipboardData?.setData('text/plain', content.plainText);
    event.clipboardData?.setData('text/html', content.html);
    event.clipboardData?.setData('application/x-google-docs-document-slice', JSON.stringify(content.native));
  }

  private handleCut(event: ClipboardEvent): void {
    this.handleCopy(event);

    const selection = this.selectionManager.getSelection();
    if (selection && !selection.isCollapsed) {
      this.deleteSelection(selection);
    }
  }

  private async handlePaste(event: ClipboardEvent): Promise<void> {
    event.preventDefault();

    const clipboardData = event.clipboardData;
    if (!clipboardData) return;

    // Try to get Google Docs native format first
    const nativeData = clipboardData.getData('application/x-google-docs-document-slice');
    if (nativeData) {
      const content = JSON.parse(nativeData);
      this.insertNativeContent(content);
      return;
    }

    // Try HTML
    const htmlData = clipboardData.getData('text/html');
    if (htmlData) {
      const content = this.parseHtmlToDocContent(htmlData);
      this.insertNativeContent(content);
      return;
    }

    // Fall back to plain text
    const textData = clipboardData.getData('text/plain');
    if (textData) {
      this.insertText(textData);
    }

    // Handle images
    const items = clipboardData.items;
    for (const item of items) {
      if (item.type.startsWith('image/')) {
        const blob = item.getAsFile();
        if (blob) {
          await this.insertImage(blob);
        }
      }
    }
  }
}

9. Offline Support

Google Docs works offline with automatic sync when reconnected:

// Offline-first architecture

class OfflineManager {
  private db: IDBDatabase;
  private operationQueue: Operation[] = [];
  private isOnline: boolean = navigator.onLine;
  private otEngine: OTEngine;
  private documentId: string;

  constructor(documentId: string, otEngine: OTEngine) {
    this.documentId = documentId;
    this.otEngine = otEngine;
    this.initIndexedDB();
    this.setupNetworkListeners();
  }

  private async initIndexedDB(): Promise<void> {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('google-docs-offline', 1);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => {
        this.db = request.result;
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;

        // Store for document snapshots
        if (!db.objectStoreNames.contains('documents')) {
          const docStore = db.createObjectStore('documents', { keyPath: 'id' });
          docStore.createIndex('lastModified', 'lastModified');
        }

        // Store for pending operations
        if (!db.objectStoreNames.contains('operations')) {
          const opStore = db.createObjectStore('operations', {
            keyPath: 'id',
            autoIncrement: true,
          });
          opStore.createIndex('documentId', 'documentId');
          opStore.createIndex('timestamp', 'timestamp');
        }
      };
    });
  }

  private setupNetworkListeners(): void {
    window.addEventListener('online', () => {
      this.isOnline = true;
      this.syncPendingOperations();
    });

    window.addEventListener('offline', () => {
      this.isOnline = false;
    });
  }

  // Save document snapshot for offline access
  async saveDocumentSnapshot(document: Document): Promise<void> {
    const transaction = this.db.transaction('documents', 'readwrite');
    const store = transaction.objectStore('documents');

    await new Promise<void>((resolve, reject) => {
      const request = store.put({
        id: this.documentId,
        document,
        lastModified: Date.now(),
        revision: document.revision,
      });
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  // Load document from offline cache
  async loadDocumentSnapshot(): Promise<Document | null> {
    const transaction = this.db.transaction('documents', 'readonly');
    const store = transaction.objectStore('documents');

    return new Promise((resolve, reject) => {
      const request = store.get(this.documentId);
      request.onsuccess = () => {
        resolve(request.result?.document || null);
      };
      request.onerror = () => reject(request.error);
    });
  }

  // Queue operation for offline storage
  async queueOperation(operation: Operation): Promise<void> {
    const transaction = this.db.transaction('operations', 'readwrite');
    const store = transaction.objectStore('operations');

    await new Promise<void>((resolve, reject) => {
      const request = store.add({
        documentId: this.documentId,
        operation,
        timestamp: Date.now(),
      });
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  // Sync pending operations when back online
  async syncPendingOperations(): Promise<void> {
    const transaction = this.db.transaction('operations', 'readonly');
    const store = transaction.objectStore('operations');
    const index = store.index('documentId');

    const pendingOps = await new Promise<StoredOperation[]>((resolve, reject) => {
      const request = index.getAll(IDBKeyRange.only(this.documentId));
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });

    if (pendingOps.length === 0) return;

    // Sort by timestamp
    pendingOps.sort((a, b) => a.timestamp - b.timestamp);

    // Compose all pending operations
    const composed = composeOperations(pendingOps.map(p => p.operation));

    try {
      // Send to server
      await this.otEngine.sendOperation(composed);

      // Clear pending operations on success
      await this.clearPendingOperations();
    } catch (error) {
      console.error('Failed to sync pending operations:', error);
      // Keep operations for retry
    }
  }

  private async clearPendingOperations(): Promise<void> {
    const transaction = this.db.transaction('operations', 'readwrite');
    const store = transaction.objectStore('operations');
    const index = store.index('documentId');

    const cursor = index.openCursor(IDBKeyRange.only(this.documentId));

    await new Promise<void>((resolve, reject) => {
      cursor.onsuccess = (event) => {
        const result = (event.target as IDBRequest).result;
        if (result) {
          result.delete();
          result.continue();
        } else {
          resolve();
        }
      };
      cursor.onerror = () => reject(cursor.error);
    });
  }

  // Handle operation when offline
  async handleOfflineOperation(operation: Operation): Promise<void> {
    // Store operation
    await this.queueOperation(operation);

    // Apply locally (optimistic)
    this.otEngine.applyLocal(operation);

    // Update local snapshot
    const currentDoc = this.otEngine.getDocument();
    await this.saveDocumentSnapshot(currentDoc);
  }
}

// Service Worker for offline caching
// sw.js
const CACHE_NAME = 'google-docs-v1';
const STATIC_ASSETS = [
  '/',
  '/document',
  '/static/js/main.js',
  '/static/css/main.css',
  '/static/fonts/roboto.woff2',
];

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

self.addEventListener('fetch', (event: FetchEvent) => {
  const url = new URL(event.request.url);

  // API requests - network first, then cache
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          // Clone and cache successful responses
          if (response.ok) {
            const responseClone = response.clone();
            caches.open(CACHE_NAME).then(cache => {
              cache.put(event.request, responseClone);
            });
          }
          return response;
        })
        .catch(() => {
          // Return cached response if network fails
          return caches.match(event.request);
        })
    );
    return;
  }

  // Static assets - cache first
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

10. Comments & Suggestions System

The comments system allows threaded discussions anchored to document positions:

// Comment and suggestion system

interface Comment {
  id: string;
  content: string;
  author: User;
  createdTime: string;
  modifiedTime: string;
  resolved: boolean;
  replies: Reply[];
  anchor: CommentAnchor;
  quotedContent?: string;
}

interface CommentAnchor {
  // Range in document this comment refers to
  startOffset: number;
  endOffset: number;
  // For robustness - text content at time of commenting
  quotedText: string;
}

interface Suggestion {
  id: string;
  author: User;
  createdTime: string;
  // The change being suggested
  originalContent: string;
  suggestedContent: string;
  anchor: SuggestionAnchor;
  status: 'pending' | 'accepted' | 'rejected';
}

interface SuggestionAnchor {
  startOffset: number;
  endOffset: number;
}

class CommentManager {
  private comments: Map<string, Comment> = new Map();
  private suggestions: Map<string, Suggestion> = new Map();
  private documentModel: DocumentModel;

  constructor(documentModel: DocumentModel) {
    this.documentModel = documentModel;

    // Listen for document changes to update anchors
    documentModel.on('operation', this.handleDocumentOperation.bind(this));
  }

  // Add a new comment
  async addComment(
    startOffset: number,
    endOffset: number,
    content: string
  ): Promise<Comment> {
    const quotedText = this.documentModel.getTextRange(startOffset, endOffset);

    const comment: Comment = {
      id: generateId(),
      content,
      author: getCurrentUser(),
      createdTime: new Date().toISOString(),
      modifiedTime: new Date().toISOString(),
      resolved: false,
      replies: [],
      anchor: {
        startOffset,
        endOffset,
        quotedText,
      },
      quotedContent: quotedText,
    };

    this.comments.set(comment.id, comment);

    // Save to server
    await this.saveComment(comment);

    return comment;
  }

  // Add a suggestion (track changes)
  async addSuggestion(
    startOffset: number,
    endOffset: number,
    suggestedContent: string
  ): Promise<Suggestion> {
    const originalContent = this.documentModel.getTextRange(startOffset, endOffset);

    const suggestion: Suggestion = {
      id: generateId(),
      author: getCurrentUser(),
      createdTime: new Date().toISOString(),
      originalContent,
      suggestedContent,
      anchor: { startOffset, endOffset },
      status: 'pending',
    };

    this.suggestions.set(suggestion.id, suggestion);

    // Apply suggestion as a tracked change in document
    this.documentModel.applyOperation({
      type: 'suggest',
      suggestionId: suggestion.id,
      position: startOffset,
      length: endOffset - startOffset,
      replacement: suggestedContent,
    });

    await this.saveSuggestion(suggestion);

    return suggestion;
  }

  // Accept suggestion
  async acceptSuggestion(suggestionId: string): Promise<void> {
    const suggestion = this.suggestions.get(suggestionId);
    if (!suggestion) return;

    // Apply the suggested change permanently
    this.documentModel.applyOperation({
      type: 'acceptSuggestion',
      suggestionId,
    });

    suggestion.status = 'accepted';
    await this.saveSuggestion(suggestion);
  }

  // Reject suggestion
  async rejectSuggestion(suggestionId: string): Promise<void> {
    const suggestion = this.suggestions.get(suggestionId);
    if (!suggestion) return;

    // Revert to original content
    this.documentModel.applyOperation({
      type: 'rejectSuggestion',
      suggestionId,
    });

    suggestion.status = 'rejected';
    await this.saveSuggestion(suggestion);
  }

  // Update comment/suggestion anchors when document changes
  private handleDocumentOperation(operation: Operation): void {
    // Update comment anchors
    for (const [id, comment] of this.comments) {
      comment.anchor = this.transformAnchor(comment.anchor, operation);
    }

    // Update suggestion anchors
    for (const [id, suggestion] of this.suggestions) {
      if (suggestion.status === 'pending') {
        suggestion.anchor = this.transformAnchor(suggestion.anchor, operation);
      }
    }
  }

  private transformAnchor(
    anchor: { startOffset: number; endOffset: number },
    operation: Operation
  ): { startOffset: number; endOffset: number } {
    let { startOffset, endOffset } = anchor;

    switch (operation.type) {
      case 'insert':
        if (operation.position <= startOffset) {
          startOffset += operation.content.length;
          endOffset += operation.content.length;
        } else if (operation.position < endOffset) {
          endOffset += operation.content.length;
        }
        break;

      case 'delete':
        const deleteEnd = operation.position + operation.length;

        if (deleteEnd <= startOffset) {
          // Delete is before anchor
          startOffset -= operation.length;
          endOffset -= operation.length;
        } else if (operation.position >= endOffset) {
          // Delete is after anchor - no change
        } else {
          // Delete overlaps anchor - adjust
          if (operation.position < startOffset) {
            startOffset = operation.position;
          }
          if (deleteEnd < endOffset) {
            endOffset -= operation.length;
          } else {
            endOffset = startOffset;
          }
        }
        break;
    }

    return { startOffset: Math.max(0, startOffset), endOffset: Math.max(0, endOffset) };
  }
}

// Comment sidebar component
function CommentSidebar({ documentId }: { documentId: string }) {
  const { comments, loading } = useComments(documentId);
  const [activeComment, setActiveComment] = useState<string | null>(null);

  // Group comments by their vertical position in document
  const positionedComments = useMemo(() => {
    return comments
      .filter(c => !c.resolved)
      .map(comment => ({
        ...comment,
        // Calculate Y position based on anchor
        yPosition: getYPositionForOffset(comment.anchor.startOffset),
      }))
      .sort((a, b) => a.yPosition - b.yPosition);
  }, [comments]);

  // Resolve overlapping comment positions
  const layoutComments = useMemo(() => {
    const minGap = 100; // Minimum pixels between comments
    const result: PositionedComment[] = [];
    let lastY = 0;

    for (const comment of positionedComments) {
      const y = Math.max(comment.yPosition, lastY + minGap);
      result.push({ ...comment, displayY: y });
      lastY = y;
    }

    return result;
  }, [positionedComments]);

  return (
    <aside className="comment-sidebar">
      {layoutComments.map(comment => (
        <CommentThread
          key={comment.id}
          comment={comment}
          style={{ top: comment.displayY }}
          isActive={activeComment === comment.id}
          onActivate={() => setActiveComment(comment.id)}
          onDeactivate={() => setActiveComment(null)}
        />
      ))}
    </aside>
  );
}

function CommentThread({
  comment,
  style,
  isActive,
  onActivate,
  onDeactivate,
}: CommentThreadProps) {
  const [replyText, setReplyText] = useState('');
  const { addReply, resolveComment } = useCommentActions(comment.id);

  return (
    <div
      className={`comment-thread ${isActive ? 'active' : ''}`}
      style={style}
      onClick={onActivate}
    >
      {/* Connection line to document */}
      <div className="comment-connector" />

      {/* Main comment */}
      <div className="comment-main">
        <div className="comment-header">
          <Avatar user={comment.author} size="small" />
          <span className="author-name">{comment.author.name}</span>
          <span className="timestamp">{formatRelativeTime(comment.createdTime)}</span>
        </div>

        {comment.quotedContent && (
          <blockquote className="quoted-content">
            "{comment.quotedContent}"
          </blockquote>
        )}

        <p className="comment-content">{comment.content}</p>
      </div>

      {/* Replies */}
      {comment.replies.map(reply => (
        <div key={reply.id} className="comment-reply">
          <Avatar user={reply.author} size="small" />
          <div>
            <span className="author-name">{reply.author.name}</span>
            <p>{reply.content}</p>
          </div>
        </div>
      ))}

      {/* Reply input (shown when active) */}
      {isActive && (
        <div className="reply-input">
          <input
            type="text"
            placeholder="Reply..."
            value={replyText}
            onChange={(e) => setReplyText(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Enter' && replyText.trim()) {
                addReply(replyText);
                setReplyText('');
              }
            }}
          />
          <button onClick={() => resolveComment()}>Resolve</button>
        </div>
      )}
    </div>
  );
}

11. Version History

Complete revision history with the ability to restore any previous version:

// Version history system

interface Revision {
  id: string;
  documentId: string;
  timestamp: string;
  author: User;
  // Snapshot or delta from previous revision
  type: 'snapshot' | 'delta';
  data: RevisionData;
  // Summary of changes
  summary: RevisionSummary;
}

interface RevisionSummary {
  addedCharacters: number;
  deletedCharacters: number;
  formattingChanges: number;
  imageChanges: number;
}

class RevisionHistoryManager {
  private revisions: Revision[] = [];
  private documentId: string;
  private currentDocument: Document;

  // Automatic snapshot every N operations or M minutes
  private operationsSinceSnapshot: number = 0;
  private lastSnapshotTime: number = Date.now();
  private snapshotThreshold = 100; // operations
  private timeThreshold = 5 * 60 * 1000; // 5 minutes

  constructor(documentId: string, document: Document) {
    this.documentId = documentId;
    this.currentDocument = document;
  }

  // Record an operation
  recordOperation(operation: Operation, author: User): void {
    this.operationsSinceSnapshot++;

    // Check if we should create a snapshot
    const timeSinceSnapshot = Date.now() - this.lastSnapshotTime;

    if (
      this.operationsSinceSnapshot >= this.snapshotThreshold ||
      timeSinceSnapshot >= this.timeThreshold
    ) {
      this.createSnapshot(author);
    }
  }

  // Create a snapshot revision
  private async createSnapshot(author: User): Promise<void> {
    const revision: Revision = {
      id: generateId(),
      documentId: this.documentId,
      timestamp: new Date().toISOString(),
      author,
      type: 'snapshot',
      data: {
        document: this.currentDocument,
      },
      summary: this.calculateSummary(),
    };

    this.revisions.push(revision);
    this.operationsSinceSnapshot = 0;
    this.lastSnapshotTime = Date.now();

    await this.saveRevision(revision);
  }

  // Get revision list (grouped by session/time)
  getRevisionList(): RevisionGroup[] {
    const groups: RevisionGroup[] = [];
    let currentGroup: RevisionGroup | null = null;

    for (const revision of this.revisions) {
      const revisionTime = new Date(revision.timestamp);

      // Group revisions by author and time proximity
      if (
        !currentGroup ||
        currentGroup.author.id !== revision.author.id ||
        revisionTime.getTime() - currentGroup.endTime.getTime() > 30 * 60 * 1000 // 30 min gap
      ) {
        currentGroup = {
          id: revision.id,
          author: revision.author,
          startTime: revisionTime,
          endTime: revisionTime,
          revisions: [],
          summary: { addedCharacters: 0, deletedCharacters: 0, formattingChanges: 0, imageChanges: 0 },
        };
        groups.push(currentGroup);
      }

      currentGroup.endTime = revisionTime;
      currentGroup.revisions.push(revision);

      // Aggregate summary
      currentGroup.summary.addedCharacters += revision.summary.addedCharacters;
      currentGroup.summary.deletedCharacters += revision.summary.deletedCharacters;
      currentGroup.summary.formattingChanges += revision.summary.formattingChanges;
      currentGroup.summary.imageChanges += revision.summary.imageChanges;
    }

    return groups.reverse(); // Most recent first
  }

  // Get document at specific revision
  async getDocumentAtRevision(revisionId: string): Promise<Document> {
    const revisionIndex = this.revisions.findIndex(r => r.id === revisionId);
    if (revisionIndex === -1) {
      throw new Error('Revision not found');
    }

    // Find nearest snapshot at or before this revision
    let snapshotIndex = revisionIndex;
    while (snapshotIndex >= 0 && this.revisions[snapshotIndex].type !== 'snapshot') {
      snapshotIndex--;
    }

    if (snapshotIndex < 0) {
      throw new Error('No snapshot found');
    }

    // Start with snapshot
    let document = this.revisions[snapshotIndex].data.document;

    // Apply deltas to reach target revision
    for (let i = snapshotIndex + 1; i <= revisionIndex; i++) {
      const revision = this.revisions[i];
      if (revision.type === 'delta') {
        document = applyDelta(document, revision.data.delta);
      } else {
        document = revision.data.document;
      }
    }

    return document;
  }

  // Restore document to specific revision
  async restoreToRevision(revisionId: string): Promise<void> {
    const document = await this.getDocumentAtRevision(revisionId);

    // Create restore operation
    const restoreOperation: Operation = {
      type: 'restore',
      revisionId,
      document,
    };

    // Apply through OT engine (will be synced to other clients)
    this.otEngine.applyLocal(restoreOperation);

    // Create new snapshot
    await this.createSnapshot(getCurrentUser());
  }

  // Named versions (manual saves)
  async createNamedVersion(name: string): Promise<Revision> {
    const revision: Revision = {
      id: generateId(),
      documentId: this.documentId,
      timestamp: new Date().toISOString(),
      author: getCurrentUser(),
      type: 'snapshot',
      data: {
        document: this.currentDocument,
        name, // Named version
      },
      summary: this.calculateSummary(),
    };

    this.revisions.push(revision);
    await this.saveRevision(revision);

    return revision;
  }
}

// Version history UI
function VersionHistoryPanel({ documentId }: { documentId: string }) {
  const { revisionGroups, loading } = useRevisionHistory(documentId);
  const [selectedRevision, setSelectedRevision] = useState<string | null>(null);
  const [previewDocument, setPreviewDocument] = useState<Document | null>(null);

  const handleSelectRevision = async (revisionId: string) => {
    setSelectedRevision(revisionId);
    const doc = await revisionHistoryManager.getDocumentAtRevision(revisionId);
    setPreviewDocument(doc);
  };

  return (
    <div className="version-history-panel">
      <header>
        <h2>Version history</h2>
        <button onClick={() => restoreToRevision(selectedRevision)}>
          Restore this version
        </button>
      </header>

      <div className="revision-list">
        {revisionGroups.map(group => (
          <div key={group.id} className="revision-group">
            <div className="group-header">
              <span className="date">
                {formatDate(group.startTime)}
              </span>
            </div>

            <div className="group-content">
              <Avatar user={group.author} />
              <div className="group-details">
                <span className="author">{group.author.name}</span>
                <span className="time-range">
                  {formatTimeRange(group.startTime, group.endTime)}
                </span>
                <span className="summary">
                  {formatSummary(group.summary)}
                </span>
              </div>

              {/* Expandable list of individual revisions */}
              <RevisionList
                revisions={group.revisions}
                selectedId={selectedRevision}
                onSelect={handleSelectRevision}
              />
            </div>
          </div>
        ))}
      </div>

      {/* Preview pane */}
      {previewDocument && (
        <div className="revision-preview">
          <DocumentPreview document={previewDocument} />
        </div>
      )}
    </div>
  );
}

12. Accessibility

Making a canvas-based editor accessible requires extensive work:

// Accessibility layer for canvas-based rendering

class AccessibilityManager {
  private accessibilityTree: AccessibilityNode;
  private liveRegion: HTMLElement;
  private documentModel: DocumentModel;
  private selectionManager: SelectionManager;

  constructor(documentModel: DocumentModel, selectionManager: SelectionManager) {
    this.documentModel = documentModel;
    this.selectionManager = selectionManager;

    this.createAccessibilityDOM();
    this.setupKeyboardNavigation();
    this.setupScreenReaderSupport();
  }

  // Create hidden accessible DOM that mirrors document structure
  private createAccessibilityDOM(): void {
    // Create container that's visually hidden but accessible
    const container = document.createElement('div');
    container.id = 'docs-accessibility-tree';
    container.setAttribute('role', 'document');
    container.setAttribute('aria-label', 'Document content');
    container.style.cssText = `
      position: absolute;
      left: -10000px;
      top: 0;
      width: 1px;
      height: 1px;
      overflow: hidden;
    `;

    document.body.appendChild(container);
    this.accessibilityTree = { element: container, children: [] };

    // Create live region for announcements
    this.liveRegion = document.createElement('div');
    this.liveRegion.setAttribute('aria-live', 'polite');
    this.liveRegion.setAttribute('aria-atomic', 'true');
    this.liveRegion.className = 'sr-only';
    document.body.appendChild(this.liveRegion);

    // Build initial tree
    this.rebuildAccessibilityTree();

    // Listen for document changes
    this.documentModel.on('change', () => this.updateAccessibilityTree());
  }

  // Build accessible DOM tree from document model
  private rebuildAccessibilityTree(): void {
    const container = this.accessibilityTree.element;
    container.innerHTML = '';

    const document = this.documentModel.getDocument();

    for (const element of document.body.content) {
      const node = this.createAccessibleNode(element);
      if (node) {
        container.appendChild(node);
      }
    }
  }

  private createAccessibleNode(element: StructuralElement): HTMLElement | null {
    switch (element.type) {
      case 'paragraph':
        return this.createParagraphNode(element);
      case 'table':
        return this.createTableNode(element);
      case 'sectionBreak':
        return this.createSectionBreakNode(element);
      default:
        return null;
    }
  }

  private createParagraphNode(paragraph: Paragraph): HTMLElement {
    // Determine appropriate element based on style
    let element: HTMLElement;

    if (paragraph.paragraphStyle.namedStyleType?.startsWith('HEADING')) {
      const level = paragraph.paragraphStyle.namedStyleType.slice(-1);
      element = document.createElement(`h${level}`);
    } else if (paragraph.bullet) {
      element = document.createElement('li');
    } else {
      element = document.createElement('p');
    }

    // Build text content with accessible formatting
    for (const run of paragraph.elements) {
      if (run.type === 'textRun') {
        const span = this.createTextRunNode(run);
        element.appendChild(span);
      } else if (run.type === 'inlineObjectElement') {
        const img = this.createImageNode(run);
        element.appendChild(img);
      }
    }

    return element;
  }

  private createTextRunNode(textRun: TextRun): HTMLElement {
    let element: HTMLElement = document.createElement('span');
    element.textContent = textRun.content;

    // Wrap with formatting elements
    if (textRun.textStyle.bold) {
      const strong = document.createElement('strong');
      strong.appendChild(element);
      element = strong;
    }

    if (textRun.textStyle.italic) {
      const em = document.createElement('em');
      em.appendChild(element);
      element = em;
    }

    if (textRun.textStyle.link) {
      const a = document.createElement('a');
      a.href = textRun.textStyle.link.url;
      a.appendChild(element);
      element = a;
    }

    return element;
  }

  private createTableNode(table: Table): HTMLElement {
    const tableElement = document.createElement('table');
    tableElement.setAttribute('role', 'table');

    for (let rowIndex = 0; rowIndex < table.rows; rowIndex++) {
      const tr = document.createElement('tr');

      for (let colIndex = 0; colIndex < table.columns; colIndex++) {
        const cell = table.cells[rowIndex][colIndex];
        const td = document.createElement(rowIndex === 0 ? 'th' : 'td');

        // Set cell content
        for (const element of cell.content) {
          const node = this.createAccessibleNode(element);
          if (node) td.appendChild(node);
        }

        tr.appendChild(td);
      }

      tableElement.appendChild(tr);
    }

    return tableElement;
  }

  // Announce changes to screen readers
  announce(message: string, priority: 'polite' | 'assertive' = 'polite'): void {
    this.liveRegion.setAttribute('aria-live', priority);
    this.liveRegion.textContent = message;

    // Clear after announcement
    setTimeout(() => {
      this.liveRegion.textContent = '';
    }, 1000);
  }

  // Announce selection changes
  private announceSelectionChange(selection: Selection): void {
    if (selection.isCollapsed) {
      // Announce cursor position
      const context = this.getContextAtPosition(selection.focus.offset);
      this.announce(`Cursor at ${context}`);
    } else {
      // Announce selected text
      const selectedText = this.documentModel.getTextRange(
        Math.min(selection.anchor.offset, selection.focus.offset),
        Math.max(selection.anchor.offset, selection.focus.offset)
      );

      if (selectedText.length < 100) {
        this.announce(`Selected: ${selectedText}`);
      } else {
        this.announce(`Selected ${selectedText.length} characters`);
      }
    }
  }

  private getContextAtPosition(offset: number): string {
    const content = this.documentModel.getContentAt(offset);

    if (content.type === 'text') {
      // Get surrounding context
      const before = this.documentModel.getTextRange(
        Math.max(0, offset - 10),
        offset
      );
      const after = this.documentModel.getTextRange(
        offset,
        Math.min(this.documentModel.getLength(), offset + 10)
      );

      return `"${before}" cursor "${after}"`;
    }

    return `${content.type} element`;
  }

  // Keyboard navigation
  private setupKeyboardNavigation(): void {
    document.addEventListener('keydown', (event) => {
      // Ctrl+Home: Go to beginning
      if (event.ctrlKey && event.key === 'Home') {
        this.selectionManager.setSelection({
          anchor: { offset: 0, path: [] },
          focus: { offset: 0, path: [] },
          isCollapsed: true,
          direction: 'none',
        });
        this.announce('Beginning of document');
        event.preventDefault();
      }

      // Ctrl+End: Go to end
      if (event.ctrlKey && event.key === 'End') {
        const length = this.documentModel.getLength();
        this.selectionManager.setSelection({
          anchor: { offset: length, path: [] },
          focus: { offset: length, path: [] },
          isCollapsed: true,
          direction: 'none',
        });
        this.announce('End of document');
        event.preventDefault();
      }

      // F7: Toggle caret browsing (read-only navigation)
      if (event.key === 'F7') {
        this.toggleCaretBrowsing();
        event.preventDefault();
      }
    });
  }
}

13. Performance Optimization

Handling large documents requires extensive optimization:

// Performance optimizations for large documents

class PerformanceOptimizer {
  private documentModel: DocumentModel;
  private layoutEngine: LayoutEngine;
  private renderEngine: RenderEngine;

  // Virtualization - only layout/render visible content
  private visibleRange: { start: number; end: number } = { start: 0, end: 0 };
  private layoutCache: Map<string, LayoutResult> = new Map();
  private renderCache: Map<string, ImageBitmap> = new Map();

  // Incremental updates
  private dirtyRegions: Set<string> = new Set();
  private pendingLayout: boolean = false;
  private pendingRender: boolean = false;

  constructor(
    documentModel: DocumentModel,
    layoutEngine: LayoutEngine,
    renderEngine: RenderEngine
  ) {
    this.documentModel = documentModel;
    this.layoutEngine = layoutEngine;
    this.renderEngine = renderEngine;

    this.setupIncrementalUpdates();
  }

  // Handle scroll - update visible range
  handleScroll(scrollTop: number, viewportHeight: number): void {
    const pageHeight = 1056; // Standard page height in pixels
    const buffer = pageHeight; // Render one page above/below

    const newStart = Math.max(0, scrollTop - buffer);
    const newEnd = scrollTop + viewportHeight + buffer;

    if (newStart !== this.visibleRange.start || newEnd !== this.visibleRange.end) {
      this.visibleRange = { start: newStart, end: newEnd };
      this.scheduleRender();
    }
  }

  // Mark region as needing re-layout
  invalidateLayout(startOffset: number, endOffset: number): void {
    // Find affected paragraphs/pages
    const affectedPages = this.getAffectedPages(startOffset, endOffset);

    for (const pageId of affectedPages) {
      this.layoutCache.delete(pageId);
      this.renderCache.delete(pageId);
      this.dirtyRegions.add(pageId);
    }

    this.scheduleLayout();
  }

  // Incremental layout - only re-layout dirty regions
  private async performIncrementalLayout(): Promise<void> {
    if (this.dirtyRegions.size === 0) return;

    const startTime = performance.now();
    const budget = 16; // 16ms budget for 60fps

    for (const pageId of this.dirtyRegions) {
      // Check if we've exceeded time budget
      if (performance.now() - startTime > budget) {
        // Schedule continuation
        this.scheduleLayout();
        break;
      }

      // Layout this page
      const pageLayout = await this.layoutEngine.layoutPage(pageId);
      this.layoutCache.set(pageId, pageLayout);
      this.dirtyRegions.delete(pageId);
    }

    this.scheduleRender();
  }

  // Render visible pages
  private async performRender(): Promise<void> {
    const visiblePages = this.getVisiblePages();

    for (const pageId of visiblePages) {
      let bitmap = this.renderCache.get(pageId);

      if (!bitmap) {
        // Render to offscreen canvas
        const layout = this.layoutCache.get(pageId);
        if (layout) {
          bitmap = await this.renderPageTobitmap(layout);
          this.renderCache.set(pageId, bitmap);
        }
      }

      if (bitmap) {
        this.renderEngine.drawBitmap(bitmap, this.getPagePosition(pageId));
      }
    }
  }

  // Render page to ImageBitmap for caching
  private async renderPageTobitmap(layout: LayoutResult): Promise<ImageBitmap> {
    const offscreen = new OffscreenCanvas(layout.width, layout.height);
    const ctx = offscreen.getContext('2d')!;

    // Render page content
    this.renderEngine.renderToContext(ctx, layout);

    // Create bitmap for efficient reuse
    return createImageBitmap(offscreen);
  }

  // Memory management - evict old cache entries
  manageMemory(): void {
    const maxCacheSize = 50; // Max pages to cache

    // Evict pages far from visible range
    if (this.layoutCache.size > maxCacheSize) {
      const visiblePages = new Set(this.getVisiblePages());

      for (const [pageId, _] of this.layoutCache) {
        if (!visiblePages.has(pageId)) {
          this.layoutCache.delete(pageId);
          this.renderCache.get(pageId)?.close(); // Free ImageBitmap memory
          this.renderCache.delete(pageId);
        }

        if (this.layoutCache.size <= maxCacheSize) break;
      }
    }
  }

  // Debounced layout scheduling
  private scheduleLayout(): void {
    if (this.pendingLayout) return;

    this.pendingLayout = true;
    requestIdleCallback(() => {
      this.pendingLayout = false;
      this.performIncrementalLayout();
    });
  }

  // RAF-based render scheduling
  private scheduleRender(): void {
    if (this.pendingRender) return;

    this.pendingRender = true;
    requestAnimationFrame(() => {
      this.pendingRender = false;
      this.performRender();
    });
  }

  private setupIncrementalUpdates(): void {
    this.documentModel.on('operation', (operation: Operation) => {
      // Determine affected range
      let start = operation.position;
      let end = operation.position;

      switch (operation.type) {
        case 'insert':
          end = operation.position + operation.content.length;
          break;
        case 'delete':
          end = operation.position + operation.length;
          break;
        case 'format':
          end = operation.position + operation.length;
          break;
      }

      this.invalidateLayout(start, end);
    });
  }
}

// Web Worker for heavy computations
// layout-worker.ts
self.onmessage = async (event: MessageEvent) => {
  const { type, payload } = event.data;

  switch (type) {
    case 'LAYOUT_PAGE': {
      const { pageId, content, styles } = payload;
      const layout = performLayout(content, styles);
      self.postMessage({ type: 'LAYOUT_RESULT', pageId, layout });
      break;
    }

    case 'SPELL_CHECK': {
      const { text, language } = payload;
      const errors = performSpellCheck(text, language);
      self.postMessage({ type: 'SPELL_CHECK_RESULT', errors });
      break;
    }

    case 'SEARCH': {
      const { query, document } = payload;
      const results = searchDocument(query, document);
      self.postMessage({ type: 'SEARCH_RESULT', results });
      break;
    }
  }
};

14. Architecture Evolution

┌─────────────────────────────────────────────────────────────────────────────┐
│                   GOOGLE DOCS ARCHITECTURE EVOLUTION                        │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  2006      2010      2014      2018      2021      2024                    │
│    │         │         │         │         │         │                     │
│    ▼         ▼         ▼         ▼         ▼         ▼                     │
│  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐                     │
│  │Writely│ │ OT  │  │ Real│  │Offline│ │Canvas│  │ AI  │                    │
│  │Acqui │ │Engine│  │-time│  │First │ │Render│  │Integ│                    │
│  │sition│ │     │  │Collab│  │      │ │      │  │ation│                    │
│  └─────┘  └─────┘  └─────┘  └─────┘  └─────┘  └─────┘                     │
│                                                                             │
│  KEY MILESTONES:                                                           │
│  ───────────────                                                           │
│  2006: Google acquires Writely, basic web editor                           │
│  2010: Operational Transformation enables real-time collaboration          │
│  2014: 60+ concurrent editors supported                                    │
│  2018: Offline-first architecture, Service Workers                         │
│  2021: Canvas-based rendering replaces DOM                                 │
│  2024: AI-powered writing assistance integrated                            │
│                                                                             │
│  RENDERING EVOLUTION:                                                       │
│  ────────────────────                                                      │
│  v1: contenteditable + DOM manipulation                                    │
│  v2: Custom DOM with synthetic selection                                   │
│  v3: Canvas rendering with accessibility tree                              │
│                                                                             │
│  PERFORMANCE IMPROVEMENTS:                                                  │
│  ─────────────────────────                                                 │
│  Document open time: 5s → 1s → 500ms                                       │
│  Max document size: 50 pages → 500 pages → 1500+ pages                     │
│  Typing latency: 150ms → 50ms → 16ms                                       │
│  Sync latency: 500ms → 100ms → 50ms                                        │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

15. Lessons & Tradeoffs

Key Architectural Decisions

1. Canvas vs DOM Rendering

// TRADEOFF: Control vs Browser Features

// DOM Rendering (Pre-2021)
// Pros:
// - Native accessibility
// - Native text selection
// - Browser handles complex typography
// - Simpler implementation

// Cons:
// - Performance degrades with document size
// - Inconsistent rendering across browsers
// - Limited control over layout

// Canvas Rendering (2021+)
// Pros:
// - Consistent cross-browser rendering
// - Better performance for large documents
// - Full control over typography
// - Pixel-perfect printing

// Cons:
// - Must implement accessibility layer
// - Must implement text selection
// - Must implement text shaping
// - Significant engineering investment

// Decision: Canvas is worth the investment at Google's scale

2. Operational Transformation vs CRDTs

// TRADEOFF: Complexity vs Offline Support

// OT (Google's choice)
// - Requires server for transformation
// - Simpler conflict resolution
// - Proven at scale
// - Better for real-time sync

// CRDTs
// - True offline-first
// - No server required for merging
// - More complex implementation
// - Can grow unbounded

// Decision: OT with offline queue works well for
// document editing where users are usually online

3. Custom vs Browser APIs

// Google Docs implements nearly everything custom:
// - Text rendering (canvas)
// - Selection handling
// - Clipboard operations
// - Input method handling
// - Spell checking
// - Accessibility

// Why? Full control over behavior across all browsers
// and ability to implement features browsers don't support

Conclusion

Google Docs represents one of the most sophisticated frontend applications ever built. Key takeaways:

  1. OT is Hard but Essential: Operational Transformation enables the magical real-time collaboration experience, but requires mathematical rigor.

  2. Canvas Enables Control: Moving from DOM to canvas rendering was a massive undertaking but enables consistent, performant rendering.

  3. Accessibility Requires Dedication: Custom rendering means rebuilding accessibility from scratch—a significant but necessary investment.

  4. Offline is a Feature: Users expect documents to work offline, requiring sophisticated sync and conflict resolution.

  5. Performance at Scale: Documents can be 1000+ pages; virtualization, caching, and incremental updates are essential.

Google Docs proves that browser-based applications can match or exceed native application capabilities with sufficient engineering investment.


Current production metrics:

  • Typing Latency: ~16ms (one frame)
  • Sync Latency: ~50ms (p50)
  • Max Concurrent Editors: 100+
  • Max Document Size: 1.5M characters
  • Offline Support: Full editing capability
  • Accessibility: WCAG 2.1 AA compliant

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.