0
# Performance Optimization
1
2
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.
3
4
## Capabilities
5
6
### Selective Re-rendering with useSlateSelector
7
8
The `useSlateSelector` hook provides Redux-style selectors to prevent unnecessary component re-renders by only updating when specific parts of the editor state change.
9
10
```typescript { .api }
11
/**
12
* Use redux-style selectors to prevent re-rendering on every keystroke
13
* @param selector - Function to select specific data from editor
14
* @param equalityFn - Optional custom equality function for comparison
15
* @param options - Optional configuration options
16
* @returns Selected value from editor state
17
*/
18
function useSlateSelector<T>(
19
selector: (editor: Editor) => T,
20
equalityFn?: (a: T | null, b: T) => boolean,
21
options?: SlateSelectorOptions
22
): T;
23
24
/**
25
* Options for slate selector hooks
26
*/
27
interface SlateSelectorOptions {
28
/**
29
* If true, defer calling the selector function until after `Editable` has
30
* finished rendering. This ensures that `ReactEditor.findPath` won't return
31
* an outdated path if called inside the selector.
32
*/
33
deferred?: boolean;
34
}
35
```
36
37
**Usage Examples:**
38
39
```typescript
40
import React from 'react';
41
import { useSlateSelector, useSlateStatic } from 'slate-react';
42
import { Editor, Node } from 'slate';
43
44
// Select only word count - only re-renders when word count changes
45
const WordCounter = () => {
46
const wordCount = useSlateSelector((editor) => {
47
const text = Node.string(editor);
48
return text.split(/\s+/).filter(word => word.length > 0).length;
49
});
50
51
return <div>Words: {wordCount}</div>;
52
};
53
54
// Select only selection state - only re-renders when selection changes
55
const SelectionInfo = () => {
56
const selectionInfo = useSlateSelector((editor) => {
57
if (!editor.selection) return null;
58
59
return {
60
isCollapsed: Range.isCollapsed(editor.selection),
61
path: editor.selection.anchor.path.join(',')
62
};
63
});
64
65
if (!selectionInfo) return <div>No selection</div>;
66
67
return (
68
<div>
69
Selection: {selectionInfo.isCollapsed ? 'Cursor' : 'Range'}
70
at [{selectionInfo.path}]
71
</div>
72
);
73
};
74
75
// Custom equality function for complex objects
76
const BlockInfo = () => {
77
const blockInfo = useSlateSelector(
78
(editor) => {
79
const blocks = Array.from(Editor.nodes(editor, {
80
match: n => Element.isElement(n) && Editor.isBlock(editor, n)
81
}));
82
return { count: blocks.length, types: blocks.map(([n]) => n.type) };
83
},
84
// Custom equality function - only update if count or types change
85
(prev, curr) => {
86
if (!prev || prev.count !== curr.count) return false;
87
return JSON.stringify(prev.types) === JSON.stringify(curr.types);
88
}
89
);
90
91
return (
92
<div>
93
Blocks: {blockInfo.count}
94
Types: {blockInfo.types.join(', ')}
95
</div>
96
);
97
};
98
99
// Deferred updates for expensive calculations
100
const ExpensiveCalculation = () => {
101
const result = useSlateSelector(
102
(editor) => {
103
// Expensive calculation that might block UI
104
return performExpensiveAnalysis(editor);
105
},
106
undefined,
107
{ deferred: true } // Defer updates to prevent blocking
108
);
109
110
return <div>Analysis: {result}</div>;
111
};
112
```
113
114
### Chunking System for Large Documents
115
116
Slate React includes a chunking system that optimizes rendering of large documents by breaking them into smaller, manageable pieces.
117
118
```typescript { .api }
119
/**
120
* Chunk-related types for performance optimization
121
*/
122
interface ChunkTree {
123
// Internal tree structure for optimizing large document rendering
124
}
125
126
type Chunk = ChunkLeaf | ChunkAncestor;
127
type ChunkLeaf = { type: 'leaf'; node: Node };
128
type ChunkAncestor = { type: 'ancestor'; children: Chunk[] };
129
type ChunkDescendant = ChunkLeaf | ChunkAncestor;
130
```
131
132
The chunking system is automatically handled by Slate React, but you can customize chunk rendering with the `renderChunk` prop:
133
134
```typescript
135
import React from 'react';
136
import { Editable, RenderChunkProps } from 'slate-react';
137
138
const optimizedRenderChunk = ({ attributes, children, highest, lowest }: RenderChunkProps) => {
139
// Add performance monitoring or custom styling for chunks
140
return (
141
<div
142
{...attributes}
143
className={`chunk ${highest ? 'highest' : ''} ${lowest ? 'lowest' : ''}`}
144
>
145
{children}
146
</div>
147
);
148
};
149
150
const LargeDocumentEditor = () => (
151
<Editable
152
renderChunk={optimizedRenderChunk}
153
// Other props...
154
/>
155
);
156
```
157
158
### Performance Monitoring and Analysis
159
160
Monitor editor performance and identify bottlenecks:
161
162
```typescript
163
import React, { useCallback, useEffect } from 'react';
164
import { useSlateSelector, useSlateStatic } from 'slate-react';
165
166
const PerformanceMonitor = () => {
167
const editor = useSlateStatic();
168
169
// Monitor selection changes with performance timing
170
const selectionMetrics = useSlateSelector((editor) => {
171
const start = performance.now();
172
const selection = editor.selection;
173
const end = performance.now();
174
175
return {
176
selection,
177
selectionTime: end - start,
178
timestamp: Date.now()
179
};
180
});
181
182
// Monitor document size changes
183
const documentMetrics = useSlateSelector((editor) => {
184
const start = performance.now();
185
const nodeCount = Array.from(Node.nodes(editor)).length;
186
const textLength = Node.string(editor).length;
187
const end = performance.now();
188
189
return {
190
nodeCount,
191
textLength,
192
calculationTime: end - start
193
};
194
});
195
196
useEffect(() => {
197
console.log('Performance metrics:', {
198
selection: selectionMetrics.selectionTime,
199
document: documentMetrics.calculationTime
200
});
201
}, [selectionMetrics, documentMetrics]);
202
203
return (
204
<div>
205
<div>Nodes: {documentMetrics.nodeCount}</div>
206
<div>Characters: {documentMetrics.textLength}</div>
207
<div>Calc Time: {documentMetrics.calculationTime.toFixed(2)}ms</div>
208
</div>
209
);
210
};
211
```
212
213
## Advanced Performance Patterns
214
215
### Memoized Render Functions
216
217
Optimize render functions with React.memo and useMemo:
218
219
```typescript
220
import React, { memo, useMemo } from 'react';
221
import { RenderElementProps, RenderLeafProps } from 'slate-react';
222
223
// Memoized element renderer
224
const OptimizedElement = memo(({ attributes, children, element }: RenderElementProps) => {
225
const className = useMemo(() => {
226
return `element-${element.type} ${element.active ? 'active' : ''}`;
227
}, [element.type, element.active]);
228
229
return (
230
<div {...attributes} className={className}>
231
{children}
232
</div>
233
);
234
});
235
236
// Memoized leaf renderer with complex formatting
237
const OptimizedLeaf = memo(({ attributes, children, leaf }: RenderLeafProps) => {
238
const style = useMemo(() => {
239
const styles: React.CSSProperties = {};
240
241
if (leaf.fontSize) styles.fontSize = `${leaf.fontSize}px`;
242
if (leaf.color) styles.color = leaf.color;
243
if (leaf.backgroundColor) styles.backgroundColor = leaf.backgroundColor;
244
245
return styles;
246
}, [leaf.fontSize, leaf.color, leaf.backgroundColor]);
247
248
const formattedChildren = useMemo(() => {
249
let result = children;
250
251
if (leaf.bold) result = <strong>{result}</strong>;
252
if (leaf.italic) result = <em>{result}</em>;
253
if (leaf.underline) result = <u>{result}</u>;
254
255
return result;
256
}, [children, leaf.bold, leaf.italic, leaf.underline]);
257
258
return (
259
<span {...attributes} style={style}>
260
{formattedChildren}
261
</span>
262
);
263
});
264
```
265
266
### Virtual Scrolling for Large Documents
267
268
Implement virtual scrolling for documents with thousands of elements:
269
270
```typescript
271
import React, { useMemo, useState, useEffect } from 'react';
272
import { useSlateSelector } from 'slate-react';
273
import { Editor, Node, Element } from 'slate';
274
275
const VirtualizedEditor = () => {
276
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 100 });
277
278
// Only select visible nodes to minimize re-renders
279
const visibleNodes = useSlateSelector((editor) => {
280
const allNodes = Array.from(Editor.nodes(editor, {
281
match: n => Element.isElement(n)
282
}));
283
284
return allNodes.slice(visibleRange.start, visibleRange.end);
285
});
286
287
const handleScroll = (event: React.UIEvent) => {
288
const target = event.target as HTMLElement;
289
const scrollTop = target.scrollTop;
290
const itemHeight = 50; // Estimated item height
291
const containerHeight = target.clientHeight;
292
293
const start = Math.floor(scrollTop / itemHeight);
294
const end = start + Math.ceil(containerHeight / itemHeight) + 5; // Buffer
295
296
setVisibleRange({ start, end });
297
};
298
299
return (
300
<div onScroll={handleScroll} style={{ height: '400px', overflowY: 'auto' }}>
301
{visibleNodes.map(([node, path]) => (
302
<div key={path.join('-')} style={{ height: '50px' }}>
303
{Node.string(node)}
304
</div>
305
))}
306
</div>
307
);
308
};
309
```
310
311
### Debounced Operations
312
313
Debounce expensive operations to improve performance:
314
315
```typescript
316
import React, { useMemo } from 'react';
317
import { useSlateSelector } from 'slate-react';
318
import { debounce } from 'lodash';
319
320
const DebouncedAnalysis = () => {
321
// Debounced expensive analysis
322
const debouncedAnalysis = useMemo(
323
() => debounce((editor: Editor) => {
324
// Expensive analysis operation
325
return performComplexAnalysis(editor);
326
}, 300),
327
[]
328
);
329
330
const analysisResult = useSlateSelector((editor) => {
331
debouncedAnalysis(editor);
332
return 'Analysis in progress...';
333
});
334
335
return <div>{analysisResult}</div>;
336
};
337
```
338
339
### Lazy Loading of Editor Features
340
341
Load editor features on demand to reduce initial bundle size:
342
343
```typescript
344
import React, { lazy, Suspense, useState } from 'react';
345
import { Editable } from 'slate-react';
346
347
// Lazy load heavy features
348
const AdvancedToolbar = lazy(() => import('./AdvancedToolbar'));
349
const SpellChecker = lazy(() => import('./SpellChecker'));
350
const ImageUploader = lazy(() => import('./ImageUploader'));
351
352
const LazyEditor = () => {
353
const [featuresEnabled, setFeaturesEnabled] = useState({
354
toolbar: false,
355
spellCheck: false,
356
imageUpload: false
357
});
358
359
return (
360
<div>
361
<button onClick={() => setFeaturesEnabled(prev => ({ ...prev, toolbar: !prev.toolbar }))}>
362
Toggle Toolbar
363
</button>
364
365
<Suspense fallback={<div>Loading...</div>}>
366
{featuresEnabled.toolbar && <AdvancedToolbar />}
367
{featuresEnabled.spellCheck && <SpellChecker />}
368
{featuresEnabled.imageUpload && <ImageUploader />}
369
</Suspense>
370
371
<Editable />
372
</div>
373
);
374
};
375
```
376
377
## Performance Best Practices
378
379
### Selector Optimization
380
381
- Use specific selectors that only select the data you need
382
- Provide custom equality functions for complex objects
383
- Use deferred updates for expensive calculations
384
- Avoid selecting the entire editor state
385
386
```typescript
387
// ✅ Good - specific selector
388
const wordCount = useSlateSelector(editor =>
389
Node.string(editor).split(/\s+/).length
390
);
391
392
// ❌ Bad - selecting entire editor
393
const editor = useSlate(); // Re-renders on every change
394
const wordCount = Node.string(editor).split(/\s+/).length;
395
```
396
397
### Component Optimization
398
399
- Use React.memo for expensive components
400
- Memoize expensive calculations with useMemo
401
- Use useCallback for event handlers
402
403
```typescript
404
// ✅ Optimized component
405
const OptimizedComponent = memo(({ element }) => {
406
const expensiveValue = useMemo(() =>
407
performExpensiveCalculation(element),
408
[element.key]
409
);
410
411
const handleClick = useCallback(() => {
412
// Handle click
413
}, []);
414
415
return <div onClick={handleClick}>{expensiveValue}</div>;
416
});
417
```
418
419
### Avoid Common Performance Pitfalls
420
421
- Don't use useSlate in components that don't need to re-render
422
- Don't perform expensive operations in render functions
423
- Don't create new objects/functions in render without memoization
424
- Use useSlateStatic for event handlers and operations
425
426
```typescript
427
// ✅ Good performance
428
const ToolbarButton = () => {
429
const editor = useSlateStatic(); // No re-renders
430
431
const handleClick = useCallback(() => {
432
Editor.addMark(editor, 'bold', true);
433
}, [editor]);
434
435
return <button onClick={handleClick}>Bold</button>;
436
};
437
438
// ❌ Poor performance
439
const BadToolbarButton = () => {
440
const editor = useSlate(); // Re-renders on every editor change
441
442
return (
443
<button onClick={() => Editor.addMark(editor, 'bold', true)}>
444
Bold
445
</button>
446
);
447
};
448
```