CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-slate-react

React components and hooks for building customizable rich text editors using the Slate framework.

Pending
Overview
Eval results
Files

performance-optimization.mddocs/

Performance Optimization

Advanced hooks and utilities for optimizing editor performance, especially important for large documents. These features help prevent unnecessary re-renders and improve the user experience with complex or large-scale editor implementations.

Capabilities

Selective Re-rendering with useSlateSelector

The useSlateSelector hook provides Redux-style selectors to prevent unnecessary component re-renders by only updating when specific parts of the editor state change.

/**
 * Use redux-style selectors to prevent re-rendering on every keystroke
 * @param selector - Function to select specific data from editor
 * @param equalityFn - Optional custom equality function for comparison
 * @param options - Optional configuration options
 * @returns Selected value from editor state
 */
function useSlateSelector<T>(
  selector: (editor: Editor) => T,
  equalityFn?: (a: T | null, b: T) => boolean,
  options?: SlateSelectorOptions
): T;

/**
 * Options for slate selector hooks
 */
interface SlateSelectorOptions {
  /**
   * If true, defer calling the selector function until after `Editable` has
   * finished rendering. This ensures that `ReactEditor.findPath` won't return
   * an outdated path if called inside the selector.
   */
  deferred?: boolean;
}

Usage Examples:

import React from 'react';
import { useSlateSelector, useSlateStatic } from 'slate-react';
import { Editor, Node } from 'slate';

// Select only word count - only re-renders when word count changes
const WordCounter = () => {
  const wordCount = useSlateSelector((editor) => {
    const text = Node.string(editor);
    return text.split(/\s+/).filter(word => word.length > 0).length;
  });
  
  return <div>Words: {wordCount}</div>;
};

// Select only selection state - only re-renders when selection changes
const SelectionInfo = () => {
  const selectionInfo = useSlateSelector((editor) => {
    if (!editor.selection) return null;
    
    return {
      isCollapsed: Range.isCollapsed(editor.selection),
      path: editor.selection.anchor.path.join(',')
    };
  });
  
  if (!selectionInfo) return <div>No selection</div>;
  
  return (
    <div>
      Selection: {selectionInfo.isCollapsed ? 'Cursor' : 'Range'} 
      at [{selectionInfo.path}]
    </div>
  );
};

// Custom equality function for complex objects
const BlockInfo = () => {
  const blockInfo = useSlateSelector(
    (editor) => {
      const blocks = Array.from(Editor.nodes(editor, {
        match: n => Element.isElement(n) && Editor.isBlock(editor, n)
      }));
      return { count: blocks.length, types: blocks.map(([n]) => n.type) };
    },
    // Custom equality function - only update if count or types change
    (prev, curr) => {
      if (!prev || prev.count !== curr.count) return false;
      return JSON.stringify(prev.types) === JSON.stringify(curr.types);
    }
  );
  
  return (
    <div>
      Blocks: {blockInfo.count} 
      Types: {blockInfo.types.join(', ')}
    </div>
  );
};

// Deferred updates for expensive calculations
const ExpensiveCalculation = () => {
  const result = useSlateSelector(
    (editor) => {
      // Expensive calculation that might block UI
      return performExpensiveAnalysis(editor);
    },
    undefined,
    { deferred: true } // Defer updates to prevent blocking
  );
  
  return <div>Analysis: {result}</div>;
};

Chunking System for Large Documents

Slate React includes a chunking system that optimizes rendering of large documents by breaking them into smaller, manageable pieces.

/**
 * Chunk-related types for performance optimization
 */
interface ChunkTree {
  // Internal tree structure for optimizing large document rendering
}

type Chunk = ChunkLeaf | ChunkAncestor;
type ChunkLeaf = { type: 'leaf'; node: Node };
type ChunkAncestor = { type: 'ancestor'; children: Chunk[] };
type ChunkDescendant = ChunkLeaf | ChunkAncestor;

The chunking system is automatically handled by Slate React, but you can customize chunk rendering with the renderChunk prop:

import React from 'react';
import { Editable, RenderChunkProps } from 'slate-react';

const optimizedRenderChunk = ({ attributes, children, highest, lowest }: RenderChunkProps) => {
  // Add performance monitoring or custom styling for chunks
  return (
    <div 
      {...attributes}
      className={`chunk ${highest ? 'highest' : ''} ${lowest ? 'lowest' : ''}`}
    >
      {children}
    </div>
  );
};

const LargeDocumentEditor = () => (
  <Editable 
    renderChunk={optimizedRenderChunk}
    // Other props...
  />
);

Performance Monitoring and Analysis

Monitor editor performance and identify bottlenecks:

import React, { useCallback, useEffect } from 'react';
import { useSlateSelector, useSlateStatic } from 'slate-react';

const PerformanceMonitor = () => {
  const editor = useSlateStatic();
  
  // Monitor selection changes with performance timing
  const selectionMetrics = useSlateSelector((editor) => {
    const start = performance.now();
    const selection = editor.selection;
    const end = performance.now();
    
    return {
      selection,
      selectionTime: end - start,
      timestamp: Date.now()
    };
  });
  
  // Monitor document size changes
  const documentMetrics = useSlateSelector((editor) => {
    const start = performance.now();
    const nodeCount = Array.from(Node.nodes(editor)).length;
    const textLength = Node.string(editor).length;
    const end = performance.now();
    
    return {
      nodeCount,
      textLength,
      calculationTime: end - start
    };
  });
  
  useEffect(() => {
    console.log('Performance metrics:', {
      selection: selectionMetrics.selectionTime,
      document: documentMetrics.calculationTime
    });
  }, [selectionMetrics, documentMetrics]);
  
  return (
    <div>
      <div>Nodes: {documentMetrics.nodeCount}</div>
      <div>Characters: {documentMetrics.textLength}</div>
      <div>Calc Time: {documentMetrics.calculationTime.toFixed(2)}ms</div>
    </div>
  );
};

Advanced Performance Patterns

Memoized Render Functions

Optimize render functions with React.memo and useMemo:

import React, { memo, useMemo } from 'react';
import { RenderElementProps, RenderLeafProps } from 'slate-react';

// Memoized element renderer
const OptimizedElement = memo(({ attributes, children, element }: RenderElementProps) => {
  const className = useMemo(() => {
    return `element-${element.type} ${element.active ? 'active' : ''}`;
  }, [element.type, element.active]);
  
  return (
    <div {...attributes} className={className}>
      {children}
    </div>
  );
});

// Memoized leaf renderer with complex formatting
const OptimizedLeaf = memo(({ attributes, children, leaf }: RenderLeafProps) => {
  const style = useMemo(() => {
    const styles: React.CSSProperties = {};
    
    if (leaf.fontSize) styles.fontSize = `${leaf.fontSize}px`;
    if (leaf.color) styles.color = leaf.color;
    if (leaf.backgroundColor) styles.backgroundColor = leaf.backgroundColor;
    
    return styles;
  }, [leaf.fontSize, leaf.color, leaf.backgroundColor]);
  
  const formattedChildren = useMemo(() => {
    let result = children;
    
    if (leaf.bold) result = <strong>{result}</strong>;
    if (leaf.italic) result = <em>{result}</em>;
    if (leaf.underline) result = <u>{result}</u>;
    
    return result;
  }, [children, leaf.bold, leaf.italic, leaf.underline]);
  
  return (
    <span {...attributes} style={style}>
      {formattedChildren}
    </span>
  );
});

Virtual Scrolling for Large Documents

Implement virtual scrolling for documents with thousands of elements:

import React, { useMemo, useState, useEffect } from 'react';
import { useSlateSelector } from 'slate-react';
import { Editor, Node, Element } from 'slate';

const VirtualizedEditor = () => {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 100 });
  
  // Only select visible nodes to minimize re-renders
  const visibleNodes = useSlateSelector((editor) => {
    const allNodes = Array.from(Editor.nodes(editor, {
      match: n => Element.isElement(n)
    }));
    
    return allNodes.slice(visibleRange.start, visibleRange.end);
  });
  
  const handleScroll = (event: React.UIEvent) => {
    const target = event.target as HTMLElement;
    const scrollTop = target.scrollTop;
    const itemHeight = 50; // Estimated item height
    const containerHeight = target.clientHeight;
    
    const start = Math.floor(scrollTop / itemHeight);
    const end = start + Math.ceil(containerHeight / itemHeight) + 5; // Buffer
    
    setVisibleRange({ start, end });
  };
  
  return (
    <div onScroll={handleScroll} style={{ height: '400px', overflowY: 'auto' }}>
      {visibleNodes.map(([node, path]) => (
        <div key={path.join('-')} style={{ height: '50px' }}>
          {Node.string(node)}
        </div>
      ))}
    </div>
  );
};

Debounced Operations

Debounce expensive operations to improve performance:

import React, { useMemo } from 'react';
import { useSlateSelector } from 'slate-react';
import { debounce } from 'lodash';

const DebouncedAnalysis = () => {
  // Debounced expensive analysis
  const debouncedAnalysis = useMemo(
    () => debounce((editor: Editor) => {
      // Expensive analysis operation
      return performComplexAnalysis(editor);
    }, 300),
    []
  );
  
  const analysisResult = useSlateSelector((editor) => {
    debouncedAnalysis(editor);
    return 'Analysis in progress...';
  });
  
  return <div>{analysisResult}</div>;
};

Lazy Loading of Editor Features

Load editor features on demand to reduce initial bundle size:

import React, { lazy, Suspense, useState } from 'react';
import { Editable } from 'slate-react';

// Lazy load heavy features
const AdvancedToolbar = lazy(() => import('./AdvancedToolbar'));
const SpellChecker = lazy(() => import('./SpellChecker'));
const ImageUploader = lazy(() => import('./ImageUploader'));

const LazyEditor = () => {
  const [featuresEnabled, setFeaturesEnabled] = useState({
    toolbar: false,
    spellCheck: false,
    imageUpload: false
  });
  
  return (
    <div>
      <button onClick={() => setFeaturesEnabled(prev => ({ ...prev, toolbar: !prev.toolbar }))}>
        Toggle Toolbar
      </button>
      
      <Suspense fallback={<div>Loading...</div>}>
        {featuresEnabled.toolbar && <AdvancedToolbar />}
        {featuresEnabled.spellCheck && <SpellChecker />}
        {featuresEnabled.imageUpload && <ImageUploader />}
      </Suspense>
      
      <Editable />
    </div>
  );
};

Performance Best Practices

Selector Optimization

  • Use specific selectors that only select the data you need
  • Provide custom equality functions for complex objects
  • Use deferred updates for expensive calculations
  • Avoid selecting the entire editor state
// ✅ Good - specific selector
const wordCount = useSlateSelector(editor => 
  Node.string(editor).split(/\s+/).length
);

// ❌ Bad - selecting entire editor
const editor = useSlate(); // Re-renders on every change
const wordCount = Node.string(editor).split(/\s+/).length;

Component Optimization

  • Use React.memo for expensive components
  • Memoize expensive calculations with useMemo
  • Use useCallback for event handlers
// ✅ Optimized component
const OptimizedComponent = memo(({ element }) => {
  const expensiveValue = useMemo(() => 
    performExpensiveCalculation(element), 
    [element.key]
  );
  
  const handleClick = useCallback(() => {
    // Handle click
  }, []);
  
  return <div onClick={handleClick}>{expensiveValue}</div>;
});

Avoid Common Performance Pitfalls

  • Don't use useSlate in components that don't need to re-render
  • Don't perform expensive operations in render functions
  • Don't create new objects/functions in render without memoization
  • Use useSlateStatic for event handlers and operations
// ✅ Good performance
const ToolbarButton = () => {
  const editor = useSlateStatic(); // No re-renders
  
  const handleClick = useCallback(() => {
    Editor.addMark(editor, 'bold', true);
  }, [editor]);
  
  return <button onClick={handleClick}>Bold</button>;
};

// ❌ Poor performance
const BadToolbarButton = () => {
  const editor = useSlate(); // Re-renders on every editor change
  
  return (
    <button onClick={() => Editor.addMark(editor, 'bold', true)}>
      Bold
    </button>
  );
};

Install with Tessl CLI

npx tessl i tessl/npm-slate-react

docs

core-components.md

index.md

performance-optimization.md

plugin-system.md

react-hooks.md

render-functions.md

tile.json