Skip to content

perf(text): improve rich text performance#5658

Merged
steveruizok merged 12 commits intomainfrom
rich-text-perf-issue
Mar 19, 2025
Merged

perf(text): improve rich text performance#5658
steveruizok merged 12 commits intomainfrom
rich-text-perf-issue

Conversation

@steveruizok
Copy link
Copy Markdown
Collaborator

@steveruizok steveruizok commented Mar 15, 2025

This PR improves the performance of the app with regard to text.

Kapture.2025-03-15.at.14.22.33.mp4

The renderPlaintextFromRichText method is slow. It depends on TipTap's getText method. This parses the rich text and flattens it to plain text. As usual, we can improve performance by:

  1. making renderPlaintextFromRichText faster
  2. not calling renderPlaintextFromRichText

Making it faster

We can make it faster by using a weak cache so that we don't need to re-flatten text if we've flattened it before. We can also make it faster by skipping the flatten if the text content is empty. This can be done very quickly without using getText.

Avoiding it entirely

It turns out that most of the places where we call renderPlaintextFromRichText are checks against empty text, meaning we don't need to call renderPlaintextFromRichText at all. In these cases we just check whether the text is empty and continue based on that result.

Change type

  • improvement

Test plan

  1. Create shapes with rich text (Geo, Note, Text).
  2. Verify text rendering and editing still works correctly.
  3. Verify performance improvements when handling large amounts of text or empty text states.
  • Unit tests
  • End to end tests

Release notes

  • Improved performance related to rich text.

@steveruizok steveruizok requested a review from mimecuvalo March 15, 2025 13:53
@huppy-bot huppy-bot bot added the improvement Product improvement label Mar 15, 2025
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 15, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
examples ✅ Ready (Inspect) Visit Preview Mar 17, 2025 0:44am
1 Skipped Deployment
Name Status Preview Updated (UTC)
tldraw-docs ⬜️ Ignored (Inspect) Visit Preview Mar 17, 2025 0:44am

@steveruizok steveruizok added the sdk-hotfix-please ⚙️ Triggers SDK hotfix release after merge label Mar 15, 2025
Comment on lines +96 to +108
export function isEmptyRichText(richText: TLRichText) {
if (richText.content.length === 1) {
if (!(richText.content[0] as any).content) return true
}
return false
}

export function isSingleCharacterRichText(richText: TLRichText) {
if (richText.content.length === 1) {
if ((richText.content[0] as any)?.content?.length === 1) return true
}
return false
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are fine for us, but we should double-check they work with non-texty extensions people might add like images, mentions, or horizontal rules

const editor = useEditor()
const rInput = useRef<HTMLDivElement>(null)
const isEmpty = richText ? renderPlaintextFromRichText(editor, richText).length === 0 : true
const isEmpty = richText && !isEmptyRichText(richText)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patching this up, this was inverted

if (prevPlaintext && !nextPlaintext) {
const wasEmpty = isEmptyRichText(prev.props.richText)
const isEmpty = isEmptyRichText(next.props.richText)
if (wasEmpty && !isEmpty) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

going to fix this inversion too :)


export function isSingleCharacterRichText(richText: TLRichText) {
if (richText.content.length === 1) {
if ((richText.content[0] as any)?.content?.length === 1) return true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be checking the text

const isEmptyTextShape =
this.editor.isShapeOfType<TLTextShape>(editingShape, 'text') &&
renderPlaintextFromRichText(this.editor, editingShape.props.richText).trim() === ''
isEmptyRichText(editingShape.props.richText)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't quite the same - we want the shapes to be removed if they're just whitespace, going to revert this bit

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'd left the one in onShapeUpdate or EditEnd or whatever that deletes the shape, though with a note that we should see if we can do a version of tiptap/pm's getText that bails as soon as it finds any text, so that we don't block after editing large pieces of text.

return false
}

export function isSingleCharacterRichText(richText: TLRichText) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, i'm gonna nix this — it's too surgical and we call this rarely.

@steveruizok
Copy link
Copy Markdown
Collaborator Author

Looks good, landing! Thanks for the help @mime

@steveruizok steveruizok added this pull request to the merge queue Mar 19, 2025
Merged via the queue into main with commit f007270 Mar 19, 2025
12 checks passed
@steveruizok steveruizok deleted the rich-text-perf-issue branch March 19, 2025 13:03
github-merge-queue bot pushed a commit that referenced this pull request Mar 24, 2025
This PR improves the performance of shapes with rich text content.

## The bug

Due to reasons I cannot understand, we extract the fonts from rich text
on every frame while translating shapes. Maybe we shouldn't, I don't
know. Anyway, this is expensive when many shapes are moving around.
Actually, not even that many. Try it yourself!

## The fix

The fix here is the same as #5658, which is to skip the work if the text
is empt; or use a weak cache to re-use the result from last time.

### Change type

- [ ] `bugfix`
- [x] `improvement`
- [ ] `feature`
- [ ] `api`
- [ ] `other`

### Test plan

1. Create lots of geo shapes.
2. Drag all the geo shapes around.

### Release notes

- Improved performance while editing many geo shapes or text shapes.

---------

Co-authored-by: Mime Čuvalo <[email protected]>
@steveruizok steveruizok added the sdk Affects the tldraw sdk label Jan 2, 2026
@steveruizok steveruizok changed the title [Fix] Rich text perf issue perf(text): improve rich text performance Jan 2, 2026
@huppy-bot
Copy link
Copy Markdown
Contributor

huppy-bot bot commented Jan 2, 2026

Hey! 👋

This PR contains changes to api-report.api.md, which means you've made some API changes. To help reviewers and SDK users understand the impact, please add a "### API changes" section to your PR description.

Here's an example:

### API changes
- Describe what API changes were made
- List any breaking changes
- Note any new or removed parameters

Once you add this section, the check will pass automatically!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Product improvement sdk Affects the tldraw sdk sdk-hotfix-please ⚙️ Triggers SDK hotfix release after merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants