or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-components.mdindex.mdperformance-optimization.mdplugin-system.mdreact-hooks.mdrender-functions.md

performance-optimization.mddocs/

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

```