Skip to main content
This guide covers performance optimization techniques for AppFlowy Editor, helping you build fast and responsive editing experiences.

Understanding Editor Performance

AppFlowy Editor’s performance depends on several factors:
  • Document Size: Number of nodes in the document
  • Rendering Complexity: Custom block components and styling
  • Transaction Frequency: How often changes are made
  • Selection Updates: Cursor movement and selection changes
  • Network Operations: For collaborative editing

Document Structure Optimization

Use Efficient Node Types

Choose the right node types for your content:
import 'package:appflowy_editor/appflowy_editor.dart';

// Efficient: Use TextNode for text content
final textNode = TextNode(
  type: 'paragraph',
  delta: Delta([TextInsert('Hello World')]),
);

// Avoid: Creating unnecessary nested structures
// Bad practice - adds complexity without benefit

Minimize Node Depth

Flat document structures perform better:
import 'package:appflowy_editor/appflowy_editor.dart';

// Good: Flat structure
final document = Document.blank()
  ..insert([0], [paragraphNode1])
  ..insert([1], [paragraphNode2])
  ..insert([2], [paragraphNode3]);

// Avoid: Unnecessary nesting
// Deep nesting increases traversal time

Lazy Loading for Large Documents

For very large documents, load content progressively:
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

class LazyEditor extends StatefulWidget {
  @override
  State<LazyEditor> createState() => _LazyEditorState();
}

class _LazyEditorState extends State<LazyEditor> {
  late EditorState editorState;
  int loadedChunks = 1;
  final int chunkSize = 100; // Load 100 nodes at a time

  @override
  void initState() {
    super.initState();
    editorState = EditorState(
      document: Document.blank(),
    );
    loadInitialContent();
  }

  void loadInitialContent() {
    // Load first chunk
    final nodes = generateNodes(0, chunkSize);
    final transaction = editorState.transaction;
    transaction.insertNodes([0], nodes);
    editorState.apply(transaction);
  }

  void loadMoreContent() {
    final startIndex = loadedChunks * chunkSize;
    final nodes = generateNodes(startIndex, chunkSize);
    
    final transaction = editorState.transaction;
    transaction.insertNodes([startIndex], nodes);
    editorState.apply(transaction);
    
    loadedChunks++;
  }

  List<Node> generateNodes(int start, int count) {
    // Generate nodes on demand
    return List.generate(count, (i) => paragraphNode(
      text: 'Paragraph ${start + i}',
    ));
  }

  @override
  Widget build(BuildContext context) {
    return AppFlowyEditor(
      editorState: editorState,
    );
  }
}

Transaction Optimization

Batch Multiple Operations

Combine multiple changes into a single transaction:
import 'package:appflowy_editor/appflowy_editor.dart';

// Good: Single transaction for multiple operations
void efficientUpdate(EditorState editorState) {
  final transaction = editorState.transaction;
  
  // Batch all operations
  transaction.insertText(textNode, 0, 'Hello');
  transaction.formatText(textNode, 0, 5, {'bold': true});
  transaction.insertNode([1], paragraphNode());
  
  // Apply once
  editorState.apply(transaction);
}

// Avoid: Multiple separate transactions
void inefficientUpdate(EditorState editorState) {
  // Bad: Each operation triggers a full update
  editorState.transaction
    ..insertText(textNode, 0, 'Hello')
    ..apply();
  
  editorState.transaction
    ..formatText(textNode, 0, 5, {'bold': true})
    ..apply();
  
  editorState.transaction
    ..insertNode([1], paragraphNode())
    ..apply();
}

Use In-Memory Updates

For temporary changes, use in-memory updates:
import 'package:appflowy_editor/appflowy_editor.dart';

// In-memory update (faster, not recorded in undo/redo)
editorState.apply(
  transaction,
  options: const ApplyOptions(
    inMemoryUpdate: true,
    recordUndo: false,
  ),
);

Debounce Rapid Changes

Debounce rapid updates like auto-save:
import 'package:appflowy_editor/appflowy_editor.dart';
import 'dart:async';

class DebouncedSaver {
  final EditorState editorState;
  Timer? _debounceTimer;
  final Duration delay;

  DebouncedSaver({
    required this.editorState,
    this.delay = const Duration(milliseconds: 500),
  }) {
    editorState.transactionStream.listen((_) {
      _debounceTimer?.cancel();
      _debounceTimer = Timer(delay, () {
        saveDocument();
      });
    });
  }

  void saveDocument() {
    // Perform save operation
    print('Saving document...');
  }

  void dispose() {
    _debounceTimer?.cancel();
  }
}

Rendering Optimization

Use Const Widgets

Use const constructors where possible:
import 'package:flutter/material.dart';

// Good: Const widget won't rebuild
const Divider(height: 1, color: Colors.grey);

// Avoid: Non-const creates new instance on every build
Divider(height: 1, color: Colors.grey);

Optimize Custom Block Components

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

class OptimizedCustomBlock extends BlockComponentBuilder {
  @override
  BlockComponentWidget build(BlockComponentContext blockComponentContext) {
    return OptimizedCustomBlockWidget(
      key: blockComponentContext.node.key,
      node: blockComponentContext.node,
      configuration: blockComponentContext.configuration,
      showActions: blockComponentContext.showActions,
      actionBuilder: blockComponentContext.actionBuilder,
    );
  }
}

class OptimizedCustomBlockWidget extends BlockComponentStatefulWidget {
  const OptimizedCustomBlockWidget({
    required super.key,
    required super.node,
    required super.configuration,
    required super.showActions,
    required super.actionBuilder,
  });

  @override
  State<OptimizedCustomBlockWidget> createState() => _OptimizedCustomBlockWidgetState();
}

class _OptimizedCustomBlockWidgetState extends State<OptimizedCustomBlockWidget> {
  @override
  Widget build(BuildContext context) {
    // Use RepaintBoundary for complex widgets
    return RepaintBoundary(
      child: YourCustomWidget(
        node: widget.node,
      ),
    );
  }
}

Use RepaintBoundary

Isolate expensive widgets from rebuilds:
import 'package:flutter/material.dart';

// Wrap expensive widgets
RepaintBoundary(
  child: ComplexVisualization(),
);

Selection Performance

Throttle Selection Updates

import 'package:appflowy_editor/appflowy_editor.dart';
import 'dart:async';

class ThrottledSelectionHandler {
  final EditorState editorState;
  final Duration throttleDuration;
  Timer? _throttleTimer;
  bool _isThrottled = false;

  ThrottledSelectionHandler({
    required this.editorState,
    this.throttleDuration = const Duration(milliseconds: 16), // ~60fps
  }) {
    editorState.selectionNotifier.addListener(_onSelectionChange);
  }

  void _onSelectionChange() {
    if (_isThrottled) return;
    
    _isThrottled = true;
    handleSelection(editorState.selection);
    
    _throttleTimer = Timer(throttleDuration, () {
      _isThrottled = false;
    });
  }

  void handleSelection(Selection? selection) {
    // Handle selection change
  }

  void dispose() {
    _throttleTimer?.cancel();
    editorState.selectionNotifier.removeListener(_onSelectionChange);
  }
}

Memory Management

Dispose Resources Properly

import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

class EditorPage extends StatefulWidget {
  @override
  State<EditorPage> createState() => _EditorPageState();
}

class _EditorPageState extends State<EditorPage> {
  late EditorState editorState;

  @override
  void initState() {
    super.initState();
    editorState = EditorState(document: Document.blank());
  }

  @override
  void dispose() {
    // Always dispose EditorState
    editorState.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AppFlowyEditor(editorState: editorState);
  }
}

Limit Undo History

import 'package:appflowy_editor/appflowy_editor.dart';

// Limit history size to prevent memory growth
final editorState = EditorState(
  document: Document.blank(),
  maxHistoryItemSize: 100, // Keep only last 100 operations
  minHistoryItemDuration: const Duration(milliseconds: 50),
);

Network Performance

Compress Transaction Data

For collaborative editing, compress transaction data:
import 'package:appflowy_editor/appflowy_editor.dart';
import 'dart:convert';
import 'dart:io';

Future<void> sendCompressedTransaction(Transaction transaction) async {
  final jsonData = jsonEncode(transaction.toJson());
  final compressed = gzip.encode(utf8.encode(jsonData));
  
  // Send compressed data
  await sendToServer(compressed);
}

Delta Synchronization

Send only changes, not entire documents:
import 'package:appflowy_editor/appflowy_editor.dart';

void syncChanges(EditorState editorState) {
  editorState.transactionStream.listen((event) {
    if (event.$1 == TransactionTime.before) {
      // Send only the transaction (delta)
      sendTransaction(event.$2);
    }
  });
}

Monitoring Performance

Use Performance Overlay

import 'package:flutter/material.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Enable performance overlay in debug mode
      showPerformanceOverlay: true,
      home: EditorPage(),
    );
  }
}

Profile Your App

# Run with performance profiling
flutter run --profile

# Use DevTools for detailed analysis
flutter pub global activate devtools
flutter pub global run devtools

Measure Transaction Time

import 'package:appflowy_editor/appflowy_editor.dart';

void measureTransactionPerformance(EditorState editorState) {
  editorState.transactionStream.listen((event) {
    final stopwatch = Stopwatch()..start();
    
    if (event.$1 == TransactionTime.before) {
      // Transaction starting
      stopwatch.start();
    } else if (event.$1 == TransactionTime.after) {
      // Transaction completed
      stopwatch.stop();
      print('Transaction took ${stopwatch.elapsedMilliseconds}ms');
    }
  });
}

Best Practices

  1. Batch Operations: Combine multiple changes into single transactions
  2. Debounce Updates: Delay non-critical updates like auto-save
  3. Lazy Load: Load large documents progressively
  4. Optimize Rendering: Use const widgets and RepaintBoundary
  5. Limit History: Set reasonable undo/redo limits
  6. Dispose Resources: Always dispose EditorState and listeners
  7. Profile Regularly: Use Flutter DevTools to identify bottlenecks
  8. Monitor Memory: Watch for memory leaks in long-running sessions

Performance Checklist

  • Transactions are batched when possible
  • Rapid updates are debounced or throttled
  • Large documents use lazy loading
  • Custom components use const constructors
  • RepaintBoundary wraps expensive widgets
  • Undo history has size limits
  • EditorState is properly disposed
  • Network data is compressed
  • Performance is profiled in production mode
  • Memory usage is monitored

Build docs developers (and LLMs) love